Skip to content

Firm API (accounting firms)

Accounting firms (cabinets d'expertise comptable) get a dedicated API to work across their whole client portfolio: list the companies they manage, read and post accounting entries, upload documents, run exports, all per client company.

Clients and token

<?php

use Pennylane\Sdk\PennylaneFirm;

$firm = new PennylaneFirm(); // reads PENNYLANE_FIRM_API_TOKEN

The token is generated from your firm account (no sandbox needed). The Firm API is rate limited at 5 requests per second; the SDK throttles accordingly.

A firm token reaches real client companies

There is no sandbox for the Firm API: every call authenticated with a firm token acts on the live data of a real client company. Test carefully, especially before posting ledger entries or running exports, since a mistake affects a client's actual books, not a throwaway test account.

Iterate your portfolio

foreach ($firm->companies->list() as $company) {
    echo $company->id . ' ' . $company->name . ' ' . $company->siren . PHP_EOL;
}

Every other resource takes the company id as its first argument:

$companyId = 4217;

// Fiscal years
foreach ($firm->fiscalYears->list($companyId) as $fy) {
    echo $fy->start->format('Y-m-d') . ' ' . $fy->finish->format('Y-m-d') . PHP_EOL;
}

// Trial balance over a period
foreach ($firm->trialBalance->list(
    $companyId,
    periodStart: '2026-01-01',
    periodEnd: '2026-06-30',
) as $row) {
    echo $row->number . ' ' . $row->debits . ' ' . $row->credits . PHP_EOL;
}

Three endpoints paginate by page number

Most list endpoints paginate with a cursor, but three Firm API endpoints instead paginate with page and perPage: companies, fiscalYears and trialBalance. They return a NumberedPage with totalPages and currentPage properties instead of the usual CursorPage. Iterating with foreach walks every page transparently either way.

Post accounting entries

$firm->ledgerEntries->create(
    $companyId,
    date: '2026-07-08',
    label: 'OD de régularisation',
    journalId: 12,
    ledgerEntryLines: [
        ['ledger_account_id' => 411, 'debit' => '120.00', 'credit' => '0.00'],
        ['ledger_account_id' => 706, 'debit' => '0.00', 'credit' => '120.00'],
    ],
);

As on the Company API, every ledger entry line needs both a debit and a credit key ("0.00" on the unused side), and the lines must be balanced.

Documents (DMS)

The firm-only DMS resource manages the document library of each client company:

use Pennylane\Sdk\FileUpload;

$firm->dms->folders->list($companyId);
$firm->dms->files->upload($companyId, FileUpload::fromPath('path/to/piece.pdf'));

Exports

$export = $firm->exports->fecs->create($companyId, periodStart: '2026-01-01', periodEnd: '2026-12-31');
$export = $firm->exports->fecs->get($companyId, $export->id); // poll until status is "done"

Read-only beta invoicing endpoints

The Firm API can read a client company's customer and supplier invoices, but this surface is in beta and read-only: list() and get() only, no creation or state changes.

foreach ($firm->customerInvoices->list($companyId) as $invoice) {
    // ...
}
$firm->supplierInvoices->get($companyId, $supplierInvoiceId);

Invoice creation and lifecycle actions (finalize, send, mark as paid, and so on) happen through each company's own Company API, with its own PENNYLANE_API_TOKEN.

Mass synchronization

Changelogs let you sync many client companies incrementally without re-reading everything:

foreach ($firm->companies->list() as $company) {
    foreach ($firm->changelogs->ledgerEntryLines($company->id) as $event) {
        // ...
    }
}

Company vs Firm scopes

The Firm API is read-write on accounting (entries, accounts, journals, banking) but read-only on invoicing (customer and supplier invoices, in beta). Invoice creation happens through each company's own Company API.