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

> Create one or up to 5,000 wastes in one request; emissions are calculated asynchronously and tracked through an ingest job

Create wastes: one, or up to 5,000 in a single request. The records are validated and stored at once, and their CO2e
emissions are calculated **asynchronously** by a background worker. The response is an **ingest job**, not the
wastes themselves: you poll the job to follow the calculation and read per-record outcomes.

<Warning>
  **Beta.** This endpoint is in beta: the contract may still change before general availability. Pin your
  integration to the fields documented here and handle unknown fields in responses gracefully. Feedback is welcome
  through your Dcycle contact.
</Warning>

<RequestExample>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -X POST "https://api.dcycle.io/v2/wastes" \
    -H "x-api-key: ${DCYCLE_API_KEY}" \
    -H "x-organization-id: ${DCYCLE_ORG_ID}" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: c41d8e77-5d40-4a11-9e8b-2c9f4d1e7a03" \
    -d '{
      "records": [
        {
          "client_row_id": "ALB-2026-09-0001",
          "facility_id": "660e8400-e29b-41d4-a716-446655440000",
          "identification_name": "ALB-2026-09-0001",
          "start_date": "2026-09-01",
          "end_date": "2026-09-30",
          "base_quantity": 1250.5,
          "unit_id": "61743a63-ff70-459c-9567-5eee8f7dfd5c",
          "waste_ler_code_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        }
      ]
    }'
  ```

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

  import requests

  response = requests.post(
      "https://api.dcycle.io/v2/wastes",
      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": [
              {
                  "client_row_id": "ALB-2026-09-0001",
                  "facility_id": "660e8400-e29b-41d4-a716-446655440000",
                  "identification_name": "ALB-2026-09-0001",
                  "start_date": "2026-09-01",
                  "end_date": "2026-09-30",
                  "base_quantity": 1250.5,
                  "unit_id": "61743a63-ff70-459c-9567-5eee8f7dfd5c",
                  "waste_ler_code_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
              }
          ]
      },
      timeout=60,
  )
  print(response.status_code, response.headers["Location"])
  ```

  ```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const axios = require('axios');
  const { randomUUID } = require('crypto');

  const response = await axios.post('https://api.dcycle.io/v2/wastes', {
    records: [
      {
        client_row_id: 'ALB-2026-09-0001',
        facility_id: '660e8400-e29b-41d4-a716-446655440000',
        identification_name: 'ALB-2026-09-0001',
        start_date: '2026-09-01',
        end_date: '2026-09-30',
        base_quantity: 1250.5,
        unit_id: '61743a63-ff70-459c-9567-5eee8f7dfd5c',
        waste_ler_code_id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
      },
    ],
  }, {
    headers: {
      'x-api-key': process.env.DCYCLE_API_KEY,
      'x-organization-id': process.env.DCYCLE_ORG_ID,
      'Idempotency-Key': randomUUID(), // keep it to retry this same batch
    },
  });
  console.log(response.status, response.headers.location);
  ```
</RequestExample>

<ResponseExample>
  ```json 202 theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "id": "2d4b90c1-7c1e-4b6f-9a55-3f2b8e61d0aa",
    "status": "processing",
    "entity_type": "wastes",
    "operation": "create",
    "source": "api_bulk",
    "counts": { "submitted": 1, "succeeded": 0, "failed": 0 },
    "chunks_total": 1,
    "chunks_done": 0,
    "created_at": "2026-09-29T10:15:02.184311",
    "finished_at": null,
    "links": {
      "self": "/v2/ingest-jobs/2d4b90c1-7c1e-4b6f-9a55-3f2b8e61d0aa",
      "items": "/v2/ingest-jobs/2d4b90c1-7c1e-4b6f-9a55-3f2b8e61d0aa/items"
    }
  }
  ```

  ```json 422 theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "code": "BULK_RECORDS_REJECTED",
    "detail": {
      "rejected": 1,
      "records": [
        {
          "source_row_index": 0,
          "client_row_id": "ALB-2026-09-0001",
          "error_code": "REFERENCE_NOT_FOUND",
          "error_params": { "field": "unit_id", "value": "61743a63-ff70-459c-9567-5eee8f7dfd5c" }
        }
      ]
    }
  }
  ```
</ResponseExample>

## What is a waste

A **waste** is a quantity of residue that one of your facilities generates and hands over to a manager, who
transports it to a treatment plant. Both steps emit greenhouse gases:

* **Treatment:** landfilling, incineration, recycling, composting… Each treatment of each type of waste has its own
  emission factor.
* **Transport** to the waste center: calculated from `total_km_to_waste_center`.

These emissions are reported under **Scope 3, Category 5 (waste generated in operations)** of the
[GHG Protocol](https://ghgprotocol.org/sites/default/files/2022-12/Chapter5.pdf).

A waste record is characterized by:

| What | Field | Notes |
| - | - | - |
| Where it was generated | `facility_id` (or `facility_percentages` to split it across facilities) | A facility of your organization or of an accepted subsidiary. |
| When | `start_date`, `end_date` | The period the waste belongs to. |
| How much | `base_quantity`, `unit_id` | E.g. 1,250.5 kg. |
| What type of waste | `waste_ler_code_id` | The **LER** code (European List of Waste, Commission Decision 2000/532/EC): six digits, e.g. `15 01 06` mixed packaging; an asterisk marks hazardous waste. |
| How it is treated | `waste_rd_code_id` | The **R/D** code: recovery (`R1`–`R13`) or disposal (`D1`–`D15`) operation, from Annexes I and II of the Waste Framework Directive 2008/98/EC. E.g. `R3` recycling, `D1` landfill. |
| Who manages it | `provider_name`, `destination` | The waste manager and the treatment plant. |

### How the emissions are calculated

Dcycle multiplies the quantity by an emission factor. You decide which factor, in one of three ways:

1. **LER code (and R/D code when you have it):** the standard route. Dcycle picks the factor that matches that waste
   type and treatment in its emission factor databases.
2. **No codes, only a `description`:** Dcycle matches the description to a waste type automatically before the
   calculation. Use it when your source data has no LER code.
3. **Your own factor, `custom_ef_record_id`:** the id of a
   [custom emission factor record](/api-reference/custom-emission-factors/overview) you created beforehand in a custom
   emission factor database of your organization, with `wastes` among its activity categories. Use it when you have a
   factor measured or provided by your waste manager. With it, no LER code is needed. See
   [Create Custom Record](/api-reference/custom-emission-factors/create-record).

A record needs at least one of the three. If a waste cannot be matched to any factor, it is still stored and its
calculation ends as `CALCULATION_FAILED` in the job's items.

## How it works

1. You send a list of records (`records`), one object per waste.
2. **All or nothing:** every record is validated. If **any** record is invalid, the whole request is rejected with
   `422 BULK_RECORDS_REJECTED`, the response lists **every** invalid record, and **nothing is written**. Fix them and
   resend the full batch.
3. If every record is valid, all wastes are written in one transaction and the API answers **`202 Accepted`** with the
   ingest job and a `Location` header pointing at it.
4. A worker calculates the emissions in chunks of 100 records. Poll
   [`GET /v2/ingest-jobs/{job_id}`](/api-reference/ingest-jobs/get) until `status` is `completed` or
   `completed_with_errors`, then read the per-record outcomes at
   [`GET /v2/ingest-jobs/{job_id}/items`](/api-reference/ingest-jobs/list-items).

```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
    participant C as Client
    participant A as Dcycle API
    participant W as Worker
    C->>A: POST /v2/wastes {records: [...]}
    alt any record invalid
        A-->>C: 422 BULK_RECORDS_REJECTED (every invalid record listed)
    else all records valid
        A->>A: write wastes + job + ledger (one transaction)
        A-->>C: 202 Accepted, Location: /v2/ingest-jobs/{id}, status "processing"
        loop per chunk of 100 records
            W->>W: calculate emissions
        end
        C->>A: GET /v2/ingest-jobs/{id}
        A-->>C: status "completed" | "completed_with_errors"
    end
```

## One waste or many

The same request creates one waste or a whole batch. To create a single waste, send `records` with one element; it
goes through the same validation, the same job and the same calculation as a batch. For more than 5,000 records,
split them into several requests (see [Limits](#limits)).

## Request

### Headers

<ParamField header="x-api-key" type="string" required>
  Your API key for authentication

  **Example:** `sk_live_1234567890abcdef`
</ParamField>

<ParamField header="x-organization-id" type="string" required>
  UUID of the organization the wastes are created for. Facilities of its accepted, enabled subsidiaries are also
  valid destinations.

  **Example:** `a8315ef3-dd50-43f8-b7ce-d839e68d51fa`
</ParamField>

<ParamField header="Content-Type" type="string" required>
  Must be `application/json`
</ParamField>

<ParamField header="Idempotency-Key" type="string">
  **Strongly recommended.** Up to 255 characters. Send a unique value per batch (a UUID works well). If a request with
  the same key was already accepted for this organization, the API returns **that same job** instead of creating the
  wastes again, so a timeout or network error can be retried safely. Use a new key for a new batch. Without it, a
  retried request creates the wastes twice.

  **Example:** `c41d8e77-5d40-4a11-9e8b-2c9f4d1e7a03`
</ParamField>

### Body Parameters

<ParamField body="records" type="array[object]" required>
  The wastes to create: between 1 and 5,000 objects. Each record has the fields below.

  <Expandable title="record fields">
    <ParamField body="facility_id" type="uuid" required>
      UUID of the facility the waste belongs to. Must belong to your organization or to one of its accepted, enabled
      subsidiaries. Retrieve options from [`GET /v1/facilities`](/api-reference/facilities/list).

      **Example:** `"660e8400-e29b-41d4-a716-446655440000"`
    </ParamField>

    <ParamField body="identification_name" type="string" required>
      Waste identification name or invoice/delivery-note number.

      **Example:** `"ALB-2026-09-0001"`
    </ParamField>

    <ParamField body="start_date" type="date" required>
      Start of the waste period (ISO 8601).

      **Example:** `"2026-09-01"`
    </ParamField>

    <ParamField body="end_date" type="date" required>
      End of the waste period (ISO 8601). Must be on or after `start_date`.

      **Example:** `"2026-09-30"`
    </ParamField>

    <ParamField body="base_quantity" type="number" required>
      Quantity of waste in `unit_id`. Must be greater than 0.

      **Example:** `1250.5`
    </ParamField>

    <ParamField body="unit_id" type="uuid">
      UUID of the quantity unit. **Send it explicitly** (for kilograms, the `kilogram_(kg)` unit): during the beta, a
      waste without a unit may end its calculation with `CALCULATION_FAILED`. Retrieve options from
      [`GET /v1/units`](/api-reference/units/list).

      **Example:** `"61743a63-ff70-459c-9567-5eee8f7dfd5c"`
    </ParamField>

    <ParamField body="waste_ler_code_id" type="uuid">
      UUID of the LER (European List of Waste) code. Required unless you send a `custom_ef_record_id`, or a
      non-empty `description` (then the description is matched to a waste type automatically before the
      calculation).

      **Example:** `"a1b2c3d4-e5f6-7890-abcd-ef1234567890"`
    </ParamField>

    <ParamField body="waste_rd_code_id" type="uuid">
      UUID of the R/D (recovery/disposal) treatment code. Optional.

      **Example:** `"b2c3d4e5-f6a7-8901-bcde-fa2345678901"`
    </ParamField>

    <ParamField body="description" type="string" default="">
      Description of the waste. Required (non-empty) when `waste_ler_code_id` is omitted.

      **Example:** `"Mezcla de envases ligeros"`
    </ParamField>

    <ParamField body="destination" type="string" default="">
      Waste destination or treatment facility.

      **Example:** `"Centro de tratamiento Ecoveza"`
    </ParamField>

    <ParamField body="provider_name" type="string | null">
      Waste manager / service provider business name.

      **Example:** `"Ecoveza S.L."`
    </ParamField>

    <ParamField body="total_km_to_waste_center" type="number" default="0">
      Distance to the waste center in km, used to calculate collection transport emissions. Must be 0 or greater.

      **Example:** `42`
    </ParamField>

    <ParamField body="facility_percentages" type="array[object] | null">
      Split one record across several facilities ("divide consumptions"). With more than one entry, one waste is
      created per facility, each with `quantity = base_quantity × percentage`. Facilities must be unique, all inside
      your perimeter, and the percentages must sum to at most 1. With one entry or none, the waste goes entirely to
      `facility_id`.

      <Expandable title="entry fields">
        <ParamField body="facility_id" type="uuid" required>
          UUID of the facility receiving this share.
        </ParamField>

        <ParamField body="percentage" type="number" required>
          Fraction of the quantity, greater than 0 and at most 1.

          **Example:** `0.6`
        </ParamField>
      </Expandable>
    </ParamField>

    <ParamField body="custom_ef_record_id" type="uuid | null">
      UUID of one of your [custom emission factor records](/api-reference/custom-emission-factors/overview), to
      calculate this waste with your own factor instead of the standard databases. The record must be visible to the organization that owns the destination facility,
      enabled, and configured for the `wastes` activity category. With it, `waste_ler_code_id` is optional. In a
      split (`facility_percentages`), every resulting waste uses the same factor.

      **Example:** `"c3d4e5f6-a7b8-9012-cdef-ab3456789012"`
    </ParamField>

    <ParamField body="file_url" type="string | null">
      URL of a supporting document (e.g. a waste manifest).
    </ParamField>

    <ParamField body="client_row_id" type="string | null">
      Your own identifier for the record, up to 255 characters. It is echoed on every outcome for this record (in
      rejections and in the job's items), so you can correlate results without tracking array positions.

      **Example:** `"ALB-2026-09-0001"`
    </ParamField>
  </Expandable>
</ParamField>

## Response

### 202 Accepted

Every record passed validation and was stored. The body is the ingest job; the `Location` header holds its path.

```http theme={"theme":{"light":"github-light","dark":"github-dark"}}
HTTP/1.1 202 Accepted
Location: /v2/ingest-jobs/2d4b90c1-7c1e-4b6f-9a55-3f2b8e61d0aa
```

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "id": "2d4b90c1-7c1e-4b6f-9a55-3f2b8e61d0aa",
  "status": "processing",
  "entity_type": "wastes",
  "operation": "create",
  "source": "api_bulk",
  "counts": { "submitted": 250, "succeeded": 0, "failed": 0 },
  "chunks_total": 3,
  "chunks_done": 0,
  "created_at": "2026-09-29T10:15:02.184311",
  "finished_at": null,
  "links": {
    "self": "/v2/ingest-jobs/2d4b90c1-7c1e-4b6f-9a55-3f2b8e61d0aa",
    "items": "/v2/ingest-jobs/2d4b90c1-7c1e-4b6f-9a55-3f2b8e61d0aa/items"
  }
}
```

<ResponseField name="id" type="uuid">
  Ingest job id. Use it with [`GET /v2/ingest-jobs/{job_id}`](/api-reference/ingest-jobs/get).
</ResponseField>

<ResponseField name="status" type="string">
  `processing` right after the request. See [Job status](#job-status).
</ResponseField>

<ResponseField name="entity_type" type="string">
  Always `wastes` for this endpoint.
</ResponseField>

<ResponseField name="operation" type="string">
  Always `create`.
</ResponseField>

<ResponseField name="source" type="string">
  Always `api_bulk` for this endpoint.
</ResponseField>

<ResponseField name="counts" type="object">
  <Expandable title="fields">
    <ResponseField name="submitted" type="integer">Records in the request. All of them were stored.</ResponseField>
    <ResponseField name="succeeded" type="integer">Records whose emissions have been calculated.</ResponseField>

    <ResponseField name="failed" type="integer">
      Records whose calculation failed. The waste exists but has no emissions; see the job's items for the reason.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="chunks_total" type="integer">
  Number of calculation chunks (100 records each).
</ResponseField>

<ResponseField name="chunks_done" type="integer">
  Chunks already calculated. `chunks_done / chunks_total` is the job's progress.
</ResponseField>

<ResponseField name="created_at" type="datetime">
  When the job was created (UTC).
</ResponseField>

<ResponseField name="finished_at" type="datetime | null">
  When the last chunk finished (UTC), `null` while the job is processing.
</ResponseField>

<ResponseField name="links" type="object">
  `self`: the job. `items`: its per-record outcomes.
</ResponseField>

### Job status

| `status` | Meaning |
| - | - |
| `processing` | Wastes are stored; emissions are being calculated. |
| `completed` | Every record was calculated. |
| `completed_with_errors` | Every chunk ran, and at least one record failed to calculate (`counts.failed > 0`). The other records are calculated normally. |
| `failed` | The job as a whole failed. Contact support with the job id. |

While a job is `processing`, the new wastes already exist with `co2e` at `0`; the value is filled in when their chunk
is calculated. Each record's waste id is its `entity_id` in the [job's items](/api-reference/ingest-jobs/list-items).

## Errors

All error bodies have a stable `code` and a `detail`.

### 422 BULK\_RECORDS\_REJECTED — invalid records

At least one record is invalid. **Nothing was written.** `detail.rejected` is the number of invalid records and
`detail.records` lists them (up to 100) in submission order, each with its position in `records`
(`source_row_index`, starting at 0), your `client_row_id` and a stable `error_code`.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "code": "BULK_RECORDS_REJECTED",
  "detail": {
    "rejected": 3,
    "records": [
      {
        "source_row_index": 1,
        "client_row_id": "ALB-2026-09-0002",
        "error_code": "FACILITY_NOT_BELONG_TO_ORGANIZATION",
        "error_params": { "field": "facility_id", "value": "00000000-0000-0000-0000-00000000c0de" }
      },
      {
        "source_row_index": 2,
        "client_row_id": "ALB-2026-09-0003",
        "error_code": "SCHEMA_INVALID",
        "error_params": {
          "errors": [
            { "loc": ["base_quantity"], "type": "greater_than", "msg": "Input should be greater than 0" }
          ]
        }
      },
      {
        "source_row_index": 4,
        "client_row_id": "ALB-2026-09-0005",
        "error_code": "REFERENCE_NOT_FOUND",
        "error_params": { "field": "unit_id", "value": "7d0b2c1e-0000-4000-8000-000000000000" }
      }
    ]
  }
}
```

| `error_code` | Cause | Fix |
| - | - | - |
| `SCHEMA_INVALID` | The record does not match the record schema: a missing required field, a wrong type, `base_quantity` ≤ 0, `end_date` before `start_date`, or the record is not a JSON object. `error_params.errors` lists each problem with its field (`loc`). | Correct the listed fields. |
| `FACILITY_NOT_BELONG_TO_ORGANIZATION` | `facility_id` (or a facility in `facility_percentages`) does not exist or is outside your organization's perimeter. | Use a facility of your organization or of an accepted subsidiary. |
| `WASTE_LER_OR_DESCRIPTION_REQUIRED` | None of `waste_ler_code_id`, `custom_ef_record_id` or a non-empty `description` was sent. | Send one of them. |
| `REFERENCE_NOT_FOUND` | `waste_ler_code_id`, `waste_rd_code_id` or `unit_id` does not exist. `error_params.field` names it. | Take ids from the LER, R/D and units catalogs. |
| `FACILITIES_MUST_BE_UNIQUE` | A facility appears twice in `facility_percentages`. | List each facility once. |
| `FACILITY_PERCENTAGES_INVALID` | The `facility_percentages` sum to more than 1. | Make them add up to 1 or less. |
| `MULTIDB_CUSTOM_RECORD_NOT_FOUND_OR_DISABLED` | `custom_ef_record_id` does not exist, is disabled, or is not visible to the organization that owns the facility. | Use an enabled record of that organization (or shared with it). |
| `MULTIDB_CUSTOM_RECORD_INCOMPATIBLE_CATEGORY` | The custom emission factor record is not configured for the `wastes` activity category. | Add `wastes` to the record's activity categories. |

### 422 — malformed body

The body itself is invalid: `records` is missing or empty, or has more than 5,000 entries. The body follows the
standard validation format:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "detail": [
    { "loc": ["body", "records"], "msg": "List should have at most 5000 items after validation, not 5001", "type": "too_long" }
  ]
}
```

### 401 Unauthorized / 403 Forbidden

Missing or invalid API key (`401`), or the API key's user is not a member of the organization in
`x-organization-id` (`403 LOGGED_USER_NOT_MEMBER`).

## Examples

### Create a batch and wait for the calculation

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -i -X POST "https://api.dcycle.io/v2/wastes" \
    -H "x-api-key: ${DCYCLE_API_KEY}" \
    -H "x-organization-id: ${DCYCLE_ORG_ID}" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: $(uuidgen)" \
    -d '{
      "records": [
        {
          "client_row_id": "ALB-2026-09-0001",
          "facility_id": "660e8400-e29b-41d4-a716-446655440000",
          "identification_name": "ALB-2026-09-0001",
          "start_date": "2026-09-01",
          "end_date": "2026-09-30",
          "base_quantity": 1250.5,
          "unit_id": "61743a63-ff70-459c-9567-5eee8f7dfd5c",
          "waste_ler_code_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
          "waste_rd_code_id": "b2c3d4e5-f6a7-8901-bcde-fa2345678901",
          "provider_name": "Ecoveza S.L.",
          "total_km_to_waste_center": 42
        },
        {
          "client_row_id": "ALB-2026-09-0002",
          "facility_id": "660e8400-e29b-41d4-a716-446655440000",
          "identification_name": "ALB-2026-09-0002",
          "start_date": "2026-09-01",
          "end_date": "2026-09-30",
          "base_quantity": 380,
          "unit_id": "61743a63-ff70-459c-9567-5eee8f7dfd5c",
          "description": "Mezcla de envases ligeros"
        }
      ]
    }'

  # Then poll the job from the Location header
  curl "https://api.dcycle.io/v2/ingest-jobs/2d4b90c1-7c1e-4b6f-9a55-3f2b8e61d0aa" \
    -H "x-api-key: ${DCYCLE_API_KEY}" \
    -H "x-organization-id: ${DCYCLE_ORG_ID}"
  ```

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

  import requests

  BASE_URL = "https://api.dcycle.io"
  HEADERS = {
      "x-api-key": os.environ["DCYCLE_API_KEY"],
      "x-organization-id": os.environ["DCYCLE_ORG_ID"],
  }

  records = [
      {
          "client_row_id": "ALB-2026-09-0001",
          "facility_id": "660e8400-e29b-41d4-a716-446655440000",
          "identification_name": "ALB-2026-09-0001",
          "start_date": "2026-09-01",
          "end_date": "2026-09-30",
          "base_quantity": 1250.5,
          "unit_id": "61743a63-ff70-459c-9567-5eee8f7dfd5c",
          "waste_ler_code_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      },
  ]

  # One key per batch: reuse it only to retry this same batch.
  idempotency_key = str(uuid.uuid4())
  response = requests.post(
      f"{BASE_URL}/v2/wastes",
      headers={**HEADERS, "Idempotency-Key": idempotency_key},
      json={"records": records},
      timeout=60,
  )

  if response.status_code == 422 and response.json().get("code") == "BULK_RECORDS_REJECTED":
      for rejection in response.json()["detail"]["records"]:
          print(rejection["source_row_index"], rejection["client_row_id"], rejection["error_code"], rejection["error_params"])
      raise SystemExit("Fix the listed records and resend the whole batch")
  response.raise_for_status()

  job_url = BASE_URL + response.headers["Location"]
  while True:
      job = requests.get(job_url, headers=HEADERS, timeout=30).json()
      print(f"{job['status']}: {job['chunks_done']}/{job['chunks_total']} chunks")
      if job["status"] != "processing":
          break
      time.sleep(10)

  if job["status"] == "completed_with_errors":
      failed = requests.get(
          f"{job_url}/items", headers=HEADERS, params={"status": "failed", "size": 500}, timeout=30
      ).json()
      for item in failed["items"]:
          print("calculation failed:", item["client_row_id"], item["error_code"])
  ```

  ```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const axios = require('axios');
  const { randomUUID } = require('crypto');

  const api = axios.create({
    baseURL: 'https://api.dcycle.io',
    headers: {
      'x-api-key': process.env.DCYCLE_API_KEY,
      'x-organization-id': process.env.DCYCLE_ORG_ID,
    },
  });

  async function createWastes(records) {
    let response;
    try {
      response = await api.post('/v2/wastes', { records }, {
        headers: { 'Idempotency-Key': randomUUID() },
      });
    } catch (error) {
      const body = error.response && error.response.data;
      if (body && body.code === 'BULK_RECORDS_REJECTED') {
        body.detail.records.forEach(r =>
          console.log(r.source_row_index, r.client_row_id, r.error_code, r.error_params));
      }
      throw error;
    }

    const jobPath = response.headers.location;
    let job = response.data;
    while (job.status === 'processing') {
      await new Promise(resolve => setTimeout(resolve, 10000));
      job = (await api.get(jobPath)).data;
      console.log(`${job.status}: ${job.chunks_done}/${job.chunks_total} chunks`);
    }
    return job;
  }
  ```
</CodeGroup>

### Split one waste across two facilities

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "records": [
    {
      "client_row_id": "ALB-2026-09-0100",
      "facility_id": "660e8400-e29b-41d4-a716-446655440000",
      "identification_name": "ALB-2026-09-0100",
      "start_date": "2026-09-01",
      "end_date": "2026-09-30",
      "base_quantity": 100,
      "unit_id": "61743a63-ff70-459c-9567-5eee8f7dfd5c",
      "waste_ler_code_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "facility_percentages": [
        { "facility_id": "660e8400-e29b-41d4-a716-446655440000", "percentage": 0.6 },
        { "facility_id": "770e8400-e29b-41d4-a716-446655440001", "percentage": 0.4 }
      ]
    }
  ]
}
```

This creates two wastes (60 and 40 units) sharing one `source_waste_id`. The job has **one** item for the record,
whose `entity_id` is the first waste of the group.

## Retries and idempotency

* **Network error or timeout:** retry with the **same** `Idempotency-Key`. If the first request was accepted, you get
  the same job back and no wastes are duplicated.
* **`422 BULK_RECORDS_REJECTED`:** nothing was stored. Fix the records and send the batch again (a new key is fine,
  since the first request created nothing).
* Without an `Idempotency-Key`, sending the same batch twice creates the wastes twice.

## Limits

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

## Related Endpoints

<CardGroup cols={2}>
  <Card title="Get Ingest Job" icon="magnifying-glass" href="/api-reference/ingest-jobs/get">
    Status and progress of the batch
  </Card>

  <Card title="List Ingest Job Items" icon="list" href="/api-reference/ingest-jobs/list-items">
    Per-record outcomes, including calculation failures
  </Card>
</CardGroup>
