> ## 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": "33d3e986-2e02-4aaa-8be0-708c143c02d9"
        }
      ]
    }'
  ```

  ```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": "33d3e986-2e02-4aaa-8be0-708c143c02d9",
              }
          ]
      },
      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: '33d3e986-2e02-4aaa-8be0-708c143c02d9',
      },
    ],
  }, {
    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.184Z",
    "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"}}
  {
    "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": "ALB-2026-09-0001",
        "error_code": "REFERENCE_NOT_FOUND",
        "error_params": { "field": "waste_ler_code_id", "value": "0f0e0d0c-0000-4000-8000-000000000000" }
      }
    ],
    "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
  }
  ```
</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 with no spaces, e.g. `150106` mixed packaging; a trailing asterisk marks hazardous waste (`130205*`). |
| How it is treated | `waste_rd_code_id` | The **R/D** code: a recovery (`R…`) or disposal (`D…`) operation from Annexes I and II of the Waste Framework Directive 2008/98/EC, plus `R14` (preparation for reuse), written with two digits as the catalog returns it. E.g. `R03` recycling of organic substances, `D01` landfill. [List R/D Codes](/api-reference/wastes/rd-codes) has the ones Dcycle accepts. |
| Who manages it | `provider_name`, `transporter_name`, `destination` | The waste manager, the carrier and the treatment plant. |

### How the emissions are calculated

Emissions are the quantity multiplied by an emission factor. What you send determines **how Dcycle finds that
factor**. Only when you bring your own factor do you choose it.

1. **LER code (and R/D code when you have it):** the standard route. The codes classify the waste in a common
   terminology, and Dcycle looks up the factor mapped to that LER + R/D pair in its emission factor databases: DEFRA
   by default, OCCC when DEFRA does not cover the code. Without an R/D code, only a factor defined for that LER with no
   treatment can be used, and not every LER has one, so send the R/D code whenever you have it.
2. **No codes, only a `description`:** Dcycle compares the description with the factors of its maintained emission
   factor databases and selects the closest one for each year of the waste period. No LER or R/D code is assigned to
   the record. A low-confidence match is still applied, but the waste is flagged for review. Use it when your source
   data has no LER code.
3. **Your own factor, `custom_ef_record_id`:** the only route where you choose the factor. 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. No LER code is needed; if you send one as well, your factor
   takes precedence. See [Create Custom Record](/api-reference/custom-emission-factors/create-record).

A record needs at least one of the three. If Dcycle cannot find a factor (the code pair is not covered, or no factor
can be applied from the description), the waste is still stored and its calculation ends as `CALCULATION_FAILED` in
the job's items.

<Note>
  **Sent with a description?** The match can leave the waste waiting for a person:
  [`GET /v1/wastes/{waste_id}/detail`](/api-reference/wastes/get) shows `status: "review"` and
  `LOW_CONFIDENCE_WASTE_FACTOR_MATCH` in `error_messages`. The job item tells the two cases apart: `succeeded` when a
  factor was applied with low confidence (the waste has emissions, someone should confirm the factor), `failed` with
  `CALCULATION_FAILED` when no factor could be applied (no emissions until someone picks one). The job does not count
  these records separately yet.
</Note>

## Find the ids you send

A record does not carry your own codes: every reference is a Dcycle id (UUID). Look them up once and keep the
mapping on your side; the catalogs rarely change. The ids of the LER, R/D and unit catalogs are the same in every
Dcycle environment, so the ones in these examples work as they are.

| Field | Where to find it |
| - | - |
| `facility_id` | [`GET /v1/facilities`](/api-reference/facilities/list): the facilities of your organization; add `consolidate_group=true` to include those of its subsidiaries. |
| `unit_id` | Kilograms: `61743a63-ff70-459c-9567-5eee8f7dfd5c`, the only unit [`GET /v2/units?type=wastes`](/api-reference/units/list) lists. A waste priced with a `custom_ef_record_id` may also use that record's unit. |
| `waste_ler_code_id` | [`GET /v1/wastes/ler-codes?search=150106`](/api-reference/wastes/ler-codes): the `id` of the LER code. The catalog is the same for every organization. |
| `waste_rd_code_id` | [`GET /v1/wastes/rd-codes?ler_code_id=…`](/api-reference/wastes/rd-codes): scoped to the LER code, it lists only the treatments that have an emission factor for it. |
| `custom_ef_record_id` | [List Custom Records](/api-reference/custom-emission-factors/list-records), only when you bring your own factor. |

## 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" required>
  1 to 255 characters. Send a unique value per batch (a UUID works well). If a request with the same key and the same
  body 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. The same key with a **different** body is rejected with
  `422 IDEMPOTENCY_KEY_REUSED`: use a new key for a new batch.

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

### Query Parameters

<ParamField query="dry_run" type="boolean" default="false">
  Validate only. Every check runs and an invalid batch answers the same `422`, but **nothing is written** and no job
  is created: a valid batch answers `200` with `{"dry_run": true, "submitted": n, "valid": n}`. Use it to test an
  integration without creating data. The `Idempotency-Key` header is still required, but a dry run does not record
  it, so you can send the real request with the same key afterwards.
</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), a year from 1970 to next year.

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

    <ParamField body="end_date" type="date" required>
      End of the waste period (ISO 8601), a year from 1970 to next year. 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" required>
      Kilograms: `61743a63-ff70-459c-9567-5eee8f7dfd5c`, the unit [`GET /v2/units?type=wastes`](/api-reference/units/list) lists. Send the quantity
      in kilograms (convert tonnes ×1,000). A waste priced with a `custom_ef_record_id` may instead use the unit of
      that record, the one its factor is per. A record without a unit is rejected with `SCHEMA_INVALID`, and one with
      any other unit with `UNIT_NOT_SUPPORTED`.

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

    <ParamField body="waste_ler_code_id" type="uuid">
      UUID of the LER (European List of Waste) code, taken from
      [`GET /v1/wastes/ler-codes`](/api-reference/wastes/ler-codes). 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:** `"33d3e986-2e02-4aaa-8be0-708c143c02d9"` (`150106`, mixed packaging)
    </ParamField>

    <ParamField body="waste_rd_code_id" type="uuid">
      UUID of the R/D (recovery/disposal) treatment code, taken from
      [`GET /v1/wastes/rd-codes?ler_code_id=…`](/api-reference/wastes/rd-codes), which lists the treatments with an
      emission factor for that LER code. Optional, but send it when you know the treatment: without it only a factor
      defined for the LER code alone can be used.

      **Example:** `"6617caab-9db0-40df-8716-54d8c94e8056"` (`R03`)
    </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">
      Business name of the waste manager that treats the waste.

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

    <ParamField body="transporter_name" type="string | null">
      Business name of the carrier that takes the waste to the treatment plant, when it is not the waste manager.

      **Example:** `"Transportes Levante 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.184Z",
  "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">
  `api_bulk` when sent with an API key; `app` when sent by a person signed in to Dcycle (e.g. the app's waste form).
</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; the job's items say which ones.
    </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. |

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).

<Warning>
  If `chunks_done` stops moving, see [A job that does not finish](/api-reference/ingest-jobs/get#a-job-that-does-not-finish):
  never resend the batch with a new `Idempotency-Key`, because its wastes are already stored.
</Warning>

### Why a record failed to calculate

A failed item carries `error_code: CALCULATION_FAILED` but not the reason yet (`error_params` is `null`). Read the
waste with [`GET /v1/wastes/{waste_id}/detail`](/api-reference/wastes/get):

* `status: "error"`: no emission factor covers its LER/R/D pair. A pair missing from
  [`GET /v1/wastes/rd-codes?ler_code_id=…`](/api-reference/wastes/rd-codes) never has one; a listed pair usually does.
* `status: "review"`: it came with a `description` that matched no factor. It has no emissions until someone picks
  the factor in Dcycle.

## Errors

Errors follow [RFC 9457 problem details](/api-reference/errors) (`Content-Type: application/problem+json`): a stable
`code`, a human `detail`, `errors` when there is a list of problems, and the `request_id` to quote to support.

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

At least one record is invalid. **Nothing was written.** `rejected` is the number of invalid records and `errors`
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"}}
{
  "type": "https://code.dcycle.io/api-reference/errors#BULK_RECORDS_REJECTED",
  "title": "Unprocessable Entity",
  "status": 422,
  "code": "BULK_RECORDS_REJECTED",
  "detail": "3 of the submitted records are invalid; nothing was written.",
  "errors": [
    {
      "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": "UNIT_NOT_SUPPORTED",
      "error_params": { "field": "unit_id", "value": "cab66828-c2b1-431b-92af-f9ab37149d3c", "supported": ["61743a63-ff70-459c-9567-5eee8f7dfd5c"] }
    }
  ],
  "rejected": 3,
  "request_id": "b2248a31-0f1b-4a7d-83ce-b29bd5977b4c"
}
```

| `error_code` | Cause | Fix |
| - | - | - |
| `SCHEMA_INVALID` | The record does not match the record schema: a missing required field (`unit_id` included), a wrong type, `base_quantity` ≤ 0, a date outside 1970 to next year (`activity_date_out_of_range`), `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` or `waste_rd_code_id` does not exist. `error_params.field` names it. | Take the ids from [LER codes](/api-reference/wastes/ler-codes) and [R/D codes](/api-reference/wastes/rd-codes). |
| `UNIT_NOT_SUPPORTED` | `unit_id` is neither kilograms nor the unit of the record's `custom_ef_record_id`. `error_params.supported` lists the accepted unit ids. | Send the quantity in kilograms with `unit_id` `61743a63-ff70-459c-9567-5eee8f7dfd5c`. |
| `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 IDEMPOTENCY\_KEY\_REUSED

The `Idempotency-Key` was already used in this organization with a different body. Nothing is written. Send a new
key for a new batch; to retry the original batch, resend exactly the same body (field order does not matter).

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "https://code.dcycle.io/api-reference/errors#IDEMPOTENCY_KEY_REUSED",
  "title": "Unprocessable Entity",
  "status": 422,
  "code": "IDEMPOTENCY_KEY_REUSED",
  "detail": "This Idempotency-Key was already used with a different request body. Use a new key per batch.",
  "request_id": "b2248a31-0f1b-4a7d-83ce-b29bd5977b4c"
}
```

### 422 REQUEST\_VALIDATION\_FAILED — malformed request

The request itself is invalid: the `Idempotency-Key` header is missing, or `records` is missing, empty or has more
than 5,000 entries. `errors` lists each problem with where it is (`loc`):

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "https://code.dcycle.io/api-reference/errors#REQUEST_VALIDATION_FAILED",
  "title": "Unprocessable Entity",
  "status": 422,
  "code": "REQUEST_VALIDATION_FAILED",
  "detail": "The request is invalid (1 error(s)); see `errors`.",
  "errors": [{ "loc": ["header", "Idempotency-Key"], "msg": "field required", "type": "value_error.missing" }],
  "request_id": "b2248a31-0f1b-4a7d-83ce-b29bd5977b4c"
}
```

### 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": "33d3e986-2e02-4aaa-8be0-708c143c02d9",
          "waste_rd_code_id": "6617caab-9db0-40df-8716-54d8c94e8056",
          "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": "33d3e986-2e02-4aaa-8be0-708c143c02d9",
      },
  ]

  # 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()["errors"]:
          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.errors.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": "33d3e986-2e02-4aaa-8be0-708c143c02d9",
      "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.

<Warning>
  Each share is a waste of its own: [deleting](/api-reference/wastes/delete-v2) that `entity_id` removes only the
  first share. If your integration may need to delete or replace the record later, send one record per facility
  instead of `facility_percentages`, so every waste has its own item and id.
</Warning>

## 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).
* **`422 IDEMPOTENCY_KEY_REUSED`:** the key already belongs to another batch. Use a new key, or resend the original
  body unchanged.

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


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