Skip to main content
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
OAuth scope required: organization.read
import requests

BASE_URL = "https://thirdparty.qonto.com/v2"
headers = {"Authorization": "Bearer {your_access_token}"}

response = requests.get(
    f"{BASE_URL}/sepa/beneficiaries",
    headers=headers,
    params={
        "iban[]": "FR7616798000010000005663951",  # Filter by IBAN
        "status[]": "validated",
        "per_page": 25,
    },
)
response.raise_for_status()

beneficiaries = response.json()["beneficiaries"]
beneficiary = beneficiaries[0]
beneficiary_id = beneficiary["id"]
beneficiary_name = beneficiary["name"]
print(f"Found beneficiary: {beneficiary['name']} ({beneficiary_id})")
2

Verify the SEPA payee (VoP)

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.
response = requests.post(
    f"{BASE_URL}/sepa/verify_payee",
    headers={**headers, "Content-Type": "application/json"},
    json={
        "iban": "FR7616958000014849440866435",
        "beneficiary_name": beneficiary_name,
    },
)
response.raise_for_status()

data = response.json()
proof_token = data["proof_token"]["token"]
match_result = data["match_result"]

# match_result values:
# MATCH_RESULT_MATCH       → proceed with confidence
# MATCH_RESULT_CLOSE_MATCH → consider reviewing the name
# MATCH_RESULT_NO_MATCH    → caution, consider skipping transfer
# MATCH_RESULT_NOT_POSSIBLE → verification unavailable for this IBAN
print(f"Verification result: {match_result}")
3

Create the SEPA transfer

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.
import uuid

response = requests.post(
    f"{BASE_URL}/sepa/transfers",
    headers={
        **headers,
        "Content-Type": "application/json",
        "X-Qonto-Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "vop_proof_token": proof_token,
        "transfer": {
            "bank_account_id": "{your_bank_account_id}",
            "reference": "Invoice payment INV-001",
            "amount": "1100.50",
            "beneficiary_id": beneficiary_id,
            # "scheduled_date": "2026-04-01",  # Optional: ISO 8601 date
            # "attachment_ids": ["uuid-1"],     # Required if amount > €30,000
        },
    },
)
response.raise_for_status()

transfer = response.json()["transfer"]
print(f"Transfer created: {transfer['id']} — status: {transfer['status']}")
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.

2. How to sync client invoices and attachments

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.
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")
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.
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")
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.
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']}")
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.
# 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}")
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.

3. How to sync supplier invoices and attachments

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.
from datetime import datetime, timezone

BASE_URL = "https://thirdparty.qonto.com/v2"
headers = {"Authorization": "Bearer {your_access_token}"}

last_sync_at = "2026-01-01T00:00:00Z"  # Load from your database
current_sync_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
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].
def fetch_all_supplier_invoices(last_sync_at, current_sync_at):
    invoices = []
    page = 1

    while True:
        response = requests.get(
            f"{BASE_URL}/supplier_invoices",
            headers=headers,
            params={
                "filter[updated_at_from]": last_sync_at,
                "filter[updated_at_to]": current_sync_at,
                "sort_by": "updated_at:asc",  # Process chronologically
                "per_page": 100,
                "page": page,
            },
        )
        response.raise_for_status()
        data = response.json()

        invoices.extend(data["supplier_invoices"])

        if data["meta"]["next_page"] is None:
            break
        page = data["meta"]["next_page"]

    return invoices

invoices = fetch_all_supplier_invoices(last_sync_at, current_sync_at)
print(f"Fetched {len(invoices)} 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.
import os

for 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']}")
4

Store the sync timestamp

Persist the new sync timestamp only after full success.
save_to_db("last_supplier_invoice_sync_at", current_sync_at) # Use your own tools here
print(f"Sync complete. Next run will start from: {current_sync_at}")
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.

4. How to bulk upload supplier invoices

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 either file or attachment_id for each invoice, not both. Including both will result in an error.
import uuid
import requests

BASE_URL = "https://thirdparty.qonto.com/v2"
headers = {"Authorization": "Bearer {your_access_token}"}

with open("./invoice.pdf", "rb") as f:
    response = requests.post(
        f"{BASE_URL}/supplier_invoices/bulk",
        headers=headers,
        data={
            "supplier_invoices[][idempotency_key]": str(uuid.uuid4()),
            "source": "integration",
        },
        files={
            "supplier_invoices[][file]": ("invoice.pdf", f, "application/pdf"),
        },
    )

# Always 200 — check errors in body
result = response.json()
2

Check the response for errors

Inspect the response to confirm each invoice was processed successfully.
supplier_invoices = result.get("supplier_invoices", [])

for invoice in supplier_invoices:
    errors = invoice.get("errors", [])
    if errors:
        print(f"Failed: {errors}")
    else:
        print(f"Uploaded successfully: {invoice.get('id')}")
Common error codes to handle:
CodeCause
invalidFile too large or unsupported content type
requiredMissing idempotency_key
limit_reachedMore than 20 invoices in the request
internal_server_errorProcessing failed on Qonto’s side
Example output:
[Step 1] POST /supplier_invoices/bulk  (file: sample_invoice.pdf)
  HTTP status: 200

[Step 2] Check response
  Uploaded successfully: 019d0705-xxxx-xxxx-xxxx-xxxxxxxxxxxx
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.
  • Scopes: client.read, client.write, client_invoice.read, client_invoice.write, attachment.read, organization.read (optional).
1

Find or create the client

Look up an existing client by email or create a new one. Save the client_id — you’ll need it in the next step.Endpoint: List clients · Create a client
OAuth scopes required: client.read, client.write
import requests

BASE_URL = "https://thirdparty.qonto.com/v2"
headers = {
    "Authorization": "Bearer {your_access_token}",
    "Content-Type": "application/json",
}

# Option A: Find an existing client
search_response = requests.get(
    f"{BASE_URL}/clients",
    headers=headers,
    params={"filter[email]": "contact@acmecorp.com"},
)
search_response.raise_for_status()
clients = search_response.json()["clients"]

if clients:
    client_id = clients[0]["id"]
    print(f"Found existing client: {client_id}")
else:
    # Option B: Create a new client
    create_response = requests.post(
        f"{BASE_URL}/clients",
        headers=headers,
        json={
            "name": "Acme Corp",
            "kind": "company",
            "email": "contact@acmecorp.com",
            "currency": "EUR",   # Required
            "locale": "en",      # Required
            "tax_identification_number": "123456789", # Required for creating client invoices
            "billing_address": {
                "street_address": "1 Market Street",
                "city": "Paris",
                "zip_code": "75001",
                "country_code": "FR",
            },
        },
    )
    create_response.raise_for_status()
    client_id = create_response.json()["client"]["id"]
    print(f"Created new client: {client_id}")
2

Get your receiving bank account IBAN

The invoice must reference the Qonto bank account where you want to receive payment. Fetch your organization to retrieve the IBAN of the account you’ll use.Endpoint: Retrieve the authenticated organization and list bank accounts
OAuth scope required: organization.read
This step is optional if you already know the IBAN of the account you want to use.
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 needed
receiving_iban = bank_accounts[0]["iban"]
print(f"Receiving IBAN: {receiving_iban}")
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
OAuth scope required: client_invoice.write
from datetime import date, timedelta

today = date.today()

response = requests.post(
    f"{BASE_URL}/client_invoices",
    headers=headers,
    json={
        "client_id": client_id,
        "issue_date": today.isoformat(),
        "due_date": (today + timedelta(days=30)).isoformat(),
        "status": "draft",
        "payment_methods": {
            "iban": receiving_iban,
        },
        "items": [
            {
                "title": "Consulting Services — March 2026",
                "quantity": "10",
                "unit": "hour",
                "unit_price": {"value": "150.00", "currency": "EUR"},
                "vat_rate": "0.20",
            }
        ],
    },
)
response.raise_for_status()

invoice = response.json()["client_invoice"]
invoice_id = invoice["id"]
print(f"Invoice draft created: {invoice_id}")
4

Finalize the 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
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 initially
finalized_at = finalized_invoice["finalized_at"]
print(f"Invoice finalized at {finalized_at} — attachment: {attachment_id}")
5

Poll until the Factur-X PDF is ready

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.
Endpoints: Retrieve a client invoice · Retrieve an attachment
OAuth scope required: client_invoice.read, attachment.read
import time

MAX_ATTEMPTS = 15
POLL_INTERVAL_SECONDS = 2

attachment = None

for 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!")
    break
else:
    raise TimeoutError("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.
import os

file_response = requests.get(attachment["url"])
file_response.raise_for_status()

os.makedirs("./client_invoices", exist_ok=True)
file_path = f"./client_invoices/{attachment['file_name']}"
with open(file_path, "wb") as f:
    f.write(file_response.content)

print(f"Downloaded Factur-X PDF: {file_path}")
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.
Factur-X PDF example
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.

6. How to sync transactions and attachments

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
OAuth scope required: organization.read
import requests

BASE_URL = "https://thirdparty.qonto.com/v2"
headers = {"Authorization": "Bearer {your_access_token}"}

response = requests.get(f"{BASE_URL}/bank_accounts", headers=headers, params={"per_page": 100})
response.raise_for_status()

bank_accounts = response.json()["bank_accounts"]
print(f"Found {len(bank_accounts)} bank account(s)")
2

Fetch transactions for each account

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.
from datetime import datetime, timezone

last_sync_at = "2026-01-01T00:00:00Z"  # Load from your database
current_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")
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.
import os

for 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']}")
4

Store the sync timestamp

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.
save_to_db("last_transaction_sync_at", current_sync_at) # Use your own tools here
print(f"Sync complete. Next run will start from: {current_sync_at}")
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.