Developer API

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
POST/wtn/create

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

FieldTypeRequiredDescription
namestringRequiredCustomer name
addressstringRequiredCustomer address
emailstringOptionalCustomer email
phonestringOptionalCustomer phone number
driverEmailstringOptionalAssign to a driver by their email address
dateOfTransferstringOptionalISO date (e.g. 2026-04-15). Defaults to today
ewcCodestringOptionalEuropean Waste Catalogue code
wasteDescriptionstringOptionalDescription of the waste
quantitynumberOptionalAmount of waste
unitstringOptionalUnit of measurement (kg, tonnes, litres, etc.)
containmentTypestringOptionalHow the waste is contained (bags, skip, drums, etc.)
additionalWasteLinesarrayOptionalExtra 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.
saveCustomerbooleanOptionalSave 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.

GET/wtn/status

Check the status of your waste transfer notes. Query by WTN reference numbers or fetch all WTNs created since a given date.

Query Parameters

FieldTypeRequiredDescription
referencesstringOptionalComma-separated WTN references (e.g. WTN-00042,WTN-00043)
sincestringOptionalISO 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"
    }
  ]
}
Hazardous Waste

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/create lands rows as DRAFT with 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 theEXEMPT sentinel, 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.
POST/hwcn/create

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

FieldTypeRequiredDescription
country"england" | "wales"RequiredCountry the waste is being collected from. Drives statute citations and code format.
consignorobjectRequiredYour company. companyName / contactName / address / email / sicCode required; phone optional. Wales: walesPremisesKind ('registered' | 'exempt') and either a 6-char NRW premises code (AAAnnn) or 'EXEMPT'.
producerobjectOptionalOriginal waste producer if different from consignor. companyName / address required when present.
consigneeobjectRequiredReceiving facility. companyName / address / permitOrLicenceNumber required. Set operatesUnderExemption + exemptionNumber together if applicable.
multipleConsignmentsobjectOptionalSuccession of multiple notes. Set isFirstInSuccession=true plus consignmentCount, frequency, durationMonths.
wasteobjectRequiredEWC code (must be hazardous, ends in *), HP codes (HP1–HP15 or POP, ≥1), processGivingRiseToWaste, description, physicalForm, quantityKg, containerType, containerCount. Optional ADR sub-section.
carrierobjectRequiredRegistered 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.

GET/hwcn/status

Look up HWCNs by consignment code or fetch all HWCNs created since a given date. Soft-deleted rows are excluded.

Query Parameters

FieldTypeRequiredDescription
referencesstringOptionalComma-separated consignment codes (e.g. ACMEWA/AB23F,ABC123/0001A).
sincestringOptionalISO 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

ENTERPRISE

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

GET/receiver/inbound

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

FieldTypeRequiredDescription
statusstringOptionalpending (default), resolved, or all.
sincestringOptionalISO 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.
limitnumberOptionalPage 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.

GET/receiver/movements

Loads you have actually taken in — both notes fired to you and your own captures of paper or emailed notes — with their DEFRA tracking state.

FieldTypeRequiredDescription
referencesstringOptionalComma-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.
sincestringOptionalISO date — movements received on or after this date.
limitnumberOptionalPage 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).

GET/receiver/wtn/{id}/pdf

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

POST/receiver/hwcn/{id}/receiptHAZARDOUS

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

FieldTypeRequiredDescription
dateReceivedstringRequiredThe date the waste was received (YYYY-MM-DD), as a UK calendar date.
timeReceivedstringOptionalUK wall-clock time of receipt (HH:MM, 24-hour). DEFRA asks for the exact time; without it the filing can only report midnight.
quantityReceivedKgnumberRequiredWhat you actually took in, in kilograms — the figure filed with DEFRA.
quantityReceivedIsEstimatebooleanOptionalWhether 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"OptionalDefaults to accepted. A rejection files nothing with DEFRA — no waste was received at your site.
rdCodestringRequiredThe recovery or disposal operation applied at your site (e.g. D10).
rdCodesByLinearrayOptionalPer-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.
sitePermitOrExemptionstringOptionalYour permit or exemption, when it differs from the one printed on the note. Filed as DEFRA's receiver.authorisationNumber.
signatoryNamestringOptionalWho signed for the load. Defaults to your company name.
certificateConfirmedtrueRequiredExplicit 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.

GET/receiver/hwcn/{id}/pdfHAZARDOUS

Downloads 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

ENTERPRISE

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

POST/receiver/inbound/{id}/accept

Accept a transfer note fired to your site, in full or in part. The hazardous equivalent is Part E — see /receiver/hwcn/{id}/receipt.

FieldTypeRequiredDescription
recoveryDisposalCodestringOptionalR/D code applied at your site (e.g. R13). Omit to log the receipt WITHOUT filing a DEFRA receive.
acceptedQuantitynumberOptionalConfirmed (e.g. weighbridge) quantity. Required when partial is true.
partialbooleanOptionalRecord a partial acceptance rather than accepting the full stated load.
receivedAtstringOptionalISO datetime of receipt. Defaults to now.
signatoryNamestringOptionalWho 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.

POST/receiver/inbound/{id}/reject

Reject a transfer note. Nothing is filed with DEFRA — no waste was received at your site, so there is no receive movement to report.

FieldTypeRequiredDescription
reasonstringRequiredWhy the load was rejected. Shown to the carrier and kept on the audit record.
receivedAtstringOptionalISO datetime. Defaults to now.
signatoryNamestringOptionalDefaults to your company name.
POST/receiver/receipts

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

FieldTypeRequiredDescription
carrierIdstringRequiredThe carrier that delivered. Must be one of your own carrier records.
ewcCodestringRequiredEuropean Waste Catalogue code.
quantitynumberRequiredQuantity received.
recoveryDisposalCodestringRequiredR/D code applied at your site. Required here — a receipt with no R/D can't be filed.
producerNamestringOptionalProducer named on the note (also producerAddress, producerPostcode).
wasteDescriptionstringOptionalDescription of the waste.
unitstringOptionalUnit for quantity (e.g. tonnes).
dateOfTransferstringOptionalISO datetime of the transfer.
vehicleRegstringOptionalVehicle that delivered (also meansOfTransport, physicalForm, containmentType, numberOfContainers).
containsPopsbooleanOptionalWhether the load was declared to contain POPs (with popsDetails).
hazardousDetailsobjectOptionalREQUIRED 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.
specialHandlingRequirementsstringOptionalHandling instructions for waste with harmful characteristics — DEFRA's specialHandlingRequirements.
theirReferencestringOptionalThe 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

ENTERPRISE

Rather 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

EventFires when
inbound.firedA 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.receivedA 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.rejectedA consignee receipt was recorded as a rejection. Nothing is filed with DEFRA. Fires for both kinds.
dwt.submittedDEFRA accepted the receive. The payload carries the wasteTrackingId.
dwt.failedA 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

HeaderValue
X-WTNCloud-SignatureHex HMAC-SHA256 of `${timestamp}.${rawBody}`, keyed with your endpoint's signing secret.
X-WTNCloud-TimestampUnix seconds. Reject anything more than 5 minutes old — it's inside the signed string, so it can't be altered.
X-WTNCloud-EventThe event name, so you can route before parsing the body.
X-WTNCloud-DeliveryThe 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.received can arrive after the dwt.submitted that 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

StatusMeaningWhat to do
400Bad RequestCheck your request body matches the schema above. The response includes validation details.
400Bad RequestINVALID_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.
400Bad RequestHAZARDOUS_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.
401UnauthorizedYour API key is missing or invalid. Check the Authorization header.
402Payment RequiredPAY_AS_YOU_GO accounts: insufficient balance for the requested WTNs. (Does not apply to /hwcn/create — drafts are free.)
403ForbiddenYour account has been suspended. Contact [email protected].
403ForbiddenAPI_ACCESS_TIER_REQUIRED — the external API is included with the Enterprise plan. Upgrade in your dashboard billing settings, or contact [email protected].
403ForbiddenINSUFFICIENT_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].
403ForbiddenRECEIVER_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.
404Not FoundThe 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.
409ConflictThe 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.
409ConflictCONSIGNMENT_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.
422UnprocessableIDEMPOTENCY_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.
429Too Many RequestsSlow down — the response carries a Retry-After header telling you how long to wait.
504Gateway TimeoutPDF_FETCH_TIMEOUT — retrieving the document from storage took too long. Honour Retry-After and try again.
500Server ErrorSomething went wrong on our end. Retry after a few seconds. If it persists, contact support.
503Service 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.

Try WTNcloud free

20 free WTNs · No card required

Start Free