How do I get an alert when a package has had no scan in X days?
Watch the numbers with POST /v1/watches (up to 100 per call, 5,000 active per account). Each one is re-checked on its own schedule, and your webhook gets a signed tracking.updated event whenever the status, newest scan or estimate changes. Silence doesn't send an event, so add a short hourly job that lists your active watches with GET /v1/watches and flags any whose newest scan is older than X days. 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.
Try it
curl -X PUT https://api.lastscanned.com/v1/webhook \
-H "Authorization: Bearer lsk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/lastscanned"}'
# the response has your signing secret (whsec_...)
curl -X POST https://api.lastscanned.com/v1/watches \
-H "Authorization: Bearer lsk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"trackingNumbers": ["1Z000Y770414233027", "541270563758"], "metadata": {"order": "1042"}}'// node stale.mjs (run hourly from cron or any scheduler)
const DAYS = 3;
const cutoff = Date.now() - DAYS * 86400000;
const stale = [];
let cursor = null;
do {
const url = new URL("https://api.lastscanned.com/v1/watches");
url.searchParams.set("limit", "200");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.LASTSCANNED_KEY}` } });
const { watches, nextCursor } = await res.json();
for (const w of watches) {
const newest = w.result?.events?.[0]; // events are newest first
const lastScan = newest?.timestamp ?? newest?.localTime ?? null;
if (Date.parse(lastScan ?? w.createdAt) < cutoff) {
stale.push({ trackingNumber: w.trackingNumber, carrier: w.carrier, status: w.status, lastScan, metadata: w.metadata });
}
}
cursor = nextCursor;
} while (cursor);
console.log(stale); // send these to Slack, email or your helpdeskimport crypto from "node:crypto";
import express from "express";
const app = express();
app.post("/lastscanned", express.raw({ type: "application/json" }), (req, res) => {
const { t, v1 } = Object.fromEntries(req.get("Lastscanned-Signature").split(",").map((p) => p.split("=")));
const expected = crypto.createHmac("sha256", process.env.LASTSCANNED_WHSEC).update(`${t}.${req.body}`).digest("hex");
if (Math.abs(Date.now() / 1000 - Number(t)) > 300 || !crypto.timingSafeEqual(Buffer.from(v1, "hex"), Buffer.from(expected, "hex"))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body);
if (event.type === "tracking.updated" && event.data.result.status === "exception") {
console.log("exception", event.data.watch.trackingNumber, event.data.result.statusDescription, event.data.watch.metadata);
}
res.sendStatus(200); // answer 2xx within 10 seconds
});
app.listen(3000);Ways to catch a stalled package
| Option | How it works | Price | Notes |
|---|---|---|---|
| Carrier email alerts | Sign up per package on the carrier's site. | Free | Tell you when something happens, not when nothing does. |
| Poll it yourself | Re-track every open number on a schedule and compare the newest scan time. | On Pro, re-checks are free | Works, but you run the schedule for every number. |
| Watches + stale check | POST /v1/watches; webhook on change; hourly GET /v1/watches finds the silent ones. | Each number counts once; re-checks are free | We re-check out-for-delivery every 15 minutes, exceptions every 30, in-transit hourly. |
Details
Why a webhook alone can't say "nothing happened"
Webhooks fire on change. A package that stops moving produces no change, so the stale check compares each watch's newest scan time with now. Listing watches doesn't count as a lookup.
Choosing X
Two or three days in transit is a reasonable starting point for domestic parcels; cross-border shipments can go quiet longer between handoffs. Keep metadata such as the order id on the watch so the alert says which order to chase.
When watches end
A watch ends on delivered or returned, after 14 days with no carrier record (a watch.ended event with reason not_found), or after 90 days (expired). A number the carrier never shows a record for ends as not_found.
What it costs
Watching a number counts as one tracked number; every re-check after that is free. An already-active number sent again is a no-op.
FAQ
- Do you send an event when a package has no new scan?
- No. Events fire on changes. Run the stale check above against GET /v1/watches to find the silent ones.
- How often are watched numbers re-checked?
- Out for delivery every 15 minutes, exceptions every 30, in transit hourly, label created every 2 hours.
- How many packages can I watch?
- Up to 5,000 active watches per account, added up to 100 per call or pasted on the dashboard's Watches page.
- Can my existing 17TRACK webhook receiver handle these events?
- Yes. Set the webhook format to 17track and the same events go out in 17TRACK's v2 shape.
- What happens if my endpoint is down?
- Any 2xx within 10 seconds counts as delivered; otherwise we retry after 1, 5, 30, 120, 360 and 720 minutes, and you can resend from the dashboard.