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 filed under one of your saved customers — the one you name with customerId, or one matched from the name and address you send, saved for you if nothing matches (see “Matching notes to customers” below). 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 as one line, ending in the postcode (e.g. 12 High Street, Bristol, BS1 2AB). The postcode is what matching and a newly saved customer take it from.
customerIdstringOptionalFile the note under this saved customer instead of matching one: the customerId a previous note came back with. Must be one of your customers, or the whole batch is refused with 400 CUSTOMER_NOT_FOUND. name and address are still required — they are what this note records. See “Matching notes to customers” below.
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.). Omit it and the note records kg — fine on a draft, where the driver sets the real figure on site. Required on a note you sign through the API, and there it must be one we can weigh: tonnes, kg, litres or m3. Common spellings (tonne, t, kilograms, litre, l, cubic metres) are accepted and stored as the canonical value. See “Signatures” below.
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.
saveCustomerbooleanOptionalWhen the note is filed under a saved customer, update that customer’s email and phone from this item. Their name and address are never changed. A customer that matches nothing is saved either way.
transferorSignaturestringOptionalThe producer's signature as a PNG data URL (data:image/png;base64,…), max 1,500,000 decoded bytes — captured in your own software. (Decimal, not 1,572,864: size your export against 1,500,000 and you are safe either way.) The only signature image the API accepts; see “Signatures” below. Requires transferorSignerName, and requires the note to describe its waste (ewcCode, wasteDescription, quantity above zero, unit, containmentType).
transferorSignerNamestringOptionalWho signed for the producer. Required with transferorSignature, and rejected without it.
transferorSignerCompanystringOptionalThe producer's company, when the signatory signs for one.
applyCarrierSignaturebooleanOptionalFill Section C from your account's saved carrier signature. Defaults to true when transferorSignature is supplied, false otherwise. Send false to leave Section C for the driver to sign at collection; send true on its own to have WTNcloud email the producer a signing link.

Matching notes to customers

Every note is filed under one of your saved customers, so a customer’s notes stay together in the dashboard, the customer portal and your reports. For each item, the first of these that finds a customer wins:

  1. customerId — the saved customer with that id. An id that isn’t one of your customers refuses the whole batch with 400 CUSTOMER_NOT_FOUND (listing the ids in customerIds) and creates nothing.
  2. Name and address. The name ignoring case and punctuation (Acme Ltd and ACME-LTD. are one name); the address ignoring case, spacing and punctuation, with or without the postcode on the end. So 12 High Street, Bristol, BS1 2AB finds a customer saved in the dashboard as 12 High Street, Bristol with the postcode BS1 2AB.
  3. Name and postcode, when your address line has a postcode in it. The same customer with the street spelled differently (High St) is still the same customer.
  4. Email, compared ignoring case.
  5. Nothing matched — a new customer is saved from the item: the postcode found at the end of the address line as their postcode, and the rest of the line as their address, the way a customer added in the dashboard is saved.

The API never changes a saved customer’s name or address. When a note is filed by customerId or by name and postcode, and the name or address you sent differs from the saved one, the note records what you sent — it prints on that note’s PDF and the driver is sent there — and the saved customer stays as it was. A note matched by email alone prints the saved customer’s details.

Every item in the response carries the customerId it was filed under. Keep it against the customer in your own system and send it with their next note: matching then has nothing to guess, and a change of address on your side can’t save them a second time.

Name and postcode are treated as one customer’s identity, the same rule the dashboard uses when it offers to save a one-off customer. Two different customers with the same name at the same postcode are rare, but if you have them, send customerId to keep them apart.

Signatures

You only ever send the producer’s signature. The carrier half of a note your account raises is your own signature, which WTNcloud already holds — the one you saved under Settings → Notes, and the one the dashboard’s “Apply My Signature” button uses. It is resolved server-side from your account, so the API neither needs nor accepts a carrier signature image: nobody can upload one on your behalf.

What an item carries decides where the note lands:

  • Neither field — DRAFT or ASSIGNED, unsigned, for the driver to complete on site. This is the original behaviour and is unchanged.
  • transferorSignature — both halves signed, COMPLETE. The PDF is rendered and the customer is sent their copy if you have auto-send on.
  • applyCarrierSignature: true on its own — carrier-signed and AWAITING_TRANSFEROR_SIGNATURE; WTNcloud emails or texts the producer a signing link, and the note completes when they sign it.

Every signature is written to the note’s immutable audit trail with how it was captured: the image you push is recorded as API (drawn in your software, attested by your key), your carrier signature as SAVED_SIGNATURE.

Two guards worth knowing before you wire this up. A producer signature over a shell — no EWC code, no description, zero quantity, no unit — is refused with a 400: the unsigned path exists for shells, and a signed note has to say what was transferred. unit is in that list for a reason. Everywhere else an omitted unit falls back to kilograms and the driver corrects it on site; on a note the producer has already signed there is no on-site correction, so a bare 4 meant as tonnes would stand as 4 kg on a duty-of-care record. For the same reason a signed note’s unit has to be one we can weigh — tonnes, kg, litres or m3, common spellings accepted — because an unrecognised one is treated as kilograms downstream, which is the same mistake in a different hat. And asking for a carrier signature your account has never saved refuses the whole batch with CARRIER_SIGNATURE_NOT_SAVED rather than creating half-signed notes.

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",
        "transferorSignature": "data:image/png;base64,iVBORw0KGgo...",
        "transferorSignerName": "Jane Doe",
        "transferorSignerCompany": "Acme Construction Ltd",
        "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": "COMPLETE",
      "signatures": { "transferor": true, "carrier": true },
      "pdf": "https://wtncloud.co.uk/api/external/wtn/a1b2c3d4e5f6/pdf",
      "publicPdf": "https://wtncloud.co.uk/s/Ab3dEf7h",
      "customerId": "cmtyitafh0003c5pbq5nhn900"
    }
  ]
}

On an unsigned item, status is "ASSIGNED" if a matching active driver was found, or "DRAFT" if no driver was specified or matched. Send signatures and it is "COMPLETE", "AWAITING_TRANSFEROR_SIGNATURE" or "AWAITING_TRANSFEREE_SIGNATURE" instead — the signatures object says which halves landed.

signatures, pdf, publicPdf and customerId are on every item, always — on an unsigned draft as much as a finished note, and on an idempotent replay of a batch created before any of them existed. You do not need to code defensively around their absence. driverEmail is the one field that comes and goes: it is echoed back only when you sent one.

pdf is the note’s permanent address, not a promise that a document is waiting there. The PDF is rendered once the note is signed, so the link answers 404 PDF_NOT_READY for a few seconds after a signed create, and for as long as an unsigned draft stays unsigned. See GET /wtn/{id}/pdf below.

publicPdf is the link to give your customer. It needs no API key and opens in any browser — it is the same short link WTNcloud texts a customer when a note completes. Until the PDF exists it shows a “not ready yet” page, and once the note is signed the same link opens the document, so you can send it straight after the create. Treat it like the document itself: anyone who has the link can read the note. It stops working while the note is deleted, and works again if the note is restored.

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. Soft-deleted notes are excluded, the same as /hwcn/status: a note in your deleted items is not a record that stands, and its pdf link would not resolve.

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,
      "pdf": "https://wtncloud.co.uk/api/external/wtn/a1b2c3d4e5f6/pdf",
      "publicPdf": "https://wtncloud.co.uk/s/Ab3dEf7h",
      "createdAt": "2026-04-15T10:30:00.000Z",
      "updatedAt": "2026-04-15T14:22:00.000Z"
    }
  ]
}

pdfUrl is a boolean, despite the name — whether a document has been rendered yet. The link is pdf, and it is the same address whether or not the render has happened. Poll pdfUrl and fetch when it turns true, or just fetch and honour Retry-After on a 404 PDF_NOT_READY. publicPdf is the no-key link to the same document, as on POST /wtn/create; a note created before public links existed is given one the first time you look it up here.

GET/wtn/{id}/pdf

Downloads the PDF of one of your own transfer notes — the document itself (application/pdf), streamed back through the API. Every download re-checks the key asking for it, so this is for your own systems; to give someone the document without your key, hand them publicPdf instead. Takes the wtnId from a create or status response, and needs only the read scope every key already carries.

curl "https://wtncloud.co.uk/api/external/wtn/a1b2c3d4e5f6/pdf" \
  -H "Authorization: Bearer YOUR_API_KEY" -o WTN-00042.pdf

A PDF is rendered once a note is signed, so a poll that beats the render gets 404 PDF_NOT_READY with a Retry-After header — retry rather than treating it as missing. An unsigned draft answers the same way until someone signs it. A load you logged from a paper note answers 404 NO_PDF_FOR_CAPTURE: nothing rendered one and nothing will, and the retained original is the record. A note belonging to another account is a plain 404, the same answer an id that doesn’t exist gets.

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 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. A batch holding one is refused whole with 400 COUNTRY_NOT_SUPPORTED and nothing in it is created: remove the items the response lists and resend.
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.
customerIdstringOptionalLink the consignment to this saved customer instead of matching one from the consignor: the customerId an earlier consignment or note came back with. Must be one of your customers, or the whole batch is refused with 400 CUSTOMER_NOT_FOUND. The consignor block is still what the consignment records.

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,
      "pdf": "https://wtncloud.co.uk/api/external/hwcn/a1b2c3d4e5f6/pdf",
      "publicPdf": "https://wtncloud.co.uk/s/Hw4Cn5Xy"
    }
  ]
}

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 the saved customer the consignment is linked to, so it shows in that customer’s history and portal. It is the one you named with customerId, or else one matched from the consignor the way /wtn/create matches a note (“Matching notes to customers” above): the company name with the address first, then the name with the postcode, then the email. The address is read as line 1, line 2, city and postcode, and finds a customer saved with or without the postcode, or saved with line 1 alone. It is null when nothing matches: this endpoint never saves a customer, and never changes one. The consignor block you send is what the consignment records, whichever customer it is linked to. Once a consignment or a note comes back with a customerId, send it with that consignor’s next consignment, and there is nothing to match.

pdf is where the consignment note will be downloadable from. Everything created here is a DRAFT and an HWCN is only rendered when Part D is signed, so expect 404 PDF_NOT_READY from it until that happens — see GET /hwcn/{id}/pdf below.

publicPdf is a link to the same document that needs no API key — the one to send the consignor, or anyone else who should have a copy. It shows a “not ready yet” page while the note is a draft and opens the PDF once Part D is signed, without the link changing. Anyone who has it can read the note; it stops working while the HWCN is deleted, and works again if it is restored.

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,
      "pdf": "https://wtncloud.co.uk/api/external/hwcn/a1b2c3d4e5f6/pdf",
      "publicPdf": "https://wtncloud.co.uk/s/Hw4Cn5Xy",
      "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, and pdf is where to fetch it with your API key. publicPdf is the no-key link to the same document, as on POST /hwcn/create; an HWCN created before public links existed is given one the first time you look it up here.

GET/hwcn/{id}/pdf

The hazardous twin of /wtn/{id}/pdf: downloads the consignment note for one of your own HWCNs, using the hwcnId from a create or status response. read scope; the file comes back as application/pdf.

curl "https://wtncloud.co.uk/api/external/hwcn/a1b2c3d4e5f6/pdf" \
  -H "Authorization: Bearer YOUR_API_KEY" -o consignment.pdf

An HWCN is rendered when Part D is signed, so a note still sitting as a DRAFT answers 404 PDF_NOT_READY — which is the normal state of anything POST /hwcn/create has just made. Poll /hwcn/status for status: "COMPLETED" rather than hammering this endpoint.

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,
      "additionalWasteLines": [],
      "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
    }
  ]
}

A transfer note carrying several waste lines lists the ones after the first (which is waste) in additionalWasteLines, each with its ewcCode, description, declared quantity and unit, and the acceptedQuantity you recorded for that line once it is resolved (0 on a rejection, null while pending). The same list comes back on /receiver/movements.

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,
      "additionalWasteLines": [],
      "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 load you logged yourself from a paper note answers 404 NO_PDF_FOR_CAPTURE instead: nothing generated a PDF and nothing will, so stop polling. 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.
quantityReceivedKgnumberOptionalWhat you took in of the first waste line, in kilograms. Required unless you send receivedLines, which supplies it.
receivedLinesarrayOptionalPart E per EWC code, as the statutory form asks it: one { ewcCode, quantityReceivedKg, accepted } per waste line, line 1 first, in the consignment's order; each code's accepted is accepted or rejected, never partially_accepted (the guidance has no partial acceptance of one waste). When sent, accepted and quantityReceivedKg are derived from it, each accepted code is filed with DEFRA at its own weight, and a rejected code isn't filed. Codes that no longer match the consignment's lines are refused 409 LINES_CHANGED.
vehicleRegistrationstringOptionalThe vehicle that delivered the load (or the mode of transport, if not road). Filed as the carrier's vehicle in place of the plate on Part C.
rejectionDetailsstringOptionalWhere waste is rejected: what was turned away and why. Printed in Part E. Required when receivedLines turns any code away.
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.
rdCodestringOptionalThe recovery or disposal operation applied at your site (e.g. D10). Required unless every code is rejected: waste you turn away gets no operation at your site, so a rejected code has no R/D code.
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. A code rejected in receivedLines keeps no R/D code, whatever is sent for it.
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",
    "quantityReceivedIsEstimate": false,
    "rdCode": "D10",
    "receivedLines": [
      { "ewcCode": "13 02 05*", "quantityReceivedKg": 195, "accepted": "accepted" },
      { "ewcCode": "16 01 07*", "quantityReceivedKg": 40, "accepted": "rejected" }
    ],
    "vehicleRegistration": "AB12 CDE",
    "rejectionDetails": "Oil filters in a split drum.",
    "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.
vehicleRegistrationstringOptionalThe plate of the lorry that arrived, when it isn't the one on the note. Recorded on your receipt and named on the DEFRA receive; the carrier's note is left as written. Omit when the lorry was the note's.
addedLinesarrayOptionalWastes you received that the note doesn't list, e.g. a load written as general that came in as general and timber: [{ ewcCode, wasteDescription, quantity, unit }], unit one of tonnes, kg, litres or m3. Send a figure for every line the note has beside them (lineQuantities; on a one-line note acceptedQuantity alone will do). Each is filed as its own waste item and counted; the carrier's note is left as written, and the status is the load's as a whole.
acceptedQuantitynumberOptionalConfirmed (e.g. weighbridge) quantity, in the note's unit. Required when partial is true, unless lineQuantities is sent. On a note with several waste lines it is the FIRST line's alone: send lineQuantities for the rest.
lineQuantitiesarrayOptionalWhat you accepted of EACH waste line, first line first, each a number in that line's own unit (the note's unit and additionalWasteLines[].unit in /receiver/inbound). How a transfer note carrying several waste lines is received: every line is recorded and filed with DEFRA at its own figure, and the status follows the lines (PARTIAL when any line is below what it declared). Send one number per line, 0 for a line you took none of. Without it, acceptedQuantity is the first line's and every other line is received as declared.
lineKeysarrayOptionalEvery waste line of the note as you read it, first line first: one { ewcCode, unit } per line, exactly as /receiver/inbound listed them (waste, then each of additionalWasteLines; other fields on an entry are ignored). Name every line, whether you send lineQuantities or a single acceptedQuantity (on a note with several lines that figure is the first line's and the others are received as declared, so you are confirming those lines too): one key for a note with two lines says you read a one-line note, and is refused. If the carrier has changed the note's lines since you read them (a line's EWC code or unit, their order, or how many there are), the request is refused with 409 LINES_CHANGED and nothing is recorded, rather than a figure being filed with DEFRA against the wrong waste. Without it, each figure is taken against the line at its position when the receipt is recorded.
partialbooleanOptionalRecord a partial acceptance rather than accepting the full stated load. With lineQuantities the lines decide it.
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. A note listing several waste lines (its additionalWasteLines in /receiver/inbound) is one receipt with a figure per line: send "lineQuantities": [6, 1.5] for a 6 t + 2 t load of which 1.5 t of the second line was taken. A weighbridge weighs the lorry as a whole, so split its net across the lines rather than sending it as the first line's figure. Send every line you read beside the figures as lineKeys, and a note the carrier has edited since is refused with 409 LINES_CHANGED instead of your figures landing on different waste: read it again from /receiver/inbound and resend.

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). Omit it and the receipt is filed in TONNES — note that this differs from /wtn/create, where an omitted unit means kilograms. Send it explicitly: this endpoint files with DEFRA immediately, so there is no draft stage in which a wrong unit gets corrected.
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.
additionalWasteLinesarrayOptionalExtra waste streams on the same transfer note — the fields above are line 1. Omit (or send null) when one stream arrived. Per entry: ewcCode, wasteDescription, quantity, unit and containmentType are required; containmentOther, physicalForm, numberOfContainers (default 1) and containsPops (default false) are optional. Each line may also carry its OWN full details, all optional and falling back to the load's: recoveryDisposalCode (what you did with that stream — DEFRA takes it per waste item, so a load you part-recover and part-dispose is one receipt), hazardousDetails ({hpCodes, sourceOfComponents, components} — stream-level, with no consignment code, which belongs to the note) and popsDetails ({sourceOfComponents, pollutants}). Max 50 extra lines, each filed as its own DEFRA wasteItem and counted separately against your permit. Every line must be the same kind of waste as the first: a transfer note lists non-hazardous codes, a consignment note hazardous ones, and neither carries the other's. A line whose class differs is refused with WASTE_LINE_CLASS_MISMATCH — send that waste as a receipt of its own.

wasteDescription is required for a hazardous load — a consignment note has to describe the waste.

A delivery carrying several EWC codes of the same kind is one receipt with several lines — put them in additionalWasteLines rather than sending a receipt each. That holds for both kinds: a transfer note may list several non-hazardous wastes, and a consignment note several hazardous ones. A delivery carrying both is two receipts, because the two move under different documents — send the hazardous part on its own and it comes back as kind: “hwcn”.

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

On a transfer note of several waste lines, acceptedQuantity is the first line's and additionalLines lists what was accepted of each of the others, each with its ewcCode, unit and acceptedQuantity. It is absent when the receipt took one figure for the load, and then every other line was received as declared. A line the site added at receipt, a waste the note doesn't list, carries added: true, a quantity of 0 and what was received of it in acceptedQuantity.

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 RequestWASTE_LINE_CLASS_MISMATCH — /receiver/receipts: a line in additionalWasteLines is a different kind of waste from the first (one hazardous, one not). A transfer note and a consignment note are different documents and neither carries the other's waste — send that line as a receipt of its own.
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.
400Bad RequestCUSTOMER_NOT_FOUND — /wtn/create and /hwcn/create: a customerId isn't one of your customers; the response lists them in customerIds. Nothing in the batch was created. Send the customerId a previous note or consignment came back with, or leave it out to match on name and address.
400Bad RequestCOUNTRY_NOT_SUPPORTED — /hwcn/create: an item's country is scotland or northern_ireland, whose consignment codes the regulator issues. Nothing from the batch was created. items lists each one as { index, country }, index counting from 0 in hwcns; enter those through the dashboard and resend the rest.
401UnauthorizedYour API key is missing or invalid. Check the Authorization header.
402Payment RequiredFREE accounts: the free WTN allowance is used up — the account needs a subscription to create more. (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, isn't yours, or isn't addressed to your site — all three answer the same way, so a key can't probe for other accounts' ids. PDF_NOT_READY means the document hasn't been rendered yet (a PDF follows the signatures, so an unsigned note keeps answering this) — honour Retry-After and try again. NO_PDF_FOR_CAPTURE means the load 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.
409ConflictLINES_CHANGED — /receiver/inbound/{id}/accept: the note's waste lines are no longer the lineKeys you sent (the carrier changed a line's EWC code or unit, their order, or how many there are, after you read it). Nothing was recorded. Read the note again from /receiver/inbound and resend your figures for its lines as they are now.
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.
502Bad GatewayPDF_FETCH_FAILED — the document exists but couldn't be retrieved from storage. Retry; if it persists, contact support.
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. The PDF download endpoints get their own budget of 60 requests a minute, separate from the JSON reads, because each one streams a file. 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

Every feature · 20 free notes · No card required

Start Free