Aller au contenu

Resource map: SDK design contract

This document is the binding naming contract between the OpenAPI specs (specs/) and the SDK surface. Every implementer MUST follow it exactly so that the whole SDK stays coherent. scripts/check-coverage.php audits that every spec operation appears in the resource classes and that every SDK call matches a spec operation.

Conventions

  • Writing style, everywhere (docblocks, comments, docs, commit messages): never use em dashes or en dashes. Use plain punctuation instead: colons, commas, parentheses, periods, or a simple hyphen. Keep the tone natural and human.
  • One resource class per top level URL namespace (grouped where noted), in src/Resources/Company/ or src/Resources/Firm/, one class per file (PSR-4).
  • Standard method names: list (GET collection, returns a cursor page), get (GET one), create (POST), update (PUT), delete (DELETE). Custom actions follow the path segment in camelCase: finalize, sendByEmail, markAsPaid, updateStatus. /import endpoints become importFromFile; /e_invoices/imports under a resource becomes importEInvoice.
  • Sub resource endpoint naming (the parent id is the first positional parameter):
Endpoint pattern Method name
GET .../{id}/categories listCategories($parentId)
PUT .../{id}/categories categorize($parentId, $categories)
GET .../{id}/appendices listAppendices($parentId)
POST .../{id}/appendices addAppendix($parentId, $file)
GET .../{id}/invoice_lines listInvoiceLines($parentId)
GET .../{id}/invoice_line_sections listInvoiceLineSections($parentId)
GET .../{id}/payments listPayments($parentId)
GET .../{id}/matched_transactions listMatchedTransactions($parentId)
POST .../{id}/matched_transactions matchTransaction($parentId, $transactionId)
DELETE .../{id}/matched_transactions/{tid} unmatchTransaction($parentId, $transactionId)
GET .../{id}/contacts listContacts($parentId)
GET .../{id}/custom_header_fields listCustomHeaderFields($parentId)
  • Resource methods call the protected helpers of AbstractResource with the path as a single double quoted string literal starting with /, interpolating variables only in braces form, for example "/customer_invoices/{$customerInvoiceId}/finalize". Never concatenation, sprintf or a variable holding the whole path. This is what scripts/check-coverage.php parses.
  • Helper choice: doGetPage for cursor lists, doGetNumberedPage for the three Firm page/per_page endpoints, doGet for single objects, doGetRaw when the response is not an object, doPost/doPut with a DTO class for typed responses, doPostVoid/doPutVoid when the response body is empty or irrelevant, doPostMultipart for file uploads, doDelete/doDeleteAs for deletions.
  • Method parameters are camelCase. Body keys are written literally in snake_case in the body array. Optional parameters default to null; the encoder drops null valued top level keys, so nothing else is needed.
  • Required spec parameters (path, query or body) become required PHP parameters; optional ones become nullable parameters with a null default.
  • anyOf request bodies (draft versus finalized invoice, for example) expose the union of the variants' top level fields as optional parameters, with the variants explained in the docblock.
  • Nested body structures (invoice_lines, ledger_entry_lines, categories, discount objects) stay associative arrays, typed with PHPStan array shapes declared as @phpstan-type aliases on the resource class.
  • Monetary amounts and quantities are strings end to end: numeric-string in array shapes and docblocks, ?string in DTOs, never float. vat_rate is a plain string, never a PHP enum: the spec enum gains values over time and the SDK tolerates spec drift by design. String only native parameter types paired with numeric-string docblocks are the canonical policy for all new endpoints: the spec types every monetary field as a pattern constrained string. Existing string|int unions stay as is for compatibility; new code must not add them.
  • List endpoints return CursorPage<T> (or NumberedPage<T> for the three Firm exceptions) through the page helpers.
  • Response DTOs live in src/Types/Company/ or src/Types/Firm/, one final class per file, extending PennylaneObject and following its documented hydration pattern: private constructor with promoted readonly properties, fromArray built from the base helpers, every field nullable except id (int, falling back to 0), ?string for money, ?\DateTimeImmutable for dates, nested DTOs and lists of DTOs hydrated with object/objectList. Tiny shared shapes (IdRef, IdUrlRef, ResourceLink) live in src/Types/ and are reused instead of duplicated.
  • Docblocks: English, one summary line, parameter notes where non obvious, the required OAuth scope on a Scope: line and the official reference URL on a Reference: line (https://pennylane.readme.io/reference/...). When two scopes grant access, list the :all scope first, connected by "or": Scope: <resource>:all or <resource>:readonly..
  • Beta endpoints (Hidden tag) and deprecated endpoints are implemented and documented as such ("Beta: subject to change." / "Deprecated: ...").

Company API (Pennylane): spec specs/company_v2.json

Base URL https://app.pennylane.com/api/external/v2, throttle 25 requests per 5 seconds.

Client property Classes Endpoints (path prefix)
customers (with ->companies, ->individuals sub resources) Customers, CompanyCustomers, IndividualCustomers /customers*, /company_customers*, /individual_customers*
customerInvoices CustomerInvoices /customer_invoices*: list, create, get, update, delete, importFromFile = /import, importEInvoice = /customer_invoices/e_invoices/imports, sendToPa, createFromQuote, finalize, markAsPaid, sendByEmail, linkCreditNote, updateImported, categories, appendices, invoice_lines, invoice_line_sections, payments, matched_transactions, custom_header_fields
quotes Quotes /quotes* (including updateStatus, sendByEmail, appendices, invoice_lines, invoice_line_sections)
commercialDocuments CommercialDocuments /commercial_documents*
customerInvoiceTemplates CustomerInvoiceTemplates /customer_invoice_templates
products Products /products*
billingSubscriptions BillingSubscriptions /billing_subscriptions*
suppliers Suppliers /suppliers*
supplierInvoices SupplierInvoices /supplier_invoices* (including importFromFile, importEInvoice, validateAccounting, updatePaymentStatus = /payment_status, updateEInvoiceStatus = /e_invoice_status, linkPurchaseRequests = /linked_purchase_requests)
purchaseRequests PurchaseRequests /purchase_requests* (importFromFile = /imports)
transactions Transactions /transactions* (including categories, matched_invoices)
bankAccounts, bankEstablishments BankAccounts, BankEstablishments /bank_accounts*, /bank_establishments
journals Journals /journals*
ledgerAccounts LedgerAccounts /ledger_accounts*
ledgerEntries, ledgerAttachments LedgerEntries, LedgerAttachments /ledger_entries*, /ledger_attachments (deprecated)
ledgerEntryLines LedgerEntryLines /ledger_entry_lines* (including letter = POST /lettering, unletter = DELETE /lettering, /lettered_ledger_entry_lines, categories)
trialBalance, fiscalYears TrialBalance, FiscalYears /trial_balance, /fiscal_years
categories, categoryGroups Categories, CategoryGroups /categories*, /category_groups*
exports (with ->fecs, ->generalLedgers, ->analyticalGeneralLedgers sub resources) Exports, FecExports, GeneralLedgerExports, AnalyticalGeneralLedgerExports /exports/*
sepaMandates, gocardlessMandates, proAccount SepaMandates, GocardlessMandates, ProAccount /sepa_mandates*, /gocardless_mandates*, /pro_account/*
eInvoices, paRegistrations EInvoices, PaRegistrations /e-invoices/imports (deprecated beta, the only kebab case path of the API), /pa_registrations
fileAttachments FileAttachments /file_attachments (multipart)
changelogs Changelogs /changelogs/{resource}, one method per resource: customerInvoices(), supplierInvoices(), customers(), suppliers(), products(), ledgerEntryLines(), transactions(), quotes()
webhookSubscriptions WebhookSubscriptions /webhook_subscriptions* (beta, hidden)
me Me /me (retrieve())

Firm API (PennylaneFirm): spec specs/firm_v1.json

Base URL https://app.pennylane.com/api/external/firm/v1, throttle 5 requests per second. Every method takes int $companyId as its first positional parameter, except the companies resource itself.

Client property Classes Endpoints
companies FirmCompanies /companies, /companies/{id} (page/per_page pagination)
fiscalYears, trialBalance, journals, ledgerAccounts, ledgerEntries, ledgerEntryLines FirmFiscalYears, FirmTrialBalance, FirmJournals, FirmLedgerAccounts, FirmLedgerEntries, FirmLedgerEntryLines /companies/{company_id}/fiscal_years*, /trial_balance, /journals*, /ledger_accounts*, /ledger_entries*, /ledger_entry_lines* (fiscal_years and trial_balance use page/per_page)
exports (with ->fecs, ->analyticalGeneralLedgers sub resources) FirmExports, FirmFecExports, FirmAnalyticalGeneralLedgerExports /companies/{company_id}/exports/*
dms (with ->files, ->folders sub resources), fileAttachments FirmDms, FirmDmsFiles, FirmDmsFolders, FirmFileAttachments /companies/{company_id}/dms/*, /file_attachments
customerInvoices, supplierInvoices, customers, suppliers FirmCustomerInvoices, FirmSupplierInvoices, FirmCustomers, FirmSuppliers /companies/{company_id}/customer_invoices*, /supplier_invoices*, /customers, /suppliers (read only, beta)
bankAccounts, transactions FirmBankAccounts, FirmTransactions /companies/{company_id}/bank_accounts*, /transactions*
categories, categoryGroups FirmCategories, FirmCategoryGroups /companies/{company_id}/categories*, /category_groups*
changelogs FirmChangelogs /companies/{company_id}/changelogs/*, methods dmsFiles(), ledgerEntryLines(), supplierInvoices(), customerInvoices()