Invoicing lifecycle¶
This guide walks the full life of a customer invoice: draft, finalize, send, get paid, reconcile.
Create a draft¶
<?php
use Pennylane\Sdk\Pennylane;
$client = new Pennylane(); // reads PENNYLANE_API_TOKEN
$invoice = $client->customerInvoices->create(
date: '2026-07-08',
deadline: '2026-08-07',
customerId: 123,
invoiceLines: [
// With a product: unit price, VAT and label come from the catalog
['product_id' => 45, 'quantity' => '2'],
// Or a standard line
[
'label' => 'On-site setup',
'quantity' => '1',
'raw_currency_unit_price' => '500.00',
'unit' => 'day',
'vat_rate' => 'FR_200',
],
],
);
echo $invoice->status; // draft
Drafts can be edited (update()) and deleted (delete()) freely.
Invoice line variants
An invoice line is either product based (only product_id and quantity are required, everything else is filled in from the product) or standard (label, quantity, raw_currency_unit_price, unit and vat_rate are all required). unit is mandatory on standard lines: there is no fallback for it.
Amounts are numeric strings
The API transports amounts as strings to avoid float rounding. Pass "500.00", never a float. Response money fields (currencyAmount, tax, debit, credit, and so on) are typed as nullable strings for the same reason.
No idempotency on creations
Posting the same invoice twice creates a duplicate: the API does not deduplicate. For this reason the SDK never automatically retries a POST after a server error or a lost connection. Never blind retry a create call yourself either. If your pipeline can replay requests, deduplicate on your side, for instance by tracking your own externalReference and checking for it before you retry.
Finalize¶
Finalizing gives the invoice its final legal number. There is no undo (accounting rules), only credit notes.
$invoice = $client->customerInvoices->finalize($invoice->id);
echo $invoice->invoiceNumber; // e.g. F-2026-0042
Send¶
$client->customerInvoices->sendByEmail($invoice->id);
Or through the French e-invoicing network for B2B, see the e-invoicing guide:
$client->customerInvoices->sendToPa($invoice->id);
From a quote¶
$quote = $client->quotes->create(
date: '2026-07-08',
deadline: '2026-08-07',
customerId: 123,
invoiceLines: [/* ... */],
);
$client->quotes->sendByEmail($quote->id);
// once accepted:
$invoice = $client->customerInvoices->createFromQuote(quoteId: $quote->id, draft: true);
Get paid and reconcile¶
// Mark as paid manually
$client->customerInvoices->markAsPaid($invoice->id);
// Or match a real bank transaction against it
$client->customerInvoices->matchTransaction($invoice->id, transactionId: 987);
$client->customerInvoices->listMatchedTransactions($invoice->id);
// Payments recorded on the invoice
$client->customerInvoices->listPayments($invoice->id);
Credit notes¶
A credit note (avoir) is a customer invoice with negative amounts. Link it to the invoice it corrects:
$creditNote = $client->customerInvoices->create(
date: '2026-07-08',
deadline: '2026-08-07',
customerId: 123,
invoiceLines: [['product_id' => 45, 'quantity' => '-2']],
);
$client->customerInvoices->finalize($creditNote->id);
$client->customerInvoices->linkCreditNote($invoice->id, creditNoteId: $creditNote->id);
Import existing invoices¶
Invoices produced outside Pennylane (another tool, a marketplace) can be imported. For a plain PDF, upload the file first, then reference it:
use Pennylane\Sdk\FileUpload;
// 1. Upload the PDF as a file attachment
$attachment = $client->fileAttachments->create(FileUpload::fromPath('path/to/invoice.pdf'));
// 2. Import the invoice referencing it
$imported = $client->customerInvoices->importFromFile(
fileAttachmentId: $attachment->id,
date: '2026-07-01',
deadline: '2026-07-31',
customerId: 123,
currencyAmountBeforeTax: '100.00',
currencyAmount: '120.00',
currencyTax: '20.00',
invoiceLines: [/* ... */],
);
// Structured e-invoices (Factur-X, UBL, CII) upload directly
$imported = $client->customerInvoices->importEInvoice(FileUpload::fromPath('path/to/factur-x.pdf'));
Recurring invoices¶
$client->billingSubscriptions creates subscriptions that generate invoices on a schedule (weekly, monthly, yearly, and so on). See the API reference for the recurrence options.
Sync changes incrementally¶
Instead of re-listing everything, poll the change feed:
foreach ($client->changelogs->customerInvoices() as $event) {
// ...
}