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.
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.from datetime import datetime, timezone
BASE_URL = "https://thirdparty.qonto.com/v2"
headers = {"Authorization": "Bearer {your_access_token}"}
# Load from your database, or use a start date for the first run
last_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 run
const lastSyncAt = "2026-01-01T00:00:00Z";
const currentSyncAt = new Date().toISOString();
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 invoicesOAuth scope required: client_invoices.read
Use per_page=100 for efficiency. The API returns a maximum of 100 records per page.
def fetch_all_client_invoices(last_sync_at, current_sync_at):
invoices = []
page = 1
while True:
response = requests.get(
f"{BASE_URL}/client_invoices",
headers=headers,
params={
"filter[updated_at_from]": last_sync_at,
"filter[updated_at_to]": current_sync_at,
"per_page": 100,
"page": page,
},
)
response.raise_for_status()
data = response.json()
invoices.extend(data["client_invoices"])
if data["meta"]["next_page"] is None:
break
page = data["meta"]["next_page"]
return invoices
invoices = fetch_all_client_invoices(last_sync_at, current_sync_at)
print(f"Fetched {len(invoices)} invoices")
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`);
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 attachmentAttachment 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.
import os
for 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}`);
}
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.# Persist to your database only after successful completion
save_to_db("last_client_invoice_sync_at", current_sync_at) # Use your own tools here
print(f"Sync complete. Next run will start from: {current_sync_at}")
// Persist to your database only after successful completion
await saveToDB("last_client_invoice_sync_at", currentSyncAt); // Use your own tools here
console.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.