Accounting operations¶
Beyond invoicing, the API exposes the accounting core: journals, chart of accounts, entries, lettering, trial balance and legal exports.
Chart of accounts and journals¶
<?php
use Pennylane\Sdk\Pennylane;
$client = new Pennylane(); // reads PENNYLANE_API_TOKEN
// Chart of accounts (plan comptable)
foreach ($client->ledgerAccounts->list() as $account) {
echo $account->number . ' ' . $account->label . PHP_EOL;
}
$client->ledgerAccounts->create(number: '706100', label: 'Prestations de services');
// Journals
foreach ($client->journals->list() as $journal) {
echo $journal->code . ' ' . $journal->label . PHP_EOL;
}
Ledger entries (écritures)¶
An entry is a balanced set of debit and credit lines:
$entry = $client->ledgerEntries->create(
date: '2026-07-08',
label: 'Vente prestation',
journalId: 12,
ledgerEntryLines: [
['ledger_account_id' => 411, 'debit' => '120.00', 'credit' => '0.00'],
['ledger_account_id' => 706, 'debit' => '0.00', 'credit' => '100.00'],
['ledger_account_id' => 445, 'debit' => '0.00', 'credit' => '20.00'],
],
);
Debit and credit are both required
Every ledger entry line must carry both a debit and a credit key, even though only one side is actually used: set the unused side to "0.00". The lines must be balanced, meaning total debits equal total credits across the whole entry. If they do not, the API answers 422 and the SDK raises ValidationException with the totals in $exception->details.
No idempotency on creations
Posting the same ledger entry twice creates a duplicate: the API does not deduplicate. The SDK never auto-retries a POST after a server error for this reason. Deduplicate on your side if your pipeline can replay requests.
Lettering (lettrage)¶
Lettering links entry lines that settle each other, typically an invoice line with its payment line:
$client->ledgerEntryLines->letter(
ledgerEntryLineIds: [111, 222],
unbalancedLetteringStrategy: 'none',
);
$client->ledgerEntryLines->unletter(
ledgerEntryLineIds: [111, 222],
unbalancedLetteringStrategy: 'none',
);
$client->ledgerEntryLines->listLetteredLines(111);
unbalancedLetteringStrategy is "none" to reject an unbalanced lettering with an error, or "partial" to allow it.
Trial balance (balance générale)¶
foreach ($client->trialBalance->list(
periodStart: '2026-01-01',
periodEnd: '2026-06-30',
) as $row) {
echo $row->number . ' ' . $row->debits . ' ' . $row->credits . PHP_EOL;
}
Period bounds are required
Unlike most list endpoints, periodStart and periodEnd are required arguments here, not optional filters: the trial balance always needs an explicit period to compute.
Analytical categories¶
Pennylane's analytics let you tag almost everything (invoices, transactions, entry lines) with categories organized in groups (axes):
$groups = $client->categoryGroups->list();
$client->categories->create(label: 'Agence Lyon', categoryGroupId: 3);
$client->customerInvoices->categorize(
$invoiceId,
categories: [['id' => 42, 'weight' => '1.0']],
);
Ledger entry lines carry their own analytical categories:
$client->ledgerEntryLines->categorize(
$ledgerEntryLineId,
categories: [['id' => 42, 'weight' => '1.0']],
);
Legal exports: FEC, general ledger¶
Exports run asynchronously: create the export, then poll until the file is ready.
$export = $client->exports->fecs->create(periodStart: '2026-01-01', periodEnd: '2026-12-31');
while (!in_array($export->status, ['ready', 'failed'], true)) {
sleep(5);
$export = $client->exports->fecs->get($export->id);
}
echo $export->fileUrl; // download link
Same pattern for $client->exports->generalLedgers and $client->exports->analyticalGeneralLedgers.
Bank data¶
use Pennylane\Sdk\Filter;
foreach ($client->bankAccounts->list() as $account) {
// ...
}
foreach ($client->transactions->list(filter: [Filter::gte('date', '2026-01-01')]) as $tx) {
// ...
}
// Reconcile: which invoices does this transaction settle?
$client->transactions->listMatchedInvoices($tx->id);