WTNcloud REST API
Create waste transfer notes (WTNs) and Hazardous Waste Consignment Notes (HWCNs), and check their status programmatically. Integrate WTNcloud with Google Sheets, your own CRM, or any system that can make HTTP requests.
Authentication
All API requests require a Bearer token in the Authorization header. Your API key is available on the API Settings page in your dashboard (Enterprise plan only).
Authorization: Bearer YOUR_API_KEY
Keep your API key secret. Do not expose it in client-side code or public repositories.
Base URL
https://wtncloud.co.uk/api/external
/wtn/createCreate one or more waste transfer notes in bulk. Each WTN is linked to a customer (matched by name and address, or created automatically). Optionally assign to a driver by email.
Request Body
Send a JSON object with a wtns array (1–100 items).
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Required | Customer name |
| address | string | Required | Customer address |
| string | Optional | Customer email | |
| phone | string | Optional | Customer phone number |
| driverEmail | string | Optional | Assign to a driver by their email address |
| dateOfTransfer | string | Optional | ISO date (e.g. 2026-04-15). Defaults to today |
| ewcCode | string | Optional | European Waste Catalogue code |
| wasteDescription | string | Optional | Description of the waste |
| quantity | number | Optional | Amount of waste |
| unit | string | Optional | Unit of measurement (kg, tonnes, litres, etc.) |
| containmentType | string | Optional | How the waste is contained (bags, skip, drums, etc.) |
| additionalWasteLines | array | Optional | Extra waste streams on the same transfer — the fields above are line 1. Omit (or send null) for a single-waste note. Per entry: ewcCode, wasteDescription, quantity, unit and containmentType are required; containmentOther, physicalForm (Gas | Liquid | Solid | Powder | Sludge | Mixed), numberOfContainers (default 1) and containsPops (default false) are optional. containsPops is answered PER LINE — the note-level POPs details apply to whichever lines declare it. Max 50 extra lines; hazardous EWC codes are rejected on every line, not just the first. |
| saveCustomer | boolean | Optional | Save the customer for reuse in future WTNs |
Example Request
curl -X POST https://wtncloud.co.uk/api/external/wtn/create \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"wtns": [
{
"name": "Acme Construction Ltd",
"address": "12 High Street, Bristol, BS1 2AB",
"email": "[email protected]",
"driverEmail": "[email protected]",
"ewcCode": "17 01 01",
"wasteDescription": "Concrete rubble",
"quantity": 500,
"unit": "kg",
"containmentType": "Skip",
"additionalWasteLines": [
{
"ewcCode": "17 04 07",
"wasteDescription": "Mixed metals",
"quantity": 300,
"unit": "kg",
"containmentType": "Skip",
"physicalForm": "Solid",
"numberOfContainers": 1,
"containsPops": false
}
],
"saveCustomer": true
}
]
}'Response
{
"created": [
{
"wtnReference": "WTN-00042",
"wtnId": "a1b2c3d4e5f6...",
"driverEmail": "[email protected]",
"status": "ASSIGNED"
}
]
}Status will be "ASSIGNED" if a matching active driver was found, or "DRAFT" if no driver was specified or matched.
/wtn/statusCheck the status of your waste transfer notes. Query by WTN reference numbers or fetch all WTNs created since a given date.
Query Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| references | string | Optional | Comma-separated WTN references (e.g. WTN-00042,WTN-00043) |
| since | string | Optional | ISO date — returns WTNs created on or after this date (max 100 results) |
Provide either references or since, not both.
Example Request
curl "https://wtncloud.co.uk/api/external/wtn/status?references=WTN-00042" \ -H "Authorization: Bearer YOUR_API_KEY"
Response
{
"wtns": [
{
"wtnReference": "WTN-00042",
"wtnId": "a1b2c3d4e5f6...",
"status": "COMPLETED",
"assignmentStatus": "COMPLETED",
"driverName": "John Smith",
"driverEmail": "[email protected]",
"pdfUrl": true,
"createdAt": "2026-04-15T10:30:00.000Z",
"updatedAt": "2026-04-15T14:22:00.000Z"
}
]
}HWCN endpoints
Hazardous Waste Consignment Notes (HWCNs) for England (SI 2005/894) and Wales (WSI 2005/1806). The endpoints follow the same shape as the WTN ones above — Bearer-token auth, bulk envelope, ApiLog tracking — but differ in two important ways:
- Drafts are free.
POST /hwcn/createlands rows asDRAFTwith no charge. The free-WTN quota / PAYG balance is only touched when a dashboard user (or driver app) signs Part D and completes the note. This matches the dashboard HWCN flow — see the FAQ on /tools/hwcn-generator. - Country-specific format checks. Each item is validated against the same Schedule 4 rules the dashboard form applies — England rejects premises codes (abolished 2016), Wales requires either a registered NRW premises code or the
EXEMPTsentinel, SIC 2003 only accepted on Welsh notes, etc. - Scotland and Northern Ireland aren't supported via the API today. Those use regulator-issued consignment codes (SEPA, NIEA) that the producer can't generate locally.
/hwcn/createBulk-create HWCN drafts. Each item gets a unique producer-generated consignment code on creation; the row stays in DRAFT until an operator completes Part D from the dashboard.No billing on this endpoint.
Request Body
Send a JSON object with an hwcns array (1–100 items). Each item is the same Parts A–C shape the dashboard form collects, minus the signed declaration (which is added when the operator completes the draft).
| Field | Type | Required | Description |
|---|---|---|---|
| country | "england" | "wales" | Required | Country the waste is being collected from. Drives statute citations and code format. |
| consignor | object | Required | Your company. companyName / contactName / address / email / sicCode required; phone optional. Wales: walesPremisesKind ('registered' | 'exempt') and either a 6-char NRW premises code (AAAnnn) or 'EXEMPT'. |
| producer | object | Optional | Original waste producer if different from consignor. companyName / address required when present. |
| consignee | object | Required | Receiving facility. companyName / address / permitOrLicenceNumber required. Set operatesUnderExemption + exemptionNumber together if applicable. |
| multipleConsignments | object | Optional | Succession of multiple notes. Set isFirstInSuccession=true plus consignmentCount, frequency, durationMonths. |
| waste | object | Required | EWC code (must be hazardous, ends in *), HP codes (HP1–HP15 or POP, ≥1), processGivingRiseToWaste, description, physicalForm, quantityKg, containerType, containerCount. Optional ADR sub-section. |
| carrier | object | Required | Registered waste carrier details — companyName, address, registrationNumber (CB-prefixed), vehicleRegistration, collectionDate (ISO), carrierCertificateAccepted: true. |
Example Request
curl -X POST https://wtncloud.co.uk/api/external/hwcn/create \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"hwcns": [
{
"country": "england",
"consignor": {
"companyName": "Acme Waste Ltd",
"contactName": "Jane Operator",
"address": { "line1": "1 High Street", "city": "London", "postcode": "SW1A 1AA" },
"email": "[email protected]",
"sicCode": "38120",
"sicVersion": "SIC_2007"
},
"consignee": {
"companyName": "Disposal Site PLC",
"address": { "line1": "10 Industrial Way", "city": "Reading", "postcode": "RG1 1AA" },
"permitOrLicenceNumber": "EPR/AB1234CD",
"operatesUnderExemption": false
},
"waste": {
"processGivingRiseToWaste": "Vehicle workshop oil change",
"description": "Used engine oil",
"ewcCode": "13 02 05*",
"physicalForm": "liquid",
"quantityKg": 200,
"hpCodes": ["HP3", "HP14"],
"containerType": "drum",
"containerCount": 1
},
"carrier": {
"companyName": "Hauliers Ltd",
"address": { "line1": "Yard 4", "city": "Slough", "postcode": "SL1 1AA" },
"registrationNumber": "CB/AB1234",
"vehicleRegistration": "AB12 CDE",
"collectionDate": "2026-04-30",
"carrierCertificateAccepted": true
}
}
]
}'Response
{
"created": [
{
"consignmentCode": "ACMEWA/AB23F",
"hwcnId": "a1b2c3d4e5f6...",
"status": "DRAFT",
"customerId": null
}
]
}Code format. England returns XXXXXX/YYYYY (six chars from your company name, padded with Q; five-char suffix). Wales registered: AAAnnn/XXXXX. Wales exempt: EXE/XXXXXXXX. Codes use a confusable-free alphabet (no 0/O/1/I/L/5/S) so they read aloud cleanly.
customerId is populated when we find an existing saved customer matching the consignor by name+address (case-insensitive) or by email. We never overwrite saved customer details from API payloads.
/hwcn/statusLook up HWCNs by consignment code or fetch all HWCNs created since a given date. Soft-deleted rows are excluded.
Query Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| references | string | Optional | Comma-separated consignment codes (e.g. ACMEWA/AB23F,ABC123/0001A). |
| since | string | Optional | ISO date — returns HWCNs created on or after this date (max 100 results, newest first). |
Provide either references or since, not both.
Example Request
curl "https://wtncloud.co.uk/api/external/hwcn/status?references=ACMEWA%2FAB23F" \ -H "Authorization: Bearer YOUR_API_KEY"
Note the URL-encoded slash (%2F) inside the consignment code — every HWCN code contains a slash separator.
Response
{
"hwcns": [
{
"consignmentCode": "ACMEWA/AB23F",
"hwcnId": "a1b2c3d4e5f6...",
"jurisdiction": "england",
"status": "COMPLETED",
"pdfUrl": true,
"completedAt": "2026-04-30T10:15:00.000Z",
"createdAt": "2026-04-26T22:00:00.000Z",
"updatedAt": "2026-04-30T10:15:00.000Z"
}
]
}status is one of DRAFT, COMPLETED, CANCELLED.pdfUrl is a boolean indicating whether a PDF has been generated for this HWCN; the actual PDF is only fetchable from the dashboard (a public download URL is intentionally not exposed via this API).
Receiver API
ENTERPRISEFor permitted receiving sites that want their own software to see what is arriving. One API key addresses exactly one permitted site — a multi-site operator runs one account and one key per site, which is what keeps DEFRA reporting unambiguous.
WTNcloud is the registered Digital Waste Tracking software supplier and files every receipt with DEFRA on your behalf, so your integration never needs its own DEFRA registration or credentials. These endpoints require the receiver:read and receiver:write scopes, which you switch on yourself under Settings → API in your dashboard. Your account also needs the receiver role and its site permit — both your own settings too, and the API names whichever is missing.
Two kinds of paperwork, two lists. Non-hazardous loads arrive on a waste transfer note and are resolved by accepting or rejecting them. Hazardous loads move under a consignment note (HWCN) and are resolved by signing Part E.
The two listing endpoints — /receiver/inbound and /receiver/movements — return each kind under its own key: inbound / movements for transfer notes, and consignments for hazardous. That is what lets an integration adopt the hazardous side when it is ready without its existing code changing behaviour. The write endpoints act on one record and return that record, not a list — a transfer note answers with wtnId, a consignment with hwcnId and its consignment code.
When a DEFRA receive is filed. A rejection never files one — no waste was received at your site. An accepted or partially accepted Part E always files. Accepting a transfer note files only when you give a recoveryDisposalCode; omit it and the receipt is logged without filing, which is the contract the dashboard has for a site not yet ready to declare a recovery or disposal route. Every response says which happened in dwt.filed.
/receiver/inboundTransfer notes carriers have fired to your site on arrival, and hazardous consignment notes addressed to it — the latter are raised by the producer and travel with the load rather than being fired. Defaults to whatever is still awaiting a decision: an accept or reject for a transfer note, a Part E for a consignment. Your to-do list.
| Field | Type | Required | Description |
|---|---|---|---|
| status | string | Optional | pending (default), resolved, or all. |
| since | string | Optional | ISO date. Filters inbound transfer notes by when they were FIRED to you, and consignments by when the note was RAISED — a consignment note is never fired, it is raised by the producer and arrives with the load. Both lists then read oldest-first so a backlog can be paged. |
| limit | number | Optional | Page size, default 50, maximum 100. |
Ordering: the pending worklist and any since query return oldest first, so you can work through a backlog larger than one page and advance a since cursor. Browsing resolved or all without a since returns newest first.
Example Request
curl "https://wtncloud.co.uk/api/external/receiver/inbound?status=pending" \ -H "Authorization: Bearer YOUR_API_KEY"
Response
{
"inbound": [
{
"wtnId": "a1b2c3d4e5f6...",
"wtnReference": "WTN-00042",
"firedByCompany": "Acme Haulage Ltd",
"carrierCompany": null,
"producerCompany": "Riverside Joinery",
"vehicleRegistration": "AB12 CDE",
"siteName": "Northside Transfer Station",
"firedAt": "2026-07-24T09:12:00.000Z",
"dateOfTransfer": "2026-07-24T00:00:00.000Z",
"batchId": null,
"resolution": "pending",
"receivedAt": null,
"waste": { "ewcCode": "17 09 04", "description": "Mixed C&D waste", "quantity": 12.5, "unit": "tonnes" },
"acceptedQuantity": null,
"recoveryDisposalCode": null,
"hasPdf": true
}
],
"consignments": [
{
"hwcnId": "b2c3d4e5f6a1...",
"consignmentCode": "ACMEWA/AB23F",
"consignmentCodeMissingReason": null,
"jurisdiction": "england",
"raisedByCompany": "Acme Waste Ltd",
"consignorCompany": "Riverside Joinery",
"carrierCompany": "Hauliers Ltd",
"vehicleRegistration": "AB12 CDE",
"siteName": "Northside Transfer Station",
"createdAt": "2026-07-24T09:00:00.000Z",
"resolution": "pending",
"partE": "awaiting",
"receivedAt": null,
"acceptedQuantityKg": null,
"recoveryDisposalCode": null,
"containsPops": false,
"waste": {
"ewcCode": "13 02 05*",
"description": "Used engine oil",
"quantityKg": 200,
"physicalForm": "liquid",
"hpCodes": ["HP_3", "HP_14"],
"containerType": "drum",
"containerCount": 1,
"specialHandling": null
},
"additionalWasteLines": [],
"hasPdf": true
}
]
}consignments are hazardous loads awaiting your Part E. status filters them the same way (pending = Part E not yet signed), and since reads the note's creation — nobody "fires" a consignment note, the producer raises it and the load turns up with it. limit applies to each list independently, so one kind can never hide behind the other. A consignment carrying several EWC-coded streams under one code lists the extras in additionalWasteLines, each with its own hazard codes and weight.
/receiver/movementsLoads you have actually taken in — both notes fired to you and your own captures of paper or emailed notes — with their DEFRA tracking state.
| Field | Type | Required | Description |
|---|---|---|---|
| references | string | Optional | Comma-separated WTN references AND/OR HWCN consignment codes (max 100 in total). Each is matched against its own list, so one request can reconcile both kinds. |
| since | string | Optional | ISO date — movements received on or after this date. |
| limit | number | Optional | Page size, default 50, maximum 100. |
Provide either references or since. More than 100 references is rejected rather than truncated, so a reconciliation run can never mistake a trimmed response for a complete one. Ordering: a since sync returns oldest first so you can page the whole window; reference lookups return newest first.
Response
{
"movements": [
{
"wtnId": "a1b2c3d4e5f6...",
"wtnReference": "WTN-00042",
"firedByCompany": "Acme Haulage Ltd",
"siteName": "Northside Transfer Station",
"dateOfTransfer": "2026-07-24T00:00:00.000Z",
"receivedAt": "2026-07-24T10:02:00.000Z",
"resolution": "accepted",
"origin": "fired",
"source": "DIGITAL",
"waste": { "ewcCode": "17 09 04", "description": "Mixed C&D waste", "quantity": 12.5, "unit": "tonnes" },
"acceptedQuantity": 12.1,
"recoveryDisposalCode": "R13",
"hasPdf": true,
"dwt": {
"wasteTrackingId": "2026ABC123",
"status": "SUBMITTED",
"env": "production",
"submittedAt": "2026-07-24T10:02:05.000Z",
"lastError": null
}
}
],
"consignments": [
{
"hwcnId": "b2c3d4e5f6a1...",
"consignmentCode": "ACMEWA/AB23F",
"jurisdiction": "england",
"raisedByCompany": "Acme Waste Ltd",
"carrierCompany": "Hauliers Ltd",
"siteName": "Northside Transfer Station",
"origin": "consigned",
"receivedAt": "2026-07-25",
"receivedTime": "10:02",
"resolution": "accepted",
"acceptedQuantityKg": 195,
"recoveryDisposalCode": "D10",
"containsPops": false,
"waste": { "ewcCode": "13 02 05*", "description": "Used engine oil", "quantityKg": 200 },
"additionalWasteLines": [],
"hasPdf": true,
"dwt": {
"wasteTrackingId": "2026DEF456",
"status": "SUBMITTED",
"env": "production",
"submittedAt": "2026-07-25T10:02:09.000Z",
"lastError": null
}
}
]
}Hazardous receipts carry the same dwt block, because a hazardous receive is filed with DEFRA exactly like a non-hazardous one — that is what makes them reconcilable. references matches consignment codes, and since filters on when the receipt completed. origin is consigned (a note raised by someone else and receipted here) or capture (your own log of a paper note that arrived with a load).
dwt.status is one of PENDING, SUBMITTED, FAILED, AMENDED, NOT_REQUIRED, or null when no submission has been attempted. Filing happens asynchronously after receipt, so a movement read immediately may still show PENDING. origin is fired (a note sent to you digitally) or capture (your own scan of a paper or emailed note).
/receiver/wtn/{id}/pdfDownloads the PDF for a transfer note at your site. Returns the file itself (application/pdf), not a URL. For a hazardous consignment note, use /receiver/hwcn/{id}/pdf below.
curl "https://wtncloud.co.uk/api/external/receiver/wtn/a1b2c3d4e5f6/pdf" \ -H "Authorization: Bearer YOUR_API_KEY" -o consignment.pdf
A PDF is rendered shortly after a note is finalised, so polling too quickly can return 404 PDF_NOT_READY with a Retry-After header — retry rather than treating it as missing. A transfer note that is not addressed to your site returns a plain 404.
/receiver/hwcn/{id}/receiptHAZARDOUSRecord Part E — the consignee receipt — on a hazardous consignment addressed to your site. This is the hazardous equivalent of accepting a fired transfer note: what a receiving site does with a consignment note is declare what it actually took in and what it did with it, and that declaration is what DEFRA's receive is built from. Needs the receiver:write scope.
| Field | Type | Required | Description |
|---|---|---|---|
| dateReceived | string | Required | The date the waste was received (YYYY-MM-DD), as a UK calendar date. |
| timeReceived | string | Optional | UK wall-clock time of receipt (HH:MM, 24-hour). DEFRA asks for the exact time; without it the filing can only report midnight. |
| quantityReceivedKg | number | Required | What you actually took in, in kilograms — the figure filed with DEFRA. |
| quantityReceivedIsEstimate | boolean | Optional | Whether that figure was gauged rather than weighed. Send it either way when you know: an explicit false is what stops a weighed receipt being filed under the consignment's own estimated declaration. |
| accepted | "accepted" | "partially_accepted" | "rejected" | Optional | Defaults to accepted. A rejection files nothing with DEFRA — no waste was received at your site. |
| rdCode | string | Required | The recovery or disposal operation applied at your site (e.g. D10). |
| rdCodesByLine | array | Optional | Per-waste-line R/D codes for a multi-stream consignment, indexed 0 = the primary line, 1..n = additionalWasteLines. Omit unless a line went somewhere other than rdCode — solvent to R2, contaminated soil to D1. |
| sitePermitOrExemption | string | Optional | Your permit or exemption, when it differs from the one printed on the note. Filed as DEFRA's receiver.authorisationNumber. |
| signatoryName | string | Optional | Who signed for the load. Defaults to your company name. |
| certificateConfirmed | true | Required | Explicit confirmation of the Part E certificate. Part E is a statutory declaration, so an integration has to make it deliberately rather than by omission. |
Example Request
curl -X POST "https://wtncloud.co.uk/api/external/receiver/hwcn/b2c3d4e5f6a1/receipt" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"dateReceived": "2026-07-25",
"timeReceived": "10:02",
"quantityReceivedKg": 195,
"quantityReceivedIsEstimate": false,
"accepted": "partially_accepted",
"rdCode": "D10",
"certificateConfirmed": true
}'Response
{
"ok": true,
"hwcnId": "b2c3d4e5f6a1...",
"consignmentCode": "ACMEWA/AB23F",
"partE": "signed",
"dwt": {
"filed": true,
"reason": "A DEFRA receive was triggered. Poll /receiver/movements for its status and wasteTrackingId."
}
}No signature image, and no Idempotency-Key. There is no canvas over an API, so the authenticated call is the signature — the same reading the transfer-note accept endpoint takes. The consignment id is a natural key, so a second call is refused with 409 and a retry after a lost response is safe. A consignment not addressed to your site returns a plain 404.
/receiver/hwcn/{id}/pdfHAZARDOUSDownloads the consignment note itself (application/pdf), for a consignment at your site.
curl "https://wtncloud.co.uk/api/external/receiver/hwcn/b2c3d4e5f6a1/pdf" \ -H "Authorization: Bearer YOUR_API_KEY" -o consignment-note.pdf
The PDF is re-rendered on each signature, so a note still being signed can return 404 PDF_NOT_READY — honour Retry-After. A consignment you logged yourself from a paper note returns 404 NO_PDF_FOR_CAPTURE: WTNcloud generated no document for it, because the original that arrived with the load is the legal record and is held in your dashboard.
Recording receipts
ENTERPRISEThese endpoints record what happened to a load at your site, and are what cause a Digital Waste Tracking receipt to be filed with DEFRA on your behalf. They need the receiver:write scope.
Filing happens in the background once the receipt is recorded, so the response tells you whether a submission was triggered, not its outcome. Poll /receiver/movements for the wasteTrackingId.
/receiver/inbound/{id}/acceptAccept a transfer note fired to your site, in full or in part. The hazardous equivalent is Part E — see /receiver/hwcn/{id}/receipt.
| Field | Type | Required | Description |
|---|---|---|---|
| recoveryDisposalCode | string | Optional | R/D code applied at your site (e.g. R13). Omit to log the receipt WITHOUT filing a DEFRA receive. |
| acceptedQuantity | number | Optional | Confirmed (e.g. weighbridge) quantity. Required when partial is true. |
| partial | boolean | Optional | Record a partial acceptance rather than accepting the full stated load. |
| receivedAt | string | Optional | ISO datetime of receipt. Defaults to now. |
| signatoryName | string | Optional | Who confirmed receipt. Defaults to your company name. |
Example Request
curl -X POST "https://wtncloud.co.uk/api/external/receiver/inbound/a1b2c3d4e5f6/accept" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "recoveryDisposalCode": "R13", "acceptedQuantity": 12.1 }'Response
{
"ok": true,
"wtnId": "a1b2c3d4e5f6...",
"status": "ACCEPTED",
"dwt": {
"filed": true,
"reason": "A DEFRA receive was triggered. Poll /receiver/movements for its status and wasteTrackingId."
}
}A transfer note can only be resolved once — accepting or rejecting an already-processed one returns 409, which makes a retry after a lost response safe.
/receiver/inbound/{id}/rejectReject a transfer note. Nothing is filed with DEFRA — no waste was received at your site, so there is no receive movement to report.
| Field | Type | Required | Description |
|---|---|---|---|
| reason | string | Required | Why the load was rejected. Shown to the carrier and kept on the audit record. |
| receivedAt | string | Optional | ISO datetime. Defaults to now. |
| signatoryName | string | Optional | Defaults to your company name. |
/receiver/receiptsRecord a load that never came through WTNcloud — a paper note handed over at the gate. Creates the movement and files its DEFRA receive.
Hazardous loads go here too. If the EWC code is hazardous, supply hazardousDetails and the load is kept as a consignment note rather than a transfer note — the record an operator and an EA inspection look for, in your HWCN register, list and CSV. The response says which you got in kind. Which one it is is decided from the EWC catalogue server-side, never from a flag you send, so the record kept can never disagree with the code declared.
| Field | Type | Required | Description |
|---|---|---|---|
| carrierId | string | Required | The carrier that delivered. Must be one of your own carrier records. |
| ewcCode | string | Required | European Waste Catalogue code. |
| quantity | number | Required | Quantity received. |
| recoveryDisposalCode | string | Required | R/D code applied at your site. Required here — a receipt with no R/D can't be filed. |
| producerName | string | Optional | Producer named on the note (also producerAddress, producerPostcode). |
| wasteDescription | string | Optional | Description of the waste. |
| unit | string | Optional | Unit for quantity (e.g. tonnes). |
| dateOfTransfer | string | Optional | ISO datetime of the transfer. |
| vehicleReg | string | Optional | Vehicle that delivered (also meansOfTransport, physicalForm, containmentType, numberOfContainers). |
| containsPops | boolean | Optional | Whether the load was declared to contain POPs (with popsDetails). |
| hazardousDetails | object | Optional | REQUIRED when the EWC code is hazardous, and rejected when it isn't. Either consignmentCode (the code off the note that travelled with the load) or consignmentCodeMissingReason (NO_DOC_WITH_WASTE | HWRC_RECEIPT), never both; hpCodes (at least one of HP_1–HP_15 or HP_POP; loose spellings like "HP4" are normalised); sourceOfComponents (PROVIDED_WITH_WASTE | NOT_PROVIDED | GUIDANCE | OWN_TESTING); and components as [{ name, concentration }] in mg/kg — required for GUIDANCE or OWN_TESTING, and refused for NOT_PROVIDED. |
| specialHandlingRequirements | string | Optional | Handling instructions for waste with harmful characteristics — DEFRA's specialHandlingRequirements. |
| theirReference | string | Optional | The carrier's own note or ticket number, filed alongside ours as a DEFRA otherReferencesForMovement entry. |
wasteDescription is required for a hazardous load — a consignment note has to describe the waste.
An Idempotency-Key header is required: this creates a brand-new record with no natural unique key, so without one a retry after a lost response would file a second DEFRA receive for the same physical load.
Example Request
curl -X POST "https://wtncloud.co.uk/api/external/receiver/receipts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: gate-ticket-88213" \
-H "Content-Type: application/json" \
-d '{
"carrierId": "clx123carrier",
"ewcCode": "17 09 04",
"quantity": 8.4,
"unit": "tonnes",
"recoveryDisposalCode": "R13",
"producerName": "Riverside Joinery",
"vehicleReg": "AB12 CDE"
}'Response
{
"ok": true,
"wtnId": "b2c3d4e5f6a1...",
"wtnReference": "WTN-00187",
"kind": "wtn",
"dwt": {
"filed": true,
"reason": "A DEFRA receive was triggered. Poll /receiver/movements for its status and wasteTrackingId."
}
}Example — a hazardous load
curl -X POST "https://wtncloud.co.uk/api/external/receiver/receipts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: gate-ticket-88214" \
-H "Content-Type: application/json" \
-d '{
"carrierId": "clx123carrier",
"ewcCode": "13 02 05*",
"wasteDescription": "Used engine oil",
"quantity": 200,
"unit": "kg",
"recoveryDisposalCode": "D10",
"producerName": "Riverside Joinery",
"vehicleReg": "AB12 CDE",
"hazardousDetails": {
"consignmentCode": "ACMEWA/AB23F",
"hpCodes": ["HP_3", "HP_14"],
"sourceOfComponents": "OWN_TESTING",
"components": [{ "name": "Mineral oil", "concentration": 900 }]
}
}'{
"ok": true,
"kind": "hwcn",
"hwcnId": "b2c3d4e5f6a1...",
"consignmentCode": "ACMEWA/AB23F",
"dwt": {
"filed": true,
"reason": "A DEFRA receive was triggered. Poll /receiver/movements for its status and wasteTrackingId."
}
}A load that arrived with no note at all is logged with consignmentCodeMissingReason instead of a code; we store a placeholder that could not be mistaken for a real consignment code, and DEFRA is sent the reason rather than the placeholder. Logging the same consignment code twice returns 409 CONSIGNMENT_ALREADY_LOGGED with the id of the note you already have — one physical movement, one record.
Webhooks
ENTERPRISERather than polling, you can have us POST an event to your server as things happen. Add up to three endpoints under Settings → API in your dashboard, choose which events each one receives, and send yourself a test delivery before going live.
Endpoints must be https and publicly reachable. Redirects are not followed — point the webhook at its final URL.
Events
| Event | Fires when |
|---|---|
inbound.fired | A carrier sends a transfer note to your receiving site. It is now in your inbox awaiting an accept or reject. Transfer notes only — a consignment note is raised by the producer, not fired. |
movement.received | A consignee receipt was recorded — the load was accepted in full or in part. Fires for a transfer-note receipt AND for Part E on a hazardous consignment. |
movement.rejected | A consignee receipt was recorded as a rejection. Nothing is filed with DEFRA. Fires for both kinds. |
dwt.submitted | DEFRA accepted the receive. The payload carries the wasteTrackingId. |
dwt.failed | A DEFRA receive failed, or was blocked before sending because the data wouldn't pass validation. The payload carries the error. |
Events reach every party to the movement that has subscribed — so a carrier and a receiving site can each run their own integration against the same consignment.
Which kind of note an event is about. A hazardous event carries data.hwcnId and data.consignmentCode where a transfer note carries data.wtnId; the dwt.* events additionally carry data.kind of "wtn" or "hwcn". Every event also carries data.reference — the WTN number or the consignment code — so a consumer that only needs to identify the movement can read one field for both.
Payload
Every delivery has the same envelope. Route on event, dedupe on id — that value is constant across retries of the same event, so a redelivery is easy to spot.
{
"id": "0f2c9f4a-4f4e-4c1a-9f3e-7b2d9c4a1e55",
"event": "movement.received",
"createdAt": "2026-07-25T09:14:22.418Z",
"data": {
"wtnId": "a1b2c3d4e5f6...",
"reference": "WTN-00187",
"receivingSitePermit": "EPR/AB1234CD",
"receivedAt": "2026-07-25T09:14:00.000Z",
"acceptanceStatus": "ACCEPTED",
"acceptedQuantity": 12.1,
"recoveryDisposalCode": "R13",
"rejectionReason": null,
"signatoryName": "Acme Recycling Ltd",
"dwtFiling": true
}
}Headers
| Header | Value |
|---|---|
X-WTNCloud-Signature | Hex HMAC-SHA256 of `${timestamp}.${rawBody}`, keyed with your endpoint's signing secret. |
X-WTNCloud-Timestamp | Unix seconds. Reject anything more than 5 minutes old — it's inside the signed string, so it can't be altered. |
X-WTNCloud-Event | The event name, so you can route before parsing the body. |
X-WTNCloud-Delivery | The delivery id — the same value as `id` in the body. |
Verifying a delivery
Sign the raw request body, byte for byte. Parsing the JSON and re-serialising it produces a different string and the signature will not match.
import { createHmac, timingSafeEqual } from "crypto";
// `rawBody` must be the unparsed request body (e.g. express.raw()).
function verify(rawBody, headers, secret) {
const timestamp = headers["x-wtncloud-timestamp"];
const signature = headers["x-wtncloud-signature"];
if (!timestamp || !signature) return false;
// Replay guard: the timestamp is inside the signed string, so an attacker
// can't re-date a captured delivery without breaking the signature.
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(age) || age > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`, "utf8")
.digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(signature, "utf8");
return a.length === b.length && timingSafeEqual(a, b);
}Delivery and retries
- Reply 2xx to acknowledge. Anything else — including a redirect — counts as a failure. Reply first and do your processing afterwards; we allow 10 seconds.
- Retries — five attempts in total, spaced 1, 5, 15 and 60 minutes apart, so a delivery survives about 81 minutes of downtime. Each attempt carries a fresh timestamp and signature.
- Ordering isn't guaranteed. A retried
movement.receivedcan arrive after thedwt.submittedthat followed it. Treat each event as a fact about a point in time, and use the API to read current state. - Persistent failure switches an endpoint off. After 20 consecutive events that exhaust every retry, we deactivate it and email the account owner. Nothing is lost: accepted movements and DEFRA submissions stay readable through
/receiver/movements.
Error Codes
| Status | Meaning | What to do |
|---|---|---|
| 400 | Bad Request | Check your request body matches the schema above. The response includes validation details. |
| 400 | Bad Request | INVALID_EWC_CODE — /receiver/receipts: the code isn't in the European Waste Catalogue. It's refused rather than guessed at, because an unrecognised code can't be shown to be non-hazardous. |
| 400 | Bad Request | HAZARDOUS_DETAILS_REQUIRED / HAZARDOUS_DETAILS_NOT_APPLICABLE — /receiver/receipts: the hazardousDetails block has to match the EWC code. A hazardous code without it under-declares the load; a non-hazardous code with it files a contradiction. |
| 401 | Unauthorized | Your API key is missing or invalid. Check the Authorization header. |
| 402 | Payment Required | PAY_AS_YOU_GO accounts: insufficient balance for the requested WTNs. (Does not apply to /hwcn/create — drafts are free.) |
| 403 | Forbidden | Your account has been suspended. Contact [email protected]. |
| 403 | Forbidden | API_ACCESS_TIER_REQUIRED — the external API is included with the Enterprise plan. Upgrade in your dashboard billing settings, or contact [email protected]. |
| 403 | Forbidden | INSUFFICIENT_SCOPE — your key doesn't carry the scope this endpoint needs; the response names it. For receiver:read / receiver:write, turn the Receiver API on under Settings → API in your dashboard. read and carrier:write are on every key by default, so if one of those is missing it was revoked — contact [email protected]. |
| 403 | Forbidden | RECEIVER_ROLE_REQUIRED / RECEIVER_TIER_REQUIRED / RECEIVER_NOT_CONFIGURED — receiver endpoints need the receiver role, an Enterprise plan, and a receiving-site permit on your account. |
| 404 | Not Found | The record doesn't exist or isn't addressed to your site. PDF_NOT_READY means the document is still being generated — honour Retry-After and try again. NO_PDF_FOR_CAPTURE means the consignment was logged from a paper note, so no PDF was ever generated: the retained original in your dashboard is the record. |
| 409 | Conflict | The consignment was already accepted or rejected, or hasn't been fired to you yet. Part E: already signed, or the note is no longer awaiting a signature. IDEMPOTENCY_IN_PROGRESS means an identical request is still running — retry shortly. |
| 409 | Conflict | CONSIGNMENT_ALREADY_LOGGED / CONSIGNMENT_IN_DELETED_ITEMS — /receiver/receipts: you have already logged a consignment with this note code (the response names it), or a note with that code is in your deleted items and has to be restored or purged first. |
| 422 | Unprocessable | IDEMPOTENCY_KEY_REUSED — that Idempotency-Key was already used with a different request body. Use a fresh key for a new load. On Part E, a 422 means the stored consignment fails validation and can't be completed until the producer corrects it; the response lists the issues. |
| 429 | Too Many Requests | Slow down — the response carries a Retry-After header telling you how long to wait. |
| 504 | Gateway Timeout | PDF_FETCH_TIMEOUT — retrieving the document from storage took too long. Honour Retry-After and try again. |
| 500 | Server Error | Something went wrong on our end. Retry after a few seconds. If it persists, contact support. |
| 503 | Service Unavailable | /hwcn/create only — failed to allocate a unique consignment code after 5 attempts (vanishingly rare). The response includes any items that were created before the collision; retry with the rest. |
Rate Limits
API requests are rate limited to prevent abuse. Each request can include up to 100 WTNs in a single batch. If you need higher limits, contact us at [email protected].
Ready to integrate?
Sign up for an Enterprise plan to get your API key and start creating WTNs programmatically.