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

1. Set your webhook URL, then watch numbers
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"}}'
2. Hourly stale check: no scan in 3 days
// 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 helpdesk
3. Webhook receiver: alert on exceptions as they happen
import 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

OptionHow it worksPriceNotes
Carrier email alertsSign up per package on the carrier's site.FreeTell you when something happens, not when nothing does.
Poll it yourselfRe-track every open number on a schedule and compare the newest scan time.On Pro, re-checks are freeWorks, but you run the schedule for every number.
Watches + stale checkPOST /v1/watches; webhook on change; hourly GET /v1/watches finds the silent ones.Each number counts once; re-checks are freeWe 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.