How do I get a webhook for DPD and PostNL tracking?

POST /v1/watches re-checks DPD and PostNL on a schedule and POSTs a signed event when status, the newest scan or the estimate changes. These are scheduled checks, not a push from the carrier. Lastscanned-Signature is HMAC-SHA256 of the timestamp and raw body. Pro is $4.99 a month for 1,000 unique numbers, then $0.015, and re-checks are free.

Try it

curl: watch a PostNL barcode (3S… detects on its own)
curl -X POST https://api.lastscanned.com/v1/watches \
  -H "Authorization: Bearer lsk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"trackingNumber": "3STBDG061270842", "metadata": {"order": "1042"}}'
curl: watch a DPD UK number (pass the country code)
curl -X POST https://api.lastscanned.com/v1/watches \
  -H "Authorization: Bearer lsk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"trackingNumber": "15504366056765", "carrier": "dpduk", "metadata": {"order": "1042"}}'
JavaScript fetch
const res = await fetch("https://api.lastscanned.com/v1/watches", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.LASTSCANNED_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ trackingNumber: "15504366056765", carrier: "dpduk", metadata: { order: "1042" } }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(res.status, await res.json());
Python requests
import os
import requests

r = requests.post(
    "https://api.lastscanned.com/v1/watches",
    headers={"Authorization": f"Bearer {os.environ['LASTSCANNED_KEY']}"},
    json={"trackingNumber": "3STBDG061270842", "metadata": {"order": "1042"}},
    timeout=60,
)
r.raise_for_status()
print(r.status_code, r.json())
Event your URL receives
{
  "id": "evt_63e401d5f73a4c31ef8fa8d9",
  "type": "tracking.updated",
  "createdAt": "2026-11-14T18:20:36Z",
  "data": {
    "watch": {
      "id": "wat_109060ade9f9a48732117f7a",
      "trackingNumber": "3STBDG061270842",
      "carrier": "postnl",
      "metadata": { "order": "1042" }
    },
    "previousStatus": "in_transit",
    "result": {
      "carrier": "postnl",
      "trackingNumber": "3STBDG061270842",
      "status": "out_for_delivery",
      "statusDescription": "<newest observation>",
      "service": null,
      "deliveredAt": null,
      "deliveredAtLocal": null,
      "signedBy": null,
      "destination": { "city": null, "state": null, "country": null },
      "events": []
    }
  }
}
Verify Lastscanned-Signature (Node)
import { createHmac, timingSafeEqual } from "node:crypto";

// Lastscanned-Signature: t=<unix seconds>,v1=<hex>
// v1 is HMAC-SHA256 of "<t>.<raw body>", keyed with the secret from PUT /v1/webhook.
export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
  const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(parts.v1 ?? "", "hex");
  return fresh && a.length === b.length && timingSafeEqual(a, b);
}
Verify Lastscanned-Signature (Python)
import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return abs(time.time() - int(parts["t"])) < tolerance and hmac.compare_digest(parts["v1"], expected)

Ways to hear about a DPD or PostNL parcel

OptionHow it worksPriceNotes
DPD and PostNL websitesOpen the country's tracking page and refresh it.FreeA page for a person, not an event to your server.
lastscanned watchesPUT /v1/webhook once, then POST /v1/watches with the number and, when the shape is shared, the DPD country code.The number counts once. Re-checks are free on Pro ($4.99/mo for 1,000, then $0.015).Scheduled checks. The carrier does not push.

DPD country codes

Carrier idNameNumber
dpdDPD14 digits starting with 0. A shared shape, so pass carrier=dpd unless detection already chose it.
dpdplDPD Poland13 digits and a letter. Pass carrier=dpdpl when detection does not choose it.
dpdptDPD Portugal14-digit labels shared with other DPD countries. Pass carrier=dpdpt.
dpdroDPD RomaniaDigits other carriers also use. Pass carrier=dpdro.
dpdukDPD UK14 digits starting with 15, or the 10-digit parcel number with carrier=dpduk.
dpdzaDPD South AfricaLetters and digits. Pass carrier=dpdza.

These are the DPD carriers in the live carrier list. PostNL is a separate carrier, postnl.

Details

Scheduled checks, not a carrier push

Nothing here is a webhook from DPD or PostNL. POST /v1/watches looks the number up immediately and returns the watch with its current result (201 when it is new, 200 when that number is already watched). Later checks run on a schedule: out for delivery every 15 minutes, exception every 30 minutes, in transit every hour, label created every 2 hours. Ocean containers use a slower schedule, every 6 hours until they are out for delivery. A change of status, newest scan or estimated delivery is POSTed to the URL you set with PUT /v1/webhook. Set that URL before you rely on delivery. The full schedule and the create, receive and verify flow are on the package tracking webhooks page.

Which DPD country code to pass

The live carrier list has dpd (DPD), dpdpl (DPD Poland), dpdpt (DPD Portugal), dpdro (DPD Romania), dpduk (DPD UK), dpdza (DPD South Africa). One watch request takes one carrier value, so a mixed list of countries is one request per code. PostNL barcodes (3S, four letters, then 7 to 9 digits) and PostNL S10 numbers ending NL detect without a carrier. 3STBDG061270842 is one recorded PostNL number. Recorded DPD examples: 01505533685981 (dpd), 15504366056765 (dpduk), 13649547289291 (dpdpl), 81353773180 (dpdro), 08448880307396 (dpdpt), WC5120157 (dpdza). The Germany and Group adapter can return a message such as "Process to DPD Belgium" and no events. That does not add a separate country code.

Signature

Every POST carries Lastscanned-Signature: t=<unix seconds>,v1=<hex>. v1 is HMAC-SHA256 of the string "<t>.<raw body>" using the secret from PUT /v1/webhook. Compare against the raw body before you parse JSON, and reject a t older than five minutes. The header Lastscanned-Event-Id repeats the event id.

Price

Watching a number counts as one tracked number when the carrier answers. Every later check is free. Pro is $4.99/mo for 1,000 unique numbers, then $0.015 each, with no daily limit. A free key tracks 5 numbers a day. With no key you get 1 lookup a day, then pay per request over MPP: $0.50 by card for a 25-number pack, or $0.02 a number in USDC on Tempo or Solana.

FAQ

Is a DPD or PostNL watch a webhook from the carrier?
No. lastscanned re-checks the number on a schedule and POSTs when the status, newest scan or estimate changes. DPD and PostNL do not push these events.
Which DPD country codes exist?
dpd (DPD), dpdpl (DPD Poland), dpdpt (DPD Portugal), dpdro (DPD Romania), dpduk (DPD UK), dpdza (DPD South Africa).
How is the webhook signed?
HMAC-SHA256 over the unix timestamp, a dot, and the raw body, in the Lastscanned-Signature header as t= and v1=.
How many numbers can I watch?
Up to 100 numbers per POST /v1/watches and 5,000 active watches on the account. A DPD request should name one country code, because the request has one carrier field.