> ## 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 Sold Products (v2)

> Create up to 1,000 sold products with their sales per period and country, or add periods to existing ones

[← Sold Products API](/api-reference/sold-products/overview)

Send the products your organization sells with their sales: one or more periods, each with the quantity sold in each
country. Nothing is calculated, so the request is synchronous (see
[how bulk ingestion works](/api-reference/ingest-jobs/overview)): it answers `201 Created` with each product and the
periods the request wrote. It also records an [ingest job](/api-reference/ingest-jobs/list), born `completed`, that
sends no webhook.

A product is identified by its name in the organization: a record whose name already exists **adds its periods** to
that product. The emissions of a sold product come from the data of its use
([use of sold products](/guides/emissions/scope-3-category-11-use-of-sold-products)), not from this route.

> [!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/sold-products" \
  -H "x-api-key: ${DCYCLE_API_KEY}" \
  -H "x-organization-id: ${DCYCLE_ORG_ID}" \
  -H "Idempotency-Key: 4d3c2b1a-0f9e-4d8c-9b7a-6f5e4d3c2b1a" \
  -H "Content-Type: application/json" \
  -d '{
    "records": [
      {
        "name": "Industrial Compressor X200",
        "client_row_id": "SKU-X200",
        "periods": [
          {
            "start_date": "2025-01-01",
            "end_date": "2025-12-31",
            "unit_id": "bf800281-bcc7-42f4-81e4-8ec9abb9745e",
            "sales": [
              { "country": "ES", "quantity": 250 },
              { "country": "DE", "quantity": 180 },
              { "country": "RoW", "quantity": 40 }
            ]
          }
        ]
      }
    ]
  }'
```

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

import requests

UNITS = "bf800281-bcc7-42f4-81e4-8ec9abb9745e"  # units (see "Units")

response = requests.post(
    "https://api.dcycle.io/v2/sold-products",
    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": [
            {
                "name": "Industrial Compressor X200",
                "client_row_id": "SKU-X200",
                "periods": [
                    {
                        "start_date": "2025-01-01",
                        "end_date": "2025-12-31",
                        "unit_id": UNITS,
                        "sales": [
                            {"country": "ES", "quantity": 250},
                            {"country": "DE", "quantity": 180},
                            {"country": "RoW", "quantity": 40},
                        ],
                    }
                ],
            }
        ]
    },
    timeout=60,
)
response.raise_for_status()
print([item["sold_product"]["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": "SKU-X200",
      "sold_product": {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "organization_id": "a8315ef3-dd50-43f8-b7ce-d839e68d51fa",
        "name": "Industrial Compressor X200",
        "created_at": "2026-10-21T09:12:44.031Z",
        "periods": [
          {
            "id": "1b2c3d4e-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
            "start_date": "2025-01-01",
            "end_date": "2025-12-31",
            "unit_id": "bf800281-bcc7-42f4-81e4-8ec9abb9745e",
            "sales": [
              { "id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d", "country": "ES", "quantity": 250.0 },
              { "id": "7b8c9d0e-1f2a-4b3c-9d4e-5f6a7b8c9d0e", "country": "DE", "quantity": 180.0 },
              { "id": "8c9d0e1f-2a3b-4c4d-8e5f-6a7b8c9d0e1f", "country": "RoW", "quantity": 40.0 }
            ]
          }
        ]
      }
    }
  ]
}
```

```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": "SKU-X200",
      "error_code": "PERIOD_OVERLAPS",
      "error_params": {
        "field": "periods[0]",
        "period_id": "9d0e1f2a-3b4c-4d5e-9f6a-7b8c9d0e1f2a"
      }
    }
  ],
  "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": 1, "valid": 1 }
```

## How it works

1. **Every record is checked**: its schema, the unit and countries of each period, and its periods against those the
   product already has. If any record is invalid the **whole** request is rejected with `422` and nothing is written
   ([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 identifies a product in the organization**, case aside (`Compressor X200` and `compressor x200` are one
   product). A record whose name is new creates the product; a record whose name exists adds its periods to that
   product, and the answer gives the existing product's `id` and stored `name`. Two records with the same name reject
   the second (`DUPLICATE_PRODUCT`): put all the periods of a product in one record.
3. **A period cannot overlap another of its product**, the dates of both included. Within a record that is a schema
   error (`SCHEMA_INVALID`); against a period the product already has, **the same period included**, it is
   `PERIOD_OVERLAPS`, with the id of that period. So send every country of a period in one request: a later request
   cannot add countries to it. To replace a period, delete it first with
   [Delete Period](/api-reference/sold-products/delete-period), or delete the product if it is its only period.
4. **Each country appears once in a period** (`DUPLICATE_SALE` otherwise). Unlike the file import, two quantities of
   the same country are never summed silently.
5. **Everything is written 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. Each product comes with the periods this request wrote,
   with their sales; the periods it already had are left out.
6. **The products 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 products with its own
   `x-organization-id`.

Nothing is calculated here: the emissions of a product come from the data of its use, sent per period with
[Create Use of Sold Products (v2)](/api-reference/use-of-sold-products/create-v2). To retry a request safely, repeat it with the same `Idempotency-Key`.

### Units

The `unit_id` of a period is one of the units of a product (`GET /v2/units?type=products`, see
[List Units](/api-reference/units/list#what-each-type-returns)). Any other is `UNIT_NOT_SUPPORTED`.

| Unit | `unit_id` |
| - | - |
| Units (pieces) | `bf800281-bcc7-42f4-81e4-8ec9abb9745e` |
| Kilograms (kg) | `61743a63-ff70-459c-9567-5eee8f7dfd5c` |
| Cubic metres (m³) | `2a09be22-a6e2-4317-a680-94aaa1048765` |
| Tonnes (t) | `cab66828-c2b1-431b-92af-f9ab37149d3c` |

## Request

### Headers

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

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

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

The organization the products 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 products (with `Idempotent-Replayed: true`) instead of
writing 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 products to create or add periods to, 1 to 1,000. A field not listed here is rejected (`SCHEMA_INVALID`).

**record**

**name** `string` *required*

The product's name, 1 to 255 characters: its identity in the organization, case aside. An existing name adds
the periods to that product.

**periods** `array[object]` *required*

The periods of sales, 1 to 20, none overlapping another.

**period**

**start\_date** `date` *required*

First day of the period, `YYYY-MM-DD`, from 1970 to next year.

**end\_date** `date` *required*

Last day of the period, on or after `start_date`, from 1970 to next year.

**unit\_id** `uuid` *required*

The unit of every quantity of the period: one of [the units of a product](#units).

**sales** `array[object]` *required*

The quantity sold in each country during the period, 1 to 300, each country once.

**sale**

**country** `string` *required*

Where it was sold: an ISO 3166-1 alpha-2 code (`ES`, `de`...), in upper or lower case, or `RoW` for the
rest of the world. Answered in upper case, or `RoW`.

**quantity** `number` *required*

How much was sold, over 0, in the period's `unit_id`.

**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 its
`sold_product`:

* `id`: the product's id, new or existing, the one [Delete Sold Products (v2)](/api-reference/sold-products/delete-v2)
  takes and the `product_id` of [List Sold Products](/api-reference/sold-products/list).
* `organization_id`, `name` (as stored: an existing product keeps its own spelling), `created_at` (when the product
  was created, earlier for an existing one).
* `periods`: the periods this request wrote, by `start_date`, each with its `id`, `start_date`, `end_date`, `unit_id`
  and `sales` (`id`, `country`, `quantity`).

## 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 `name` or `periods`, no `sales`, more than 20 periods or 300 sales, a `quantity` not over 0, a country that is neither two letters nor `RoW`, a date out of range, `end_date` before `start_date`, two periods of the record overlapping, an unknown field (e.g. `organization_id`)... `error_params.errors` says where. |
| `DUPLICATE_PRODUCT` | Another record of the request has the same name, case aside (`error_params.field` is `name`). Put all its periods in one record. |
| `PERIOD_OVERLAPS` | The period overlaps one the product already has, or is the same one. `error_params.field` is `periods[i]` and `error_params.period_id` the existing period. Send all the countries of a period in one request. |
| `DUPLICATE_SALE` | A country appears twice in the same period (`error_params.field` is `periods[i].sales[j].country`). |
| `REFERENCE_NOT_FOUND` | Dcycle does not know the country (`error_params.field` is `periods[i].sales[j].country`). |
| `UNIT_NOT_SUPPORTED` | `unit_id` is not one of [the units of a product](#units) (`error_params.field` is `periods[i].unit_id`). |

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 |
| Periods per record | 1 to 20 |
| Countries per period | 1 to 300 |

## Related Endpoints

> **[Delete Sold Products (v2)](/api-reference/sold-products/delete-v2)**
>
> Delete products by id

> **[List Sold Products](/api-reference/sold-products/list)**
>
> The products stored

> **[List Periods](/api-reference/sold-products/list-periods)**
>
> All the periods of a product

> **[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.