Skip to content

Clients

The SDK has two entry points. Both are final classes with readonly resource properties, so every resource is available right after construction and cannot be reassigned.

Pennylane

Pennylane\Sdk\Pennylane talks to the Company API v2, scoped to a single company. Base URL: https://app.pennylane.com/api/external/v2. Requests are throttled to 25 requests per 5 seconds.

$client = new Pennylane\Sdk\Pennylane();

Constructor options

Option Type Default Description
apiToken ?string null The bearer token. Falls back to the PENNYLANE_API_TOKEN environment variable (checked in $_SERVER, $_ENV, then getenv()). Throws ConfigurationException when neither is set.
baseUrl ?string null (uses Pennylane::DEFAULT_BASE_URL) Must be https, except http://localhost, http://127.0.0.1 or http://[::1] for local development. Any other scheme throws ConfigurationException so the token is never sent in cleartext.
httpClient ?ClientInterface null Injected PSR-18 HTTP client. When omitted, the SDK uses Guzzle if installed (with redirects disabled and http_errors off), otherwise discovers any PSR-18 client via php-http/discovery. Disable automatic redirects on any client you inject yourself, since a redirect could leak the Authorization header.
requestFactory ?RequestFactoryInterface null Injected PSR-17 request factory. Discovered automatically when omitted.
streamFactory ?StreamFactoryInterface null Injected PSR-17 stream factory. Discovered automatically when omitted.
autoThrottle bool true Keeps outgoing requests under the API rate limit on the client side, so a loop over thousands of records never trips a 429. Only paces the current process.
maxRetries int 3 Retry budget for retries that are safe, meaning GET requests and connection level failures. The API has no idempotency mechanism, so a POST is never retried after a server error.
timeout ?float null Request timeout in seconds. Only has an effect on the SDK's default Guzzle client; passing it together with an injected httpClient throws ConfigurationException, since the SDK cannot enforce a timeout on a client it does not own.

Resource properties

Property Class Endpoint prefix
customers (with ->companies, ->individuals) Company\Customers, Company\CompanyCustomers, Company\IndividualCustomers /customers, /company_customers, /individual_customers
customerInvoices Company\CustomerInvoices /customer_invoices
customerInvoiceTemplates Company\CustomerInvoiceTemplates /customer_invoice_templates
quotes Company\Quotes /quotes
commercialDocuments Company\CommercialDocuments /commercial_documents
products Company\Products /products
billingSubscriptions Company\BillingSubscriptions /billing_subscriptions
suppliers Company\Suppliers /suppliers
supplierInvoices Company\SupplierInvoices /supplier_invoices
purchaseRequests Company\PurchaseRequests /purchase_requests
transactions Company\Transactions /transactions
bankAccounts Company\BankAccounts /bank_accounts
bankEstablishments Company\BankEstablishments /bank_establishments
journals Company\Journals /journals
ledgerAccounts Company\LedgerAccounts /ledger_accounts
ledgerEntries Company\LedgerEntries /ledger_entries
ledgerAttachments Company\LedgerAttachments /ledger_attachments (deprecated)
ledgerEntryLines Company\LedgerEntryLines /ledger_entry_lines
trialBalance Company\TrialBalance /trial_balance
fiscalYears Company\FiscalYears /fiscal_years
categories Company\Categories /categories
categoryGroups Company\CategoryGroups /category_groups
exports (with ->fecs, ->generalLedgers, ->analyticalGeneralLedgers) Company\Exports, Company\FecExports, Company\GeneralLedgerExports, Company\AnalyticalGeneralLedgerExports /exports/*
sepaMandates Company\SepaMandates /sepa_mandates
gocardlessMandates Company\GocardlessMandates /gocardless_mandates
proAccount Company\ProAccount /pro_account/*
eInvoices Company\EInvoices /e-invoices/imports (deprecated beta)
paRegistrations Company\PaRegistrations /pa_registrations
fileAttachments Company\FileAttachments /file_attachments
changelogs Company\Changelogs /changelogs/{resource}
webhookSubscriptions Company\WebhookSubscriptions /webhook_subscriptions (beta)
me Company\Me /me

PennylaneFirm

Pennylane\Sdk\PennylaneFirm talks to the Firm API v1, for accounting firms managing several client companies. Base URL: https://app.pennylane.com/api/external/firm/v1. Requests are throttled to 5 requests per second.

$firm = new Pennylane\Sdk\PennylaneFirm();

Every resource method except companies takes the client company id as its first argument: $firm->ledgerEntries->list($companyId).

Constructor options

PennylaneFirm accepts the exact same options as Pennylane, with one difference: apiToken falls back to the PENNYLANE_FIRM_API_TOKEN environment variable instead of PENNYLANE_API_TOKEN.

Option Type Default Description
apiToken ?string null Falls back to PENNYLANE_FIRM_API_TOKEN. Throws ConfigurationException when neither is set.
baseUrl ?string null (uses PennylaneFirm::DEFAULT_BASE_URL) Same https rule as Pennylane.
httpClient ?ClientInterface null Same as Pennylane.
requestFactory ?RequestFactoryInterface null Same as Pennylane.
streamFactory ?StreamFactoryInterface null Same as Pennylane.
autoThrottle bool true Paces requests to the Firm API limit (5 req/s) instead of the Company limit.
maxRetries int 3 Same retry policy as Pennylane.
timeout ?float null Same rule as Pennylane: incompatible with an injected httpClient.

Resource properties

Property Class Endpoint prefix
companies Firm\FirmCompanies /companies
fiscalYears Firm\FirmFiscalYears /companies/{company_id}/fiscal_years
trialBalance Firm\FirmTrialBalance /companies/{company_id}/trial_balance
journals Firm\FirmJournals /companies/{company_id}/journals
ledgerAccounts Firm\FirmLedgerAccounts /companies/{company_id}/ledger_accounts
ledgerEntries Firm\FirmLedgerEntries /companies/{company_id}/ledger_entries
ledgerEntryLines Firm\FirmLedgerEntryLines /companies/{company_id}/ledger_entry_lines
exports (with ->fecs, ->analyticalGeneralLedgers) Firm\FirmExports, Firm\FirmFecExports, Firm\FirmAnalyticalGeneralLedgerExports /companies/{company_id}/exports/*
dms (with ->files, ->folders) Firm\FirmDms, Firm\FirmDmsFiles, Firm\FirmDmsFolders /companies/{company_id}/dms/*
fileAttachments Firm\FirmFileAttachments /file_attachments
customerInvoices Firm\FirmCustomerInvoices /companies/{company_id}/customer_invoices
supplierInvoices Firm\FirmSupplierInvoices /companies/{company_id}/supplier_invoices
customers Firm\FirmCustomers /companies/{company_id}/customers (read only, beta)
suppliers Firm\FirmSuppliers /companies/{company_id}/suppliers (read only, beta)
bankAccounts Firm\FirmBankAccounts /companies/{company_id}/bank_accounts
transactions Firm\FirmTransactions /companies/{company_id}/transactions
categories Firm\FirmCategories /companies/{company_id}/categories
categoryGroups Firm\FirmCategoryGroups /companies/{company_id}/category_groups
changelogs Firm\FirmChangelogs /companies/{company_id}/changelogs/*

lastRateLimit()

Both clients expose the same method:

public function lastRateLimit(): ?RateLimitInfo

Returns the rate limit state read from the ratelimit-* response headers of the most recent API call, or null if no such header was present (for example, before any request has been made). This is purely informational: the SDK throttles proactively on the client side regardless of what this method returns.

RateLimitInfo (Pennylane\Sdk\RateLimitInfo) exposes three nullable properties:

Property Type Description
limit ?int Request budget of the current window (ratelimit-limit header).
remaining ?int Requests left in the current window (ratelimit-remaining header).
reset ?int Unix timestamp at which the window resets (ratelimit-reset header).
$client->customerInvoices->list();

$rateLimit = $client->lastRateLimit();
if ($rateLimit !== null && $rateLimit->remaining !== null) {
    echo "Requests left in the current window: {$rateLimit->remaining}\n";
}