Skip to main content
POST
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 (see how bulk ingestion works), not the wastes themselves: follow it with GET /v2/ingest-jobs/{id}, or, for a job submitted with an API key, let the ingest_job.finished webhook tell you when it ends, and read the per-record outcomes.
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.

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. It is optional: a waste sent without a distance has no transport emissions (see How the emissions are calculated).
These emissions are reported under Scope 3, Category 5 (waste generated in operations) of the GHG Protocol. A waste record is characterized by:

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. With the LER code alone, Dcycle uses DEFRA’s landfill factor for its material (its combustion factor where DEFRA publishes no landfill one), the conservative choice when the treatment is unknown, or, for a code DEFRA does not cover, OCCC’s average across treatments for its waste fraction. A recovery treatment usually has a lower factor, so send the R/D code whenever you know 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 and counts; the waste is flagged so someone confirms the factor. 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 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.
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. Each year of the waste period is priced with that year’s edition of the database, or with its latest edition for a year it has no edition of. A period that spans several years is split between them in proportion to its days. Transport to the waste center is calculated only when total_km_to_waste_center is greater than 0. A waste in a weight unit is priced in tonne-kilometres (its quantity in tonnes × the distance); a waste in a volume (m3), per vehicle-kilometre. The freight factor is DEFRA’s heavy goods vehicle factor by default; another source, such as ecoinvent, is chosen in the Dcycle app, in the emission factors panel.
Sent with a description? A factor applied with low confidence counts like any other: the waste ends uploaded, with its emissions and LOW_CONFIDENCE_WASTE_FACTOR_MATCH in error_messages (GET /v1/wastes/{waste_id}/detail), and its job item is succeeded. Confirm the factor in the Dcycle app. When no factor could be applied, the waste stays status: "review", without emissions until someone picks a factor in the app, and its job item is failed with CALCULATION_FAILED. A description that names a material but no treatment is calculated with the database’s default treatment for that material, with WASTE_DEFAULT_TREATMENT_ASSUMED in error_messages. To calculate the real treatment, name it in the description (e.g. “recycling”, “landfill”) or send the LER code with its R/D code (waste_ler_code_id and waste_rd_code_id): an R/D code without a LER code is not used.

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.

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 and nothing is written: rejected is the number of invalid records and errors lists them (up to 100). 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} until status is completed or completed_with_errors, then read the per-record outcomes at GET /v2/ingest-jobs/{job_id}/items.
  5. When every chunk is done, a job submitted with an API key also sends ingest_job.finished to your webhook endpoints, so you can wait for it instead of polling. (A batch sent by someone signed in to Dcycle notifies them in the app instead.)

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

Request

Headers

string
required
Your API key (see Authentication).Example: ${DCYCLE_API_KEY}
string
required
UUID of the organization the wastes are created for: in Dcycle, Company settings → Organization info, click Copy organization ID (Get an API Key). Every facility of its perimeter (the organization and its accepted, enabled subsidiaries) is a valid destination.Example: a8315ef3-dd50-43f8-b7ce-d839e68d51fa
string
required
Must be application/json
string
required
1 to 255 characters. Send one key per batch (a UUID works well); it is looked up per organization across every v2 bulk route (wastes, facilities, invoices…). Within 24 hours, the same key with the same route and body returns that same job (with Idempotent-Replayed: true) instead of creating the wastes again, so a timeout or network error can be retried safely. The same key with a different body or on another route is rejected with 422 IDEMPOTENCY_KEY_REUSED: use a new key for a new batch. A retry that arrives while the first request is still being processed gets 409 IDEMPOTENCY_KEY_IN_PROGRESS. After 24 hours the key is free and a request with it is a new one, so retry within that window. See The Idempotency-Key.Example: c41d8e77-5d40-4a11-9e8b-2c9f4d1e7a03

Query Parameters

boolean
default:"false"
Validate only. The record checks run without writing: an invalid batch answers the same 422, 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 check or store it, so you can send the real request with the same key afterwards. See Dry run.

Body Parameters

array[object]
required
The wastes to create: between 1 and 5,000 objects. Each record has the fields below; a field it does not declare (a typo, or facility_id) rejects the record with SCHEMA_INVALID.

Response

202 Accepted

Every record passed validation and was stored. The body is the ingest job; the Location header holds its path.
uuid
Ingest job id. Use it with GET /v2/ingest-jobs/{job_id}.
string
processing right after the request. See Job status.
string
Always wastes for this endpoint.
string
Always create.
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).
object
integer
Number of calculation chunks (100 records each).
integer
Chunks already calculated. chunks_done / chunks_total is the job’s progress.
datetime
When the job was created (UTC).
datetime | null
When the last chunk finished (UTC), null while the job is processing.
self: the job. items: its per-record outcomes.

Job status

While a job is processing, the new wastes already exist without emissions (co2e is null); the value is filled in when their chunk is calculated. Each record’s waste id is its entity_id in the job’s items.
If chunks_done stops moving, see A job that does not finish: never resend the batch with a new Idempotency-Key, because its wastes are already stored.

Why a record failed to calculate

A failed item carries error_code: CALCULATION_FAILED with error_params: null. For the reason, read the waste with GET /v1/wastes/{waste_id}/detail:
  • status: "error": no emission factor covers its LER/R/D pair. A pair missing from GET /v1/wastes/rd-codes?ler_code_id=… 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 (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. Each record carries one error_code: the first rule it breaks. A record with two problems shows the second one only after you fix the first, so validate again with ?dry_run=true before sending the batch. (SCHEMA_INVALID is the exception inside a record: its error_params.errors lists all of the record’s schema problems, up to 10.)

422 IDEMPOTENCY_KEY_REUSED

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

409 IDEMPOTENCY_KEY_IN_PROGRESS

Another request with the same Idempotency-Key was still being processed, typically your own retry after a timeout. This request wrote nothing. Wait the Retry-After seconds and retry with the same key and body: you get the job of the request that went through. See IDEMPOTENCY_KEY_IN_PROGRESS.

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

401, 400 and 403: key and organization

Before the records are looked at, the API key and the organization are checked. Each code has its section in Errors:
  • 401: no API key (AUTHENTICATION_REQUIRED), a wrong or revoked one (INVALID_API_KEY), an expired one (API_KEY_EXPIRED), a key of another organization (API_KEY_ORG_MISMATCH) or one too old to have an owner (API_KEY_NO_OWNER).
  • 400 ORGANIZATION_ID_REQUIRED: no x-organization-id.
  • 403: the key’s creator is not a member of the organization in x-organization-id (LOGGED_USER_NOT_MEMBER), the key is read-only (API_KEY_SCOPE_DENIED), or its creator is a Viewer (VIEWER_CANNOT_WRITE).

429 TOO_MANY_REQUESTS

The API key, or its IP, sent more requests than the rate limits allow. Nothing was written: wait the Retry-After seconds and send the same request again, with the same Idempotency-Key. See TOO_MANY_REQUESTS.

Examples

Create a batch and wait for the calculation

Split one waste across two facilities

This creates two wastes (60 and 40 units) sharing one source_invoice_id, the id of the first one, as List Wastes and Get Waste return it. The job has one item for the record, whose entity_id is the first waste of the group.
Each share is a waste of its own: deleting that entity_id removes only the first share (Delete Wastes shows how to find every share). If your integration may need to delete or replace the record later, send one record per facility with that facility’s quantity ([{facility_id, percentage: 1}]), so every waste has its own item and id.

Retries and idempotency

  • Network error or timeout: retry within 24 hours with the same Idempotency-Key and body. 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.
  • 409 IDEMPOTENCY_KEY_IN_PROGRESS: the first request with that key is still being processed. Wait Retry-After seconds and retry with the same key and body.
  • 429 TOO_MANY_REQUESTS: nothing was written. Wait Retry-After seconds and resend with the same key and body.

Limits

Get Ingest Job

Status and progress of the batch

List Ingest Job Items

Per-record outcomes, including calculation failures

Webhooks

Be told when the job finishes instead of polling