How do I get a webhook when a package's tracking status changes?

Set your URL once with PUT /v1/webhook and keep the signing secret it returns. Then watch numbers with POST /v1/watches (up to 100 per call, 5,000 active). Each watch is re-checked on its own schedule, and your endpoint gets a tracking.updated event, signed with HMAC-SHA256 in the Lastscanned-Signature header, whenever the status, newest scan or estimated delivery changes, then watch.ended when the package is delivered or returned. 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 the 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": ["1ZG2242W6822298280", "541270563758"], "metadata": {"order": "1042"}}'
2. Python receiver: verify the signature, then act
import hashlib, hmac, os, time
from flask import Flask, abort, request

app = Flask(__name__)

@app.post("/lastscanned")
def lastscanned():
    parts = dict(p.split("=", 1) for p in request.headers["Lastscanned-Signature"].split(","))
    body = request.get_data()
    expected = hmac.new(os.environ["LASTSCANNED_WHSEC"].encode(), parts["t"].encode() + b"." + body, hashlib.sha256).hexdigest()
    if abs(time.time() - int(parts["t"])) > 300 or not hmac.compare_digest(expected, parts["v1"]):
        abort(401)
    event = request.get_json()
    watch = event["data"]["watch"]
    if event["type"] == "tracking.updated":
        result = event["data"]["result"]
        print(watch["trackingNumber"], result["status"], result["statusDescription"], watch["metadata"])
    elif event["type"] == "watch.ended":
        print("done", watch["trackingNumber"])
    return "", 200  # answer 2xx within 10 seconds

Ways to hear about tracking changes

OptionHow it worksPriceNotes
Poll on a scheduleRe-track every open number and compare with what you stored.On Pro re-checks are freeYou run the schedule and the diff.
Carrier email and text alertsSign up per package on the carrier's site.FreeMessages to a person, not events your code can act on.
Watches + webhookPUT /v1/webhook once, POST /v1/watches, receive signed events.Each number counts once; re-checks are freeOut for delivery re-checked every 15 minutes, exceptions every 30, in transit hourly.

Details

What sends an event

A change in status, newest scan or estimated delivery sends tracking.updated with the full result. A watch ends on delivered or returned, after 14 days with no carrier record (not_found), or after 90 days (expired), and sends watch.ended.

Retries

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.

Moving from 17TRACK

Set the webhook format to 17track and the same events go out in 17TRACK's v2 shape, so an existing receiver keeps working.

Silence is not an event

A package that stops moving sends nothing. Pair the webhook with the short stale check in the no-scan alerts guide.

FAQ

How are tracking webhooks signed?
With HMAC-SHA256 over the timestamp and the raw body, sent as t= and v1= in the Lastscanned-Signature header. Reject anything older than 5 minutes.
How many numbers can I watch?
Up to 5,000 active watches per account, added up to 100 per call or pasted on the Watches page at app.lastscanned.com/watches.
Do webhooks work for every carrier?
Yes, for all 549 carriers, in the same event shape.
What does a webhook cost?
Watching a number counts as one tracked number. Every re-check and event after that is free.