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

# Create Purchase Suppliers (v2)

> Create up to 1,000 purchase suppliers in one request, classified at once when you send their activity code

[← Purchases API](/api-reference/purchases/overview)

Add the suppliers your organization buys from before you send their purchases: a name, a country and, if you know
it, the supplier's activity code (CNAE or SIC), which picks the spend-based emission factor of its purchases.
Nothing is calculated, so the request is synchronous (see
[how bulk ingestion works](/api-reference/ingest-jobs/overview)): it answers `201 Created` with the created suppliers.
It also records an [ingest job](/api-reference/ingest-jobs/list), born `completed`, that sends no webhook.

A supplier is the one [Create Purchases (v2)](/api-reference/purchases/create-v2) uses for a purchase sent with its
`supplier_id`, or with the same `supplier_name` and `supplier_country`. This route **sends no email** and calculates
no emissions: those come with the purchases.

> [!WARNING]
>
> **Beta.** Part of the bulk ingest API, in beta. The contract may still change before general availability.

```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "https://api.dcycle.io/v2/purchases/suppliers" \
  -H "x-api-key: ${DCYCLE_API_KEY}" \
  -H "x-organization-id: ${DCYCLE_ORG_ID}" \
  -H "Idempotency-Key: 6b5a4f3e-2d1c-4b0a-9f8e-7d6c5b4a3f2e" \
  -H "Content-Type: application/json" \
  -d '{
    "records": [
      {
        "business_name": "Papelera del Norte SL",
        "country": "ES",
        "code": "1712",
        "framework": "cnae_2009",
        "client_row_id": "SUP-0042"
      },
      {
        "business_name": "Transportes Ribera SA",
        "country": "es"
      }
    ]
  }'
```

```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os
import uuid

import requests

response = requests.post(
    "https://api.dcycle.io/v2/purchases/suppliers",
    headers={
        "x-api-key": os.environ["DCYCLE_API_KEY"],
        "x-organization-id": os.environ["DCYCLE_ORG_ID"],
        "Idempotency-Key": str(uuid.uuid4()),  # keep it to retry this same batch
    },
    json={
        "records": [
            {
                "business_name": "Papelera del Norte SL",
                "country": "ES",
                "code": "1712",
                "framework": "cnae_2009",
                "client_row_id": "SUP-0042",
            },
            {"business_name": "Transportes Ribera SA", "country": "es"},
        ]
    },
    timeout=60,
)
response.raise_for_status()
print([item["supplier"]["id"] for item in response.json()["items"]])
```

```json 201 theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "items": [
    {
      "source_row_index": 0,
      "client_row_id": "SUP-0042",
      "supplier": {
        "id": "5f4e3d2c-1b0a-4f9e-8d7c-6b5a4f3e2d1c",
        "organization_id": "a8315ef3-dd50-43f8-b7ce-d839e68d51fa",
        "business_name": "Papelera del Norte SL",
        "country": "ES",
        "status": "active",
        "created_at": "2026-10-20T09:12:44.031Z"
      }
    },
    {
      "source_row_index": 1,
      "client_row_id": null,
      "supplier": {
        "id": "7a6b5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
        "organization_id": "a8315ef3-dd50-43f8-b7ce-d839e68d51fa",
        "business_name": "Transportes Ribera SA",
        "country": "ES",
        "status": "in_progress",
        "created_at": "2026-10-20T09:12:44.031Z"
      }
    }
  ]
}
```

```json 422 theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "https://code.dcycle.io/api-reference/errors#BULK_RECORDS_REJECTED",
  "title": "Unprocessable Entity",
  "status": 422,
  "code": "BULK_RECORDS_REJECTED",
  "detail": "1 of the submitted records are invalid; nothing was written.",
  "errors": [
    {
      "source_row_index": 0,
      "client_row_id": "SUP-0042",
      "error_code": "SUPPLIER_ALREADY_EXISTS",
      "error_params": {
        "field": "business_name",
        "supplier_id": "5f4e3d2c-1b0a-4f9e-8d7c-6b5a4f3e2d1c"
      }
    }
  ],
  "rejected": 1,
  "request_id": "b2248a31-0f1b-4a7d-83ce-b29bd5977b4c"
}
```

```json 200 (dry_run=true) theme={"theme":{"light":"github-light","dark":"github-dark"}}
{ "dry_run": true, "submitted": 2, "valid": 2 }
```

## How it works

1. **Every record is checked**: its name and country are free, and its `code`, if sent, is an active code of its
   `framework`. If any record is invalid the **whole** request is rejected with `422` and nothing is created
   ([all or nothing](/api-reference/ingest-jobs/overview#all-or-nothing)): `rejected` is the number of invalid records
   and `errors` lists them (up to 100).
2. **The name and country identify a supplier in the organization**, with the key
   [Create Purchases (v2)](/api-reference/purchases/create-v2) matches `supplier_name` by: the exact name (case, spaces
   and punctuation count; commas become dots) and the country, in upper case. A supplier already stored with that key,
   whatever its status, rejects the record (`SUPPLIER_ALREADY_EXISTS`, with its id): an existing supplier is never
   reclassified here, because its purchases would need recalculating. Two records with the same key reject the second
   (`DUPLICATE_SUPPLIER`), also when they differ only by commas and dots or the case of the country.
3. **Every supplier is created in one transaction** and returned, in the order sent. `source_row_index` is the position
   of the record in `records`, and `client_row_id` echoes yours. The stored name has its commas replaced by dots, and
   the country is upper-cased.
   * **With `code` and `framework`**, the supplier is classified at once: it is `active`, and its purchases are
     calculated with the factor of that activity and its country, without a D\&B lookup.
   * **Without**, it is created exactly as Create Purchases (v2) creates a supplier it does not know: if Dcycle
     already knows an official (D\&B) classification for that exact name and country, the supplier takes it; otherwise
     it is `in_progress` until its first purchase classifies it (the 80 % rule of
     [Create Purchases (v2)](/api-reference/purchases/create-v2#how-it-works)).
4. **The suppliers belong to the `x-organization-id` organization.** A record cannot name another one, not even one of
   your [perimeter](/api-reference/ingest-jobs/overview#perimeter): send each organization's suppliers with its own
   `x-organization-id`.

A later purchase sent with the same `supplier_name` and `supplier_country`, or with the supplier's `id` as
`supplier_id`, uses it. To retry a request safely, repeat it with the same `Idempotency-Key`.

### Activity codes

`GET /v1/purchases/suppliers/codes?framework=cnae_2009` (or `us_sic_v87`), with your `x-api-key` and
`x-organization-id`, lists the codes of a framework, as strings (`["0111", "0130", ...]`). A `code` is accepted when it
is an active code of its framework in Dcycle's classification; any other is `REFERENCE_NOT_FOUND`.

> [!NOTE]
>
> For `us_sic_v87`, the list can include codes that are not active, which this route rejects with
> `REFERENCE_NOT_FOUND`. Prefer `cnae_2009` codes, or leave `code` out and let Dcycle classify the supplier.

## Request

### Headers

**x-api-key** `string` *required*

Your API key (see [Authentication](/api-reference/introduction#authentication)).

**x-organization-id** `string` *required*

The organization the suppliers belong to.

**Idempotency-Key** `string` *required*

1 to 255 characters, one per batch, looked up per organization across every v2 bulk route. Within 24 hours, the
same key with the same route and body answers the same suppliers (with `Idempotent-Replayed: true`) instead of
creating them again; the same key with another body or on another route is `422 IDEMPOTENCY_KEY_REUSED`, and while
the first request is still being processed `409 IDEMPOTENCY_KEY_IN_PROGRESS`. After 24 hours the key is free, and a
request with it is a new one. See [The Idempotency-Key](/api-reference/ingest-jobs/overview#the-idempotency-key).

### Query Parameters

**dry\_run** `boolean` (default: false)

Check only: the same `422` for invalid records, nothing written; `200` with
`{"dry_run": true, "submitted": n, "valid": n}` when every record is valid. It does not check or store the
`Idempotency-Key`.

### Body Parameters

**records** `array[object]` *required*

The suppliers to create, 1 to 1,000. A field not listed here is rejected (`SCHEMA_INVALID`).

**record**

**business\_name** `string` *required*

The supplier's legal name, 1 to 255 characters, spelt as your purchases will send it in `supplier_name`.

**country** `string` *required*

The supplier's country: two letters, in upper or lower case (stored in upper case), not checked against ISO
3166-1, so send its ISO 3166-1 alpha-2 code (`ES`, `FR`...). It picks the country of the spend-based factor.

**code** `string`

The supplier's activity code in `framework`, up to 16 characters (see [Activity codes](#activity-codes)). Send it
with `framework`. Omitted: Dcycle classifies the supplier.

**framework** `string`

The classification of `code`: `cnae_2009` (CNAE 2009) or `us_sic_v87` (US SIC 1987). Send it with `code`.

**client\_row\_id** `string`

Your own reference for the record, up to 255 characters, echoed in errors and in the answer.

## Response

`201 Created` with `items`: one per record, in the order sent, with its `source_row_index`, `client_row_id` and the
created `supplier`:

* `id`: the id to send as `supplier_id` in [Create Purchases (v2)](/api-reference/purchases/create-v2), and the one
  [Delete Purchase Suppliers (v2)](/api-reference/purchases/suppliers-delete-v2) takes.
* `organization_id`, `business_name` (as stored), `country` (upper case), `created_at`.
* `status`: `active` when the supplier is classified, `in_progress` while Dcycle classifies it.

[List Purchase Suppliers](/api-reference/purchases/suppliers-list) shows them with their activity `code` and
`code_type`.

## Errors

[Problem details](/api-reference/errors). Per-record codes in `errors[].error_code` of a `422 BULK_RECORDS_REJECTED`:

| `error_code` | Cause |
| - | - |
| `SCHEMA_INVALID` | The record does not match the schema: no `business_name` or `country`, a country that is not two letters, `code` without `framework` or the other way round, a `framework` not listed, an unknown field (e.g. `organization_id` or `status`)... `error_params.errors` says where. |
| `SUPPLIER_ALREADY_EXISTS` | The organization already has a supplier with this name and country (commas as dots, country case aside). `error_params.supplier_id` is its id: use it as `supplier_id`. |
| `DUPLICATE_SUPPLIER` | Another record of the request has the same name and country (commas as dots, country case aside). |
| `REFERENCE_NOT_FOUND` | `code` is not an active code of `framework` (`error_params.field` is `code`). |

Request-level errors: `401` for a missing or invalid API key, `403 LOGGED_USER_NOT_MEMBER` when the key's user is not a
member of the organization, `422 REQUEST_VALIDATION_FAILED` for a malformed body (no `records`, more than 1,000, no
`Idempotency-Key`), `422 IDEMPOTENCY_KEY_REUSED`, `409 IDEMPOTENCY_KEY_IN_PROGRESS`, and `429` beyond the
[rate limits](/api-reference/rate-limits).

## Limits

| Limit | Value |
| - | - |
| Records per request | 1 to 1,000 |

## Related Endpoints

> **[Delete Purchase Suppliers (v2)](/api-reference/purchases/suppliers-delete-v2)**
>
> Delete suppliers by id

> **[Create Purchases (v2)](/api-reference/purchases/create-v2)**
>
> Purchases from these suppliers

> **[List Purchase Suppliers](/api-reference/purchases/suppliers-list)**
>
> The suppliers stored, with their activity code

> **[List Ingest Jobs](/api-reference/ingest-jobs/list)**
>
> The job each request records


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.