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/orsrc/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./importendpoints becomeimportFromFile;/e_invoices/importsunder a resource becomesimportEInvoice. - 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
AbstractResourcewith 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 whatscripts/check-coverage.phpparses. - Helper choice:
doGetPagefor cursor lists,doGetNumberedPagefor the three Firm page/per_page endpoints,doGetfor single objects,doGetRawwhen the response is not an object,doPost/doPutwith a DTO class for typed responses,doPostVoid/doPutVoidwhen the response body is empty or irrelevant,doPostMultipartfor file uploads,doDelete/doDeleteAsfor 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.
anyOfrequest 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-typealiases on the resource class. - Monetary amounts and quantities are strings end to end:
numeric-stringin array shapes and docblocks,?stringin DTOs, never float.vat_rateis 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 withnumeric-stringdocblocks are the canonical policy for all new endpoints: the spec types every monetary field as a pattern constrained string. Existingstring|intunions stay as is for compatibility; new code must not add them. - List endpoints return
CursorPage<T>(orNumberedPage<T>for the three Firm exceptions) through the page helpers. - Response DTOs live in
src/Types/Company/orsrc/Types/Firm/, one final class per file, extendingPennylaneObjectand following its documented hydration pattern: private constructor with promoted readonly properties,fromArraybuilt from the base helpers, every field nullable exceptid(int, falling back to 0),?stringfor money,?\DateTimeImmutablefor dates, nested DTOs and lists of DTOs hydrated withobject/objectList. Tiny shared shapes (IdRef,IdUrlRef,ResourceLink) live insrc/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 aReference:line (https://pennylane.readme.io/reference/...). When two scopes grant access, list the:allscope first, connected by "or":Scope: <resource>:all or <resource>:readonly.. - Beta endpoints (
Hiddentag) 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() |