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

<Steps>
  <Step title="Upload the invoice file" titleSize="h3">
    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](/api-reference/business-api/expense-management/supplier-invoices/create-supplier-invoices)

    <Info>
      **OAuth scope required:** `supplier_invoice.write`.
    </Info>

    <Info>
      Maximum **20 invoices per request**. Each invoice requires a unique `idempotency_key` (UUID recommended) to prevent duplicate uploads. See [Idempotent requests](/get-started/general/idempotent-requests).
    </Info>

    <Warning>
      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`.*
    </Warning>

    <Warning>
      You must provide **either** `file` or `attachment_id` for each invoice, not both. Including both will result in an error.
    </Warning>

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

      <Tab title="Node.js">
        ```javascript theme={null}
        import { readFile } from "node:fs/promises";
        import { randomUUID } from "node:crypto";

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

        const fileBuffer = await readFile("./invoice.pdf");
        const formData = new FormData();
        formData.append(
          "supplier_invoices[][file]",
          new Blob([fileBuffer], { type: "application/pdf" }),
          "invoice.pdf"
        );
        formData.append("supplier_invoices[][idempotency_key]", randomUUID());
        formData.append("source", "integration");

        const response = await fetch(`${BASE_URL}/supplier_invoices/bulk`, {
          method: "POST",
          headers, // Do NOT set Content-Type — fetch sets it automatically with the boundary
          body: formData,
        });

        // Always 200 — check errors in body
        const result = await response.json();
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Check the response for errors" titleSize="h3">
    Inspect the response to confirm each invoice was processed successfully.

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

      <Tab title="Node.js">
        ```javascript theme={null}
        const supplierInvoices = result.supplier_invoices ?? [];

        for (const invoice of supplierInvoices) {
          if (invoice.errors?.length) {
            console.error("Failed:", invoice.errors);
          } else {
            console.log(`Uploaded successfully: ${invoice.id}`);
          }
        }
        ```
      </Tab>
    </Tabs>

    Common error codes to handle:

    | Code                    | Cause                                      |
    | ----------------------- | ------------------------------------------ |
    | `invalid`               | File too large or unsupported content type |
    | `required`              | Missing `idempotency_key`                  |
    | `limit_reached`         | More than 20 invoices in the request       |
    | `internal_server_error` | Processing failed on Qonto's side          |
  </Step>
</Steps>

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

<Check>
  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`.
</Check>

***
