> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qonto.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 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`.

<Tip>
  **Consider using webhooks instead of polling.** The [transactions webhook](/api-reference/business-api/webhooks/supported-webhooks/v1-transactions) 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.
</Tip>

<Steps>
  <Step title="Retrieve your bank accounts" titleSize="h3">
    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](/api-reference/business-api/accounts-organizations/business-accounts/list)

    <Info>
      **OAuth scope required:** `organization.read`
    </Info>

    <Tabs>
      <Tab title="Python">
        ```python theme={null}
        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)")
        ```
      </Tab>

      <Tab title="Node.js">
        ```javascript theme={null}
        const BASE_URL = "https://thirdparty.qonto.com/v2";
        const headers = { Authorization: "Bearer {your_access_token}" };

        const response = await fetch(`${BASE_URL}/bank_accounts?per_page=100`, { headers });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);

        const { bank_accounts: bankAccounts } = await response.json();
        console.log(`Found ${bankAccounts.length} bank account(s)`);
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Fetch transactions for each account" titleSize="h3">
    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](/api-reference/business-api/transactions-statements/transactions/list-transactions)

    <Info>
      **OAuth scope required:** `organization.read`
    </Info>

    <Warning>
      By default, only `completed` transactions are returned. If you need `pending` or `declined` transactions, you must explicitly include them using the `status[]` parameter.
    </Warning>

    <Tabs>
      <Tab title="Python">
        ```python theme={null}
        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")
        ```
      </Tab>

      <Tab title="Node.js">
        ```javascript theme={null}
        const lastSyncAt = "2026-01-01T00:00:00Z"; // Load from your database
        const 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`);
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Download transaction attachments" titleSize="h3">
    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](/api-reference/business-api/expense-management/attachments/retrieve-an-attachment)

    <Info>
      **OAuth scope required:** `attachment.read`
    </Info>

    <Warning>
      Do not store the attachment URLs for later use. If you need to re-download a file, call [`GET /v2/attachments/{id}`](/api-reference/business-api/expense-management/attachments/retrieve-an-attachment) to get a fresh URL.
    </Warning>

    <Tabs>
      <Tab title="Python">
        ```python theme={null}
        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']}")
        ```
      </Tab>

      <Tab title="Node.js">
        ```javascript theme={null}
        import { writeFile, mkdir } from "node:fs/promises";

        for (const transaction of allTransactions) {
          for (const attachment of transaction.attachments ?? []) {
            const fileResponse = await fetch(attachment.url);
            await mkdir("./transaction_attachments", { recursive: true });
            const buffer = Buffer.from(await fileResponse.arrayBuffer());
            await writeFile(`./transaction_attachments/${attachment.file_name}`, buffer);
            console.log(`Downloaded: ${attachment.file_name}`);
          }
        }
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Store the sync timestamp" titleSize="h3">
    Persist the sync timestamp only after all transactions across all accounts have been successfully processed.

    <Tip>
      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.
    </Tip>

    <Tabs>
      <Tab title="Python">
        ```python theme={null}
        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}")
        ```
      </Tab>

      <Tab title="Node.js">
        ```javascript theme={null}
        await saveToDB("last_transaction_sync_at", currentSyncAt); // Use your own tools here
        console.log(`Sync complete. Next run will start from: ${currentSyncAt}`);
        ```
      </Tab>
    </Tabs>
  </Step>
</Steps>

**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)
```

<Check>
  We now have an up-to-date local view of all transactions across all bank accounts, with their attachments downloaded.
</Check>
