> ## 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 Waste Water Treatments (v2)

> Write up to 5,000 measurements of waste water treatment plants in one request; their emissions are calculated asynchronously

[← Waste Water Treatments](/api-reference/waste-water-treatments/list)

Write one or many measurements of waste water treatment plants (WWTP, EDAR). A measurement is what a plant measured
during a period, typically a month: the water in and out, the sludge, and the BOD and nitrogen of each treatment line.
Dcycle calculates its CH₄ and N₂O emissions in the background with the lines the plant was created with. The
measurements are the ones [`GET /v1/waste-water-treatments`](/api-reference/waste-water-treatments/list) reads.

The plant is a facility created with a `waste_water` block in
[`POST /v2/facilities`](/api-reference/facilities/create-v2#waste-water-treatment-plants), which sets its three
treatment lines.

The request checks every record and writes them all at once, then answers `202 Accepted` with an
[ingest job](/api-reference/ingest-jobs/get) that calculates them (see
[how bulk ingestion works](/api-reference/ingest-jobs/overview)). Follow it with
[`GET /v2/ingest-jobs/{id}`](/api-reference/ingest-jobs/get) or, for a job submitted with an API key, the
[`ingest_job.finished`](/api-reference/webhooks/overview) webhook.

> [!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/waste-water-treatments" \
  -H "x-api-key: ${DCYCLE_API_KEY}" \
  -H "x-organization-id: ${DCYCLE_ORG_ID}" \
  -H "Idempotency-Key: 4b3a2918-7f6e-4d5c-9b8a-1e0d9c8b7a69" \
  -H "Content-Type: application/json" \
  -d '{
    "records": [
      {
        "facility_id": "660e8400-e29b-41d4-a716-446655440000",
        "name": "January 2026",
        "start_date": "2026-01-01",
        "end_date": "2026-01-31",
        "m3_water_in": 125000,
        "m3_water_out": 120000,
        "kg_sludge": 48000,
        "kg_bod_per_m3_wwt_line": 0.3,
        "kg_bod_per_kg_sludge_line": 0.05,
        "kg_bod_per_m3_wwd_line": 0.02,
        "kg_n_per_m3_wwt_line": 0.04,
        "kg_n_per_m3_wwd_line": 0.01,
        "kg_r_wwt_line": 150,
        "client_row_id": "EDAR-SUR-2026-01"
      }
    ]
  }'
```

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

import requests

response = requests.post(
    "https://api.dcycle.io/v2/waste-water-treatments",
    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": [
            {
                "facility_id": "660e8400-e29b-41d4-a716-446655440000",
                "name": "February 2026",
                "start_date": "2026-02-01",
                "end_date": "2026-02-28",
                "m3_water_in": 118000,
                "m3_water_out": 114000,
                "kg_sludge": 45500,
                "kg_bod_per_m3_wwt_line": 0.29,
                "kg_bod_per_kg_sludge_line": 0.05,
                "kg_bod_per_m3_wwd_line": 0.02,
                "kg_n_per_m3_wwt_line": 0.04,
                "kg_n_per_m3_wwd_line": 0.01,
                "client_row_id": "EDAR-SUR-2026-02",
            }
        ]
    },
    timeout=60,
)
response.raise_for_status()
job = response.json()
print(job["id"], job["status"])
```

```json 202 theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "id": "5d4c3b2a-1908-4f7e-8d6c-5b4a39281706",
  "status": "processing",
  "entity_type": "waste_water_treatments",
  "operation": "create",
  "source": "api_bulk",
  "counts": { "submitted": 1, "succeeded": 0, "failed": 0 },
  "chunks_total": 1,
  "chunks_done": 0,
  "created_at": "2026-10-21T09:12:44.031Z",
  "finished_at": null,
  "links": {
    "self": "/v2/ingest-jobs/5d4c3b2a-1908-4f7e-8d6c-5b4a39281706",
    "items": "/v2/ingest-jobs/5d4c3b2a-1908-4f7e-8d6c-5b4a39281706/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": "EDAR-SUR-2026-01",
      "error_code": "FACILITY_NOT_WASTE_WATER",
      "error_params": { "field": "facility_id", "value": "660e8400-e29b-41d4-a716-446655440000" }
    }
  ],
  "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 plant included: `facility_id` must be an enabled facility of your
   [perimeter](/api-reference/ingest-jobs/overview#perimeter), a waste water treatment plant, with its three treatment
   lines set. If any record is invalid the **whole** request is rejected with `422` and nothing is written; `rejected`
   is the number of invalid records and `errors` lists them (up to 100).
2. **The measurements are written at once**, with status `uploaded`. Each belongs to its plant, so to the plant's
   organization (the `x-organization-id` one or a subsidiary of it). Periods of a plant may overlap, as in v1 and the
   Data Importer: nothing is merged or replaced.
3. **The job calculates them** in the background, in chunks, with the IPCC 2019 method and the emission factors of
   the plant's lines:

   | Line | Gas | From | Emission `category` |
   | - | - | - | - |
   | Water treatment (`wwt_line_id`) | CH₄ | The BOD treated (`m3_water_in` × `kg_bod_per_m3_wwt_line`) minus the BOD removed with the sludge (`kg_sludge` × `kg_bod_per_kg_sludge_line`), less the CH₄ recovered (`kg_r_wwt_line`) | `waste_water_treatment_water` |
   | Water treatment (`wwt_line_id`) | N₂O | The nitrogen treated (`m3_water_in` × `kg_n_per_m3_wwt_line`) | `waste_water_treatment_population` |
   | Sludge (`sludge_line_id`) | CH₄ | The sludge (`kg_sludge`), less the CH₄ recovered (`kg_r_sludge_line`) | `waste_water_treatment_sludge` |
   | Discharge (`wwd_line_id`) | CH₄ and N₂O | The BOD and nitrogen discharged (`m3_water_out` × `kg_bod_per_m3_wwd_line` and × `kg_n_per_m3_wwd_line`) | `waste_water_treatment_discharge` |

   No line's CH₄ goes below 0. The gases are converted to CO₂e with the IPCC GWPs and spread over the days of the
   period. The treatment and sludge lines are direct emissions of the plant; the discharge is reported apart, under
   other Scope 3 emissions (WWTP discharges). A measurement keeps the status `uploaded` with its `co2e`; one whose
   calculation fails ends `error`, without emissions, and its job item is `failed` with `CALCULATION_FAILED`.
4. **When every chunk is done** the job is `completed` (or `completed_with_errors`). For a job submitted with an API
   key, `ingest_job.finished` is sent to the webhook endpoints of the `x-organization-id` organization.

### Coming from `POST /v1/waste-water-treatments`

The record is the measurement v1 takes, with these differences:

* Up to 5,000 measurements per request, all or nothing, with an `Idempotency-Key`; the answer is a job, not the
  measurement.
* A period cannot end in the future.
* A facility that is not a plant is a record code (`FACILITY_NOT_WASTE_WATER`), not a `400`; a plant without its
  three lines is rejected too (`WASTE_WATER_LINES_NOT_CONFIGURED`), where v1 stores the measurement and it ends in
  `error`.
* There is no `file_url`, nor any other field not listed below.
* `kg_r_wwt_line` and `kg_r_sludge_line` omitted are `0`.

## Request

### Headers

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

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

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

Your organization. The plants of its [perimeter](/api-reference/ingest-jobs/overview#perimeter) (its own and those of
the subsidiaries it may write to) can take measurements. The job and its `ingest_job.finished` webhook belong to this
organization.

**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 returns the original job (with `Idempotent-Replayed: true`) instead of
writing the measurements 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, no job; `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 measurements to write, 1 to 5,000. A field not listed here is rejected (`SCHEMA_INVALID`). Every number is 0 or
more (and under 10¹²).

**record**

**facility\_id** `uuid` *required*

The plant: a facility created with `waste_water` in
[`POST /v2/facilities`](/api-reference/facilities/create-v2#waste-water-treatment-plants) (or with
`POST /v1/facilities/waste_water`). Find it with [`GET /v1/facilities`](/api-reference/facilities/list).

**name** `string`

A label for the measurement, up to 255 characters. **Example:** `"January 2026"`

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

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

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

Last day of the period, on or after `start_date` and not in the future.

**m3\_water\_in** `number` *required*

Influent water volume, in m³.

**m3\_water\_out** `number` *required*

Effluent (discharged) water volume, in m³.

**kg\_sludge** `number` *required*

Sludge produced, in kg.

**kg\_bod\_per\_m3\_wwt\_line** `number` *required*

BOD of the influent in the water treatment line, in kg BOD/m³.

**kg\_bod\_per\_kg\_sludge\_line** `number` *required*

BOD of the sludge in the sludge line, in kg BOD/kg sludge.

**kg\_bod\_per\_m3\_wwd\_line** `number` *required*

BOD of the effluent in the discharge line, in kg BOD/m³.

**kg\_n\_per\_m3\_wwt\_line** `number` *required*

Nitrogen of the influent in the water treatment line, in kg N/m³.

**kg\_n\_per\_m3\_wwd\_line** `number` *required*

Nitrogen of the effluent in the discharge line, in kg N/m³.

**kg\_r\_wwt\_line** `number` (default: 0)

CH₄ recovered in the water treatment line, in kg.

**kg\_r\_sludge\_line** `number` (default: 0)

CH₄ recovered in the sludge line, in kg.

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

Your own reference for the record, up to 255 characters, echoed in errors and in the job's items.

## Response

`202 Accepted` with the job (`entity_type: waste_water_treatments`, `operation: create`) and a `Location` header to
it. To read the result:

* [`GET /v2/ingest-jobs/{id}/items`](/api-reference/ingest-jobs/list-items) has one item per record, in the order
  sent. Its `entity_id` is the measurement's id. A `failed` item carries its `error_code`.
* [List Waste Water Treatments](/api-reference/waste-water-treatments/list) (`?facility_id=` the plant) returns the
  measurements with their status and emissions (`co2e`, kg CO₂e).

> [!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 measurements are already stored.

## 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: a missing or unknown field (e.g. `file_url`), a negative number, an `end_date` before `start_date`, a period that ends in the future, a year before 1970... `error_params.errors` says where. |
| `REFERENCE_NOT_FOUND` | `facility_id` does not exist, is disabled or is outside your organization's [perimeter](/api-reference/ingest-jobs/overview#perimeter) (all answer the same; `error_params.field` is `facility_id`). |
| `FACILITY_NOT_WASTE_WATER` | `facility_id` is a facility, but not a waste water treatment plant. Create the plant with `waste_water` in [`POST /v2/facilities`](/api-reference/facilities/create-v2#waste-water-treatment-plants). |
| `WASTE_WATER_LINES_NOT_CONFIGURED` | The plant has not its three treatment lines set, so its emissions could not be calculated. |

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 5,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 5,000 |
| Measurements per calculation chunk | 100 |

## Related Endpoints

> **[Delete Waste Water Treatments (v2)](/api-reference/waste-water-treatments/delete-v2)**
>
> Delete measurements by id

> **[Create Facilities (v2)](/api-reference/facilities/create-v2#waste-water-treatment-plants)**
>
> Create the plant with its treatment lines

> **[Get Ingest Job](/api-reference/ingest-jobs/get)**
>
> Follow the calculation

> **[List Waste Water Treatments](/api-reference/waste-water-treatments/list)**
>
> The measurements and their emissions


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