Learn how to build common use cases using our Business API
This guide helps you build common use cases with our Business API, showcasing how to chain API requests to achieve real-world goals. Each section walks through the full request sequence: which endpoints to call, in what order, and what to watch out for.
You need a working authentication to follow this guide. Visit the Authentication section to get started.
All code examples target the Production environment (https://thirdparty.qonto.com/v2). To run them against the Sandbox, use the Sandbox base URL and include the X-Qonto-Staging-Token header.
1. How to send a SEPA transfer to a trusted beneficiary
Automating payments, such as supplier payouts, salary payments, or refunds to eliminate manual processing time and reduces the risk of human error. In this guide, we will list beneficiaries to find the right recipient, verify the payee identity through the Verification of Payee (VoP) service, and submit the transfer.
Example: A freelance management platform automatically pays contractors every Friday based on approved timesheets, without anyone logging into Qonto manually.
Prerequisites:
At least one beneficiary with status: validated.
Scopes: organization.read, payment.write
Beneficiaries can only be trusted through the Qonto web app, unless you are are an Embed partner with the beneficary.trust scope. Learn more about sensitive scopes.
1
List SEPA beneficiaries
Retrieve the list of validated beneficiaries and find the one you want to pay. You can filter by IBAN or name to narrow down the results.Endpoint: List SEPA beneficiaries
Before creating a transfer, you must verify the payee through Verification of Payee (VoP). This returns a proof_token that you will include in the transfer request.Endpoint: Verify a SEPA payee
OAuth scope required:payment.write
The proof_token is valid for 23 hours only. Always verify immediately before creating the transfer. Excessive verifications not followed by a transfer may be rate-limited.
Submit the transfer using the beneficiary_id from Step 1 and the proof_token from Step 2.Endpoint: Create a SEPA transfer
OAuth scope required:payment.write
Do not forget to include the X-Qonto-Idempotency-Key header to prevent duplicate transfers in case of network errors. See Idempotent requests.
To retrieve your bank_account_id, needed in this request, call GET /v2/organization or GET /v2/banks_accounts, the responses include a bank_accounts array, each with an id field.
Transfers above €30,000 require at least one attachment. Upload the attachment first via POST /v2/attachments and include its ID in the request.
Instant transfer limits: Instant transfers fall back to standard SEPA if the amount exceeds the limit. For trusted beneficiaries: €10,000 per transfer or €50,000 within 24h.
Example output:
[Step 1] GET /sepa/beneficiaries Found beneficiary: Admin (0199cd7a-xxxx-xxxx-xxxx-xxxxxxxxxxxx)[Step 2] POST /sepa/verify_payee Verification result: MATCH_RESULT_MATCH proof_token: 1|1|1773938795|MEUCI… (valid 23h)[Step 3] POST /sepa/transfers Transfer created: 019d06fd-xxxx-xxxx-xxxx-xxxxxxxxxxxx — status: pending
We now have a submitted SEPA transfer. The transfer id and status in the response confirm it has been accepted for processing.
Keeping your accounting or ERP system in sync with Qonto invoices avoids manual exports and ensures your financial records are always up to date. In this guide, we will fetch all client invoices updated since the last sync, paginate through results, download their PDF attachments, and store a timestamp to pick up exactly where we left off next time.
Example: An accounting software automatically imports new and updated client invoices from Qonto every hour, so the bookkeeper always sees the latest billing status without switching tools.
Prerequisites:
At least one client invoice exists in Qonto.
Scopes: client_invoices.read, attachment.read
Consider using webhooks instead of polling. The client invoice webhook notifies your server in real-time whenever an invoice is created or updated, no need to poll our API on a schedule. This reduces latency and API usage, and ensures your system reacts immediately to changes.
1
Initialize the sync timestamp
Before your first sync, define the earliest date from which you want to retrieve invoices. Store this timestamp in your database, you will update it after each successful sync.
Python
Node.js
from datetime import datetime, timezoneBASE_URL = "https://thirdparty.qonto.com/v2"headers = {"Authorization": "Bearer {your_access_token}"}# Load from your database, or use a start date for the first runlast_sync_at = "2026-01-01T00:00:00Z"current_sync_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
const BASE_URL = "https://thirdparty.qonto.com/v2";const headers = { Authorization: "Bearer {your_access_token}" };// Load from your database, or use a start date for the first runconst lastSyncAt = "2026-01-01T00:00:00Z";const currentSyncAt = new Date().toISOString();
2
Fetch all invoices updated in the time window
List client invoices updated between your last sync and now. Paginate through all pages before processing.Endpoint: List client invoices
OAuth scope required:client_invoices.read
Use per_page=100 for efficiency. The API returns a maximum of 100 records per page.
async function fetchAllClientInvoices(lastSyncAt, currentSyncAt) { const invoices = []; let page = 1; while (true) { const params = new URLSearchParams({ "filter[updated_at_from]": lastSyncAt, "filter[updated_at_to]": currentSyncAt, per_page: 100, page, }); const response = await fetch(`${BASE_URL}/client_invoices?${params}`, { headers }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); invoices.push(...data.client_invoices); if (data.meta.next_page === null) break; page = data.meta.next_page; } return invoices;}const invoices = await fetchAllClientInvoices(lastSyncAt, currentSyncAt);console.log(`Fetched ${invoices.length} invoices`);
3
Retrieve and download attachments
For each invoice that has an attachment_id, fetch the attachment to get its download URL, then download the file.Endpoint: Retrieve an attachment
Attachment download URLs are valid for 30 minutes only. Download the file immediately after retrieving the URL — do not store the URL for later use. If you need the file later, call the attachment endpoint again to get a fresh URL.
Attachments are generated asynchronously, roughly 10 seconds after invoice creation. If attachment_id is null on a recently created invoice, wait a few seconds and re-fetch that invoice before concluding the attachment is missing.
Python
Node.js
import osfor invoice in invoices: if not invoice.get("attachment_id"): continue # No attachment yet — may still be generating attachment_response = requests.get( f"{BASE_URL}/attachments/{invoice['attachment_id']}", headers=headers, ) attachment_response.raise_for_status() attachment = attachment_response.json()["attachment"] # Download the file immediately (URL expires in 30 minutes) file_response = requests.get(attachment["url"]) file_response.raise_for_status() file_path = f"./invoices/{attachment['file_name']}" os.makedirs("./invoices", exist_ok=True) with open(file_path, "wb") as f: f.write(file_response.content) print(f"Downloaded: {attachment['file_name']}")
import { writeFile, mkdir } from "node:fs/promises";import { join } from "node:path";for (const invoice of invoices) { if (!invoice.attachment_id) continue; // No attachment yet const attachmentResponse = await fetch( `${BASE_URL}/attachments/${invoice.attachment_id}`, { headers } ); if (!attachmentResponse.ok) throw new Error(`HTTP ${attachmentResponse.status}`); const { attachment } = await attachmentResponse.json(); // Download the file immediately (URL expires in 30 minutes) const fileResponse = await fetch(attachment.url); if (!fileResponse.ok) throw new Error(`Download failed: ${fileResponse.status}`); await mkdir("./invoices", { recursive: true }); const buffer = Buffer.from(await fileResponse.arrayBuffer()); await writeFile(join("./invoices", attachment.file_name), buffer); console.log(`Downloaded: ${attachment.file_name}`);}
4
Store the sync timestamp
Only after all invoices and attachments have been successfully processed, persist current_sync_at as the new last_sync_at in your database. This ensures you never miss an invoice if the sync fails midway.
Python
Node.js
# Persist to your database only after successful completionsave_to_db("last_client_invoice_sync_at", current_sync_at) # Use your own tools hereprint(f"Sync complete. Next run will start from: {current_sync_at}")
// Persist to your database only after successful completionawait saveToDB("last_client_invoice_sync_at", currentSyncAt); // Use your own tools hereconsole.log(`Sync complete. Next run will start from: ${currentSyncAt}`);
Example output:
[Step 1] Initialize sync timestamps lastSyncAt: 2026-03-19T00:00:00Z currentSyncAt: 2026-03-19T16:48:32.565Z[Step 2] GET /client_invoices (paginated) Fetched 2 invoices[Step 3] Download attachments Downloaded: invoice-1.pdf Downloaded: invoice-2.pdf[Step 4] Store sync timestamp Sync complete. Next run will start from: 2026-03-19T16:48:32.565Z (In production: persist currentSyncAt to your database here)
We now have an up-to-date local copy of all client invoices and their PDF attachments for the given time window.
Automatically pulling supplier invoices into your system removes the need for manual data entry and gives your finance team real-time visibility into what is owed and when. In this guide, we will fetch all supplier invoices updated since the last sync, paginate through results, and download their attachments; using the same polling pattern as client invoices, with different endpoints and filters.
Example: A spend management tool syncs supplier invoices from Qonto nightly to match them against purchase orders and flag any discrepancies before payment.
Prerequisites:
At least one supplier invoice exists in Qonto.
Scopes: supplier_invoice.read, attachment.read
1
Initialize the sync timestamp
Same approach as client invoices: load the last successful sync timestamp from your database and capture the current time.
Python
Node.js
from datetime import datetime, timezoneBASE_URL = "https://thirdparty.qonto.com/v2"headers = {"Authorization": "Bearer {your_access_token}"}last_sync_at = "2026-01-01T00:00:00Z" # Load from your databasecurrent_sync_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
const BASE_URL = "https://thirdparty.qonto.com/v2";const headers = { Authorization: "Bearer {your_access_token}" };const lastSyncAt = "2026-01-01T00:00:00Z"; // Load from your databaseconst currentSyncAt = new Date().toISOString();
2
Fetch all supplier invoices updated in the time window
List supplier invoices updated between your last sync and now, sorted chronologically and paginated.Endpoint: List supplier invoices
OAuth scope required:supplier_invoice.read.
Additional filters are available: filter[status] (e.g., to_review, to_pay, paid), filter[due_date], and filter[created_at_from] / filter[created_at_to].
async function fetchAllSupplierInvoices(lastSyncAt, currentSyncAt) { const invoices = []; let page = 1; while (true) { const params = new URLSearchParams({ "filter[updated_at_from]": lastSyncAt, "filter[updated_at_to]": currentSyncAt, sort_by: "updated_at:asc", // Process chronologically per_page: 100, page, }); const response = await fetch(`${BASE_URL}/supplier_invoices?${params}`, { headers }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); invoices.push(...data.supplier_invoices); if (data.meta.next_page === null) break; page = data.meta.next_page; } return invoices;}const invoices = await fetchAllSupplierInvoices(lastSyncAt, currentSyncAt);console.log(`Fetched ${invoices.length} supplier invoices`);
3
Retrieve and download attachments
For each invoice with an attachment_id, fetch the attachment and download the file. The attachment URL expires in 30 minutes — download immediately.Endpoint: Retrieve an attachment
OAuth scope required:attachment.read.
Python
Node.js
import osfor invoice in invoices: if not invoice.get("attachment_id"): continue attachment_response = requests.get( f"{BASE_URL}/attachments/{invoice['attachment_id']}", headers=headers, ) attachment_response.raise_for_status() attachment = attachment_response.json()["attachment"] file_response = requests.get(attachment["url"]) file_response.raise_for_status() os.makedirs("./supplier_invoices", exist_ok=True) with open(f"./supplier_invoices/{attachment['file_name']}", "wb") as f: f.write(file_response.content) print(f"Downloaded: {attachment['file_name']}")
Persist the new sync timestamp only after full success.
Python
Node.js
save_to_db("last_supplier_invoice_sync_at", current_sync_at) # Use your own tools hereprint(f"Sync complete. Next run will start from: {current_sync_at}")
await saveToDB("last_supplier_invoice_sync_at", currentSyncAt); // Use your own tools hereconsole.log(`Sync complete. Next run will start from: ${currentSyncAt}`);
Example output:
[Step 1] Initialize sync timestamps lastSyncAt: 2026-03-18T00:00:00Z currentSyncAt: 2026-03-19T16:50:12.227Z[Step 2] GET /supplier_invoices (paginated) Fetched 2 supplier invoices[Step 3] Download attachments Downloaded: supplier_invoice_1.pdf Downloaded: supplier_invoice_2.pdf[Step 4] Store sync timestamp Sync complete. Next run will start from: 2026-03-19T16:50:12.22
We now have an up-to-date local copy of all supplier invoices and their attachments for the given time window.
When your platform receives bills or purchase invoices, pushing them directly into Qonto saves your team from re-uploading files manually and keeps expense tracking centralised. In this guide, we will upload a supplier invoice file via the bulk endpoint and verify the response to confirm it was ingested successfully.
Example: A procurement platform forwards vendor invoices received by email directly into Qonto as soon as they arrive, ready for approval without any manual upload.
Prerequisites:
A PDF invoice file ready to upload (max 10 MB).
Scope: supplier_invoice.write
1
Upload the invoice file
Send the PDF using POST /v2/supplier_invoices/bulk. You can upload the raw file binary or reference an existing attachment by its ID.Endpoint: Create supplier invoices
OAuth scope required:supplier_invoice.write.
Maximum 20 invoices per request. Each invoice requires a unique idempotency_key (UUID recommended) to prevent duplicate uploads. See Idempotent requests.
This endpoint returns HTTP 200 even when individual invoices fail to process. Always inspect the errors array in the response body for each invoice when you receive a 200.Authentication failures still return 401, permission issues return 403, and server-side errors return 500.
You must provide eitherfile or attachment_id for each invoice, not both. Including both will result in an error.
We now have the supplier invoice uploaded to Qonto and queued for review. It will appear in the “Supplier invoices” section of the Qonto app with status to_review.
5. How to generate and download a Factur-X invoice
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.
Syncing transactions automatically into your accounting software or data warehouse eliminates manual reconciliation and gives your finance team an always-current view of cash flow. In this guide, we will retrieve all bank accounts, fetch transactions updated since the last sync across each account, and download any associated attachments, all without manual exports.
Example: An ERP integration pulls completed and pending transactions from Qonto every 15 minutes and automatically reconciles them against open purchase orders.
Prerequisites:
At least one active bank account in Qonto.
Scopes: organization.read, attachment.read.
Consider using webhooks instead of polling. The transactions webhook notifies your server in real-time whenever a transaction is created or updated, no need to poll our API on a schedule. This reduces latency and API usage, and ensures your system reacts immediately to changes.
1
Retrieve your bank accounts
Fetch the list of bank accounts to get their IDs. You can either retrieve the organization object (which includes accounts) or list accounts directly.Endpoint: List business accounts
For each bank account, list transactions updated since your last sync. Include includes[]=attachments to retrieve attachment metadata in the same request and avoid extra API calls.Endpoint: List transactions
OAuth scope required:organization.read
By default, only completed transactions are returned. If you need pending or declined transactions, you must explicitly include them using the status[] parameter.
Python
Node.js
from datetime import datetime, timezonelast_sync_at = "2026-01-01T00:00:00Z" # Load from your databasecurrent_sync_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")all_transactions = []for account in bank_accounts: page = 1 while True: response = requests.get( f"{BASE_URL}/transactions", headers=headers, params={ "bank_account_id": account["id"], "updated_at_from": last_sync_at, "updated_at_to": current_sync_at, "status[]": ["completed", "pending", "declined"], "includes[]": "attachments", "per_page": 100, "page": page, }, ) response.raise_for_status() data = response.json() all_transactions.extend(data["transactions"]) if data["meta"]["next_page"] is None: break page = data["meta"]["next_page"]print(f"Fetched {len(all_transactions)} transaction(s) across all accounts")
const lastSyncAt = "2026-01-01T00:00:00Z"; // Load from your databaseconst currentSyncAt = new Date().toISOString();const allTransactions = [];for (const account of bankAccounts) { let page = 1; while (true) { const params = new URLSearchParams({ bank_account_id: account.id, updated_at_from: lastSyncAt, updated_at_to: currentSyncAt, per_page: 100, page, }); params.append("status[]", "completed"); params.append("status[]", "pending"); params.append("status[]", "declined"); params.append("includes[]", "attachments"); const response = await fetch(`${BASE_URL}/transactions?${params}`, { headers }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); allTransactions.push(...data.transactions); if (data.meta.next_page === null) break; page = data.meta.next_page; }}console.log(`Fetched ${allTransactions.length} transaction(s) across all accounts`);
3
Download transaction attachments
When includes[]=attachments is used, each transaction includes an attachments array with pre-signed download URLs. Download them immediately, the URLs expire after 30 minutes.Endpoint: Retrieve an attachment
OAuth scope required:attachment.read
Do not store the attachment URLs for later use. If you need to re-download a file, call GET /v2/attachments/{id} to get a fresh URL.
Python
Node.js
import osfor transaction in all_transactions: for attachment in transaction.get("attachments", []): file_response = requests.get(attachment["url"]) file_response.raise_for_status() os.makedirs("./transaction_attachments", exist_ok=True) with open(f"./transaction_attachments/{attachment['file_name']}", "wb") as f: f.write(file_response.content) print(f"Downloaded: {attachment['file_name']}")
Persist the sync timestamp only after all transactions across all accounts have been successfully processed.
You can further filter transactions by side (credit or debit), operation_type[] (card, transfer, income, etc.), or settled_at_from / settled_at_to for settlement-date-based filtering.
Python
Node.js
save_to_db("last_transaction_sync_at", current_sync_at) # Use your own tools hereprint(f"Sync complete. Next run will start from: {current_sync_at}")
await saveToDB("last_transaction_sync_at", currentSyncAt); // Use your own tools hereconsole.log(`Sync complete. Next run will start from: ${currentSyncAt}`);
Example output:
[Step 1] GET /bank_accounts Found 6 bank account(s)[Step 2] GET /transactions (paginated, all accounts) Fetched 2 transaction(s) across 6 accounts[Step 3] Download transaction attachments Downloaded: Invoice-1.pdf Downloaded: Invoice-2.pdf[Step 4] Store sync timestamp Sync complete. Next run will start from: 2026-03-19T21:11:02.942Z (In production: persist currentSyncAt to your database here)
We now have an up-to-date local view of all transactions across all bank accounts, with their attachments downloaded.