Invoicing lifecycle¶
This guide walks the full life of a customer invoice: draft, finalize, send, get paid, reconcile.
Create a draft¶
from pennylane_sdk import Pennylane
client = Pennylane()
invoice = client.customer_invoices.create(
customer_id=123,
date="2026-07-08",
deadline="2026-08-07",
invoice_lines=[
# With a product: unit price, VAT and label come from the catalog
{"product_id": 45, "quantity": "2"},
# Or a free line
{
"label": "On-site setup",
"quantity": "1",
"raw_currency_unit_price": "500.00",
"unit": "day",
"vat_rate": "FR_200",
},
],
)
print(invoice.status) # draft
Drafts can be edited (update) and deleted (delete) freely.
Amounts are strings or Decimals
The API transports amounts as strings to avoid float rounding. Pass Decimal("500.00") or "500.00"; the SDK serializes both correctly, and parses monetary response fields into Decimal.
Finalize¶
Finalizing gives the invoice its final legal number. There is no undo (accounting rules), only credit notes.
invoice = client.customer_invoices.finalize(invoice.id)
print(invoice.invoice_number) # e.g. F-2026-0042
Send¶
client.customer_invoices.send_by_email(invoice.id)
Or through the French e-invoicing network for B2B, see the e-invoicing guide:
client.customer_invoices.send_to_pa(invoice.id)
From a quote¶
quote = client.quotes.create(customer_id=123, invoice_lines=[...])
client.quotes.send_by_email(quote.id)
# once accepted:
invoice = client.customer_invoices.create_from_quote(quote_id=quote.id, draft=True)
Get paid and reconcile¶
# Mark as paid manually
client.customer_invoices.mark_as_paid(invoice.id)
# Or match a real bank transaction against it
client.customer_invoices.match_transaction(invoice.id, transaction_id=987)
client.customer_invoices.list_matched_transactions(invoice.id)
# Payments recorded on the invoice
client.customer_invoices.list_payments(invoice.id)
Credit notes¶
A credit note (avoir) is a customer invoice with negative amounts. Link it to the invoice it corrects:
credit_note = client.customer_invoices.create(
customer_id=123,
invoice_lines=[{"product_id": 45, "quantity": "-2"}],
)
client.customer_invoices.finalize(credit_note.id)
client.customer_invoices.link_credit_note(invoice.id, credit_note_id=credit_note.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:
# 1. Upload the PDF as a file attachment
attachment = client.file_attachments.create(file="path/to/invoice.pdf")
# 2. Import the invoice referencing it
imported = client.customer_invoices.import_from_file(
file_attachment_id=attachment.id,
date="2026-07-01",
deadline="2026-07-31",
customer_id=123,
currency_amount_before_tax="100.00",
currency_amount="120.00",
currency_tax="20.00",
invoice_lines=[...],
)
# Structured e-invoices (Factur-X, UBL, CII) upload directly
imported = client.customer_invoices.import_e_invoice(file="path/to/factur-x.pdf")
Recurring invoices¶
client.billing_subscriptions creates subscriptions that generate invoices on a schedule (weekly, monthly, yearly...). See the API reference for the recurrence options.
Sync changes incrementally¶
Instead of re-listing everything, poll the change feed:
for event in client.changelogs.customer_invoices():
...