Generating invoices programmatically speeds up your billing cycle and ensures every invoice meets electronic invoicing standards without manual intervention. In this guide, we will find or create a client, draft and finalize an invoice, then poll until the Factur-X PDF (a standard PDF with embedded XML required for e-invoicing compliance) is ready to download.
Example: A SaaS platform automatically generates a Qonto invoice at the end of each billing period and sends the Factur-X PDF to the customer, with no manual step required.
Prerequisites:
E-invoicing must be enabled on your Qonto account.
This step is optional if you already know the IBAN of the account you want to use.
Python
Node.js
response = requests.get(f"{BASE_URL}/organization", headers=headers)response.raise_for_status()bank_accounts = response.json()["organization"]["bank_accounts"]# Use the first account, or filter by slug/currency as neededreceiving_iban = bank_accounts[0]["iban"]print(f"Receiving IBAN: {receiving_iban}")
const orgResponse = await fetch(`${BASE_URL}/organization`, { headers });if (!orgResponse.ok) throw new Error(`HTTP ${orgResponse.status}`);const { organization } = await orgResponse.json();// Use the first account, or filter by slug/currency as neededconst receivingIban = organization.bank_accounts[0].iban;console.log(`Receiving IBAN: ${receivingIban}`);
3
Create the invoice draft
Create the invoice in draft state. It will not yet be sent or assigned a number.Endpoint: Create a client invoice
Finalizing the invoice assigns it an official number, locks it for editing, and triggers the asynchronous generation of the Factur-X PDF.Endpoint: Finalize a client invoice
OAuth scope required:client_invoice.write
Python
Node.js
response = requests.post( f"{BASE_URL}/client_invoices/{invoice_id}/finalize", headers=headers,)response.raise_for_status()finalized_invoice = response.json()["client_invoice"]attachment_id = finalized_invoice.get("attachment_id") # May be None initiallyfinalized_at = finalized_invoice["finalized_at"]print(f"Invoice finalized at {finalized_at} — attachment: {attachment_id}")
The Factur-X PDF is generated asynchronously after finalization. The attachment_id may not be set immediately on the invoice — re-fetch the invoice until it appears, then poll GET /attachments/{id} until it returns 200. That is your signal to download.
Do not download the PDF until the attachment endpoint returns 200.
import timeMAX_ATTEMPTS = 15POLL_INTERVAL_SECONDS = 2attachment = Nonefor attempt in range(MAX_ATTEMPTS): # attachment_id may not be set immediately — re-fetch the invoice if needed if not attachment_id: invoice_response = requests.get( f"{BASE_URL}/client_invoices/{invoice_id}", headers=headers, ) invoice_response.raise_for_status() attachment_id = invoice_response.json()["client_invoice"].get("attachment_id") if not attachment_id: print(f"Attempt {attempt + 1}/{MAX_ATTEMPTS} — attachment_id not set yet, waiting...") time.sleep(POLL_INTERVAL_SECONDS) continue attachment_response = requests.get( f"{BASE_URL}/attachments/{attachment_id}", headers=headers, ) attachment_response.raise_for_status() attachment = attachment_response.json()["attachment"] print("Factur-X PDF is ready!") breakelse: raise TimeoutError("Factur-X PDF was not ready within the timeout period.")
const MAX_ATTEMPTS = 15;const POLL_INTERVAL_MS = 2000;let attachment;for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) { // attachment_id may not be set immediately — re-fetch the invoice if needed if (!attachmentId) { const invoiceResponse = await fetch( `${BASE_URL}/client_invoices/${invoiceId}`, { headers } ); if (!invoiceResponse.ok) throw new Error(`HTTP ${invoiceResponse.status}`); const { client_invoice } = await invoiceResponse.json(); attachmentId = client_invoice.attachment_id ?? null; if (!attachmentId) { console.log(`Attempt ${attempt + 1}/${MAX_ATTEMPTS} — attachment_id not set yet, waiting...`); await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)); continue; } } const attachmentResponse = await fetch( `${BASE_URL}/attachments/${attachmentId}`, { headers } ); if (!attachmentResponse.ok) throw new Error(`HTTP ${attachmentResponse.status}`); attachment = (await attachmentResponse.json()).attachment; console.log("Factur-X PDF is ready!"); break;}if (!attachment) { throw new Error("Factur-X PDF was not ready within the timeout period.");}
6
Download the Factur-X PDF
Download the file from the attachment URL. The URL is valid for 30 minutes, download immediately.
To verify the Factur-X XML is correctly embedded, open the PDF in Adobe Acrobat Reader and check for a paperclip icon, it should contain a factur-x.xml attachment.
See example PDF
Example output:
[Step 1] Find or create client Found existing client: b84a2798-xxxx-xxxx-xxxx-xxxxxxxxxxxx[Step 2] GET /organization Receiving IBAN: FR761695xxxxxxxxxxxxxxxxxxx[Step 3] POST /client_invoices (draft) Invoice draft created: 019d07ca-xxxx-xxxx-xxxx-xxxxxxxxxxxx[Step 4] POST /client_invoices/{id}/finalize Invoice finalized at 2026-03-19T20:30:01Z — attachment_id: null[Step 5] Poll GET /attachments/{id} until Factur-X is ready Attempt 1/15 — attachment_id not set yet, waiting... Attempt 2/15 - Factur-X PDF is ready![Step 6] Download Factur-X PDF Downloaded Factur-X PDF: ./client_invoices/2603-F-2026-0000291-Qonto-SA.pdf
We now have the finalized Factur-X PDF downloaded locally. It contains the embedded factur-x.xml file required for electronic invoicing compliance.