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 -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 -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"}}'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());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()){
"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": []
}
}
}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);
}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
| Option | How it works | Price | Notes |
|---|---|---|---|
| DPD and PostNL websites | Open the country's tracking page and refresh it. | Free | A page for a person, not an event to your server. |
| lastscanned watches | PUT /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 id | Name | Number |
|---|---|---|
| dpd | DPD | 14 digits starting with 0. A shared shape, so pass carrier=dpd unless detection already chose it. |
| dpdpl | DPD Poland | 13 digits and a letter. Pass carrier=dpdpl when detection does not choose it. |
| dpdpt | DPD Portugal | 14-digit labels shared with other DPD countries. Pass carrier=dpdpt. |
| dpdro | DPD Romania | Digits other carriers also use. Pass carrier=dpdro. |
| dpduk | DPD UK | 14 digits starting with 15, or the 10-digit parcel number with carrier=dpduk. |
| dpdza | DPD South Africa | Letters 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.