Field Report // NO. 023

Shipment tracking from forwarded emails

28 August 2026· 6 min read· Case № 023

Carrier tracking pages work for humans and fight servers. I forward shipping emails to my agent; one silent hourly cron polls a registry until a parcel is delivered, pings me once, and files the pickup task. The reproducible pattern, including the carriers that cannot be polled and the nudge that covers them.

On the third of August I forwarded one shipping email to my agent. It parsed the parcel into a registry, checked it hourly, and asked me for nothing. Since then, parcels enter my life as a forwarded email and leave as one message: something needs collecting. When nothing needs me, the system says nothing at all. That is the entire user interface.

This is how the pipeline works, the one design rule that keeps it quiet, and the honest list of carriers a server cannot watch at all.

The real problem

Parcel tracking is free for humans and hostile to machines. A carrier’s tracking page renders fine in a browser, but from a datacenter IP the same pages fight back. Royal Mail returns 403 because it sits behind Akamai, which blocks server IPs outright. Amazon returns 503 from a bot wall. The aggregator sites (17track, AfterShip, ParcelsApp, track123) are single-page apps with no data in their HTML, and their APIs want keys. TrackingMore, the one aggregator with a clean JSON API, has a catch worth knowing: its free tier does not include an API key at all. I checked the signup page myself before building on it. The key ships with the paid plan.

So the wall in one sentence: every parcel tells you where it is, but almost nothing will tell a server.

What was built

The system has four parts and one design decision holding them together. I nearly created a cron job per order, each pinned to its delivery estimate. That fails twice: estimates slip, so a pinned job stops checking exactly when things get interesting, and every order is another job to create and clean up. The rule the whole thing hangs on: one cron watches a list, and the list is the only thing that changes.

The registry. One JSON file holds every live parcel:

{
  "id": "c2118d30",
  "item": "Angling Direct PLC: Shipment for #5210…",
  "merchant": "Angling Direct PLC",
  "carrier": "royalmail",
  "tracking_number": "KJ8044...",
  "tracking_url": "https://www.royalmail.com/portal/rm/track?trackNumber=...",
  "estimated_delivery": "4-5 August",
  "added_at": "2026-08-03T16:17:12"
}

Ingestion. I forward the merchant’s dispatch email to an address my assistant reads; a sender allowlist keeps everything else out. A small parser finds the tracking number by carrier pattern, follows wrapped redirect links to the real tracking URL, and appends the entry. Duplicates are dropped, so forwarding twice is harmless.

The poller. A single Python script reads the registry each run. Carriers it can reach (DPD, Evri, Yodel, InPost all serve their tracking pages to my server) get a status check. Carriers that block the server (Royal Mail, Amazon) get something better than a quiet failure: one nudge on the estimated delivery day, with the link, then silence. A delivered parcel is removed from the registry, a Telegram ping goes out, and a TickTick task appears: “Pick up delivery from {merchant}”, due that day.

The cron. One job, hourly through the waking day:

hermes cron create "0 9-22 * * *" \
  --name shipment-tracker \
  --script shipment-poller.sh \
  --no-agent

The --no-agent flag is the other half of the design: the script is the whole job, no model call, no tokens, and empty output means nothing is sent. It has run 325 times in under a month and almost every run printed nothing, which is the product working.

The reveal

The first parcel through the system was Royal Mail, the carrier my server cannot poll. On its estimated delivery day the cron nudged me once with the link, I checked, and the parcel was with the concierge. I picked it up, said so in chat, and the registry entry was gone. That path carried the real first delivery.

The delivered-alert branch has a quieter proof: hundreds of hourly runs, silent while parcels were in transit, and a nudge that fired on exactly the right day. The full ceremony, delivery ping plus filed pickup task, I have tested against a fake registry entry rather than watched live, because my parcels keep arriving via Royal Mail. The first real DPD or Evri delivery will be its debut.

The same chat caught the failure mode that matters. I once mentioned collecting a parcel from a shop that had never been tracked; the answer was that no shipping email was ever forwarded, so it was never tracked. The pipeline only watches what it is given, and it never pretends to know where something is.

The reproducible pattern

The flow is a fixed sequence, and every step is load-bearing.

  1. Give the agent an inbox you can forward to. Any IMAP account works. Allowlist the senders whose mail may be read (your own addresses) and drop the rest.
  2. Keep a registry file, not a scheduler entry. One JSON list, one entry per parcel, deduplicated on the tracking number. Adding a parcel is a line; cancelling one is a deletion. The scheduler never changes.
  3. Parse deterministically. Carrier comes from keywords, then the number’s shape: Royal Mail tracked numbers are 13 characters ending GB, DPD is 12 to 14 digits, Yodel starts with JJD. Follow redirect links with a GET, because merchant platforms wrap tracking links and some ignore HEAD requests. Read the merchant and item from the original message, not the forwarding envelope.
  4. Poll on a fixed hourly schedule with output as the only signal. No output, no notification. Print only for the three events that matter: delivered, exception, or nudge. Never report “out for delivery”.
  5. Split carriers by reachability, verified from your own server. Probe each carrier once with curl -s -o /dev/null -w "%{remote_ip}". Whatever answers, poll. Whatever blocks you gets the nudge path: one message on the estimated day with the link, then silence.
  6. On delivered, remove the entry and file one task. The alert and the task are one event. The task is for the human, who taps it off after carrying the box upstairs.

What broke

Every free aggregator was a wall, and one was a trap. The tracking sites render client-side, so the HTML holds a search box and nothing else. Their APIs return 401 without keys. TrackingMore’s pricing page implies a free API tier; the signup page reveals the key needs a paid plan. I found that out at the signup page, not in production. A free tier often means a free web page, not a free API.

The proxy trick was aimed at the wrong CDN. For other UK services, a Cloudflare-routing tunnel has been the escape hatch. Royal Mail is not on Cloudflare; it is on Akamai, which blocks datacenter IPs on its own layer, so the trick could never work. One command (curl -w "%{remote_ip}") settles who is actually blocking you before you spend an evening building a workaround for a wall that is not there.

The first parse matched the wrong person. The merchant field came out as my own name, because the parser read the envelope sender: me, forwarding the email. The fix was to parse the embedded original message after the forward divider. Forwarded mail has two of everything, two senders, two dates, two sets of links. Decide which layer you are reading, or your data is about the forwarder.

Cost

Nothing. The inbox was already running, the server was already running, the poller is standard-library Python, and the cron runs with the agent switched off, so 325 executions cost zero tokens. The to-do integration uses a free TickTick account and its open API token. No paid tier survives anywhere in this build: the aggregator path died, and the registry-and-nudge design is what grew in its place.

Honest limits

Royal Mail and Amazon parcels are not watched, they are reminded. If the estimate is wrong, the nudge fires on the wrong day. The status check for reachable carriers is substring matching on public pages, so a redesign could produce a false silence rather than a false alarm, which is the right way round but still a limit. Unofficial endpoints can change without notice, and polling them is a grey area the carriers tolerate rather than authorise.

The pipeline also has one blind spot I have lived. In mid-August I rebuilt the mail side of the house and retired the inbox the parser lived in. A parcel forwarded after the swap entered the registry as a bare tracking number, no item, no merchant, no date. The agent still closed it out, but the entry it closed was half-empty. Keep the parser and the transport separate, and swapping inboxes costs nothing.

The takeaway

The useful lesson is not that an agent can watch your parcels. It is the pattern: a registry of things to watch, one silent cron that watches the registry, and one notification that means act now. Cron-per-item dies at the second item; noise-per-status kills the notification channel inside a week. The list absorbs change, the cron never notices, and the human only hears from the system when walking to the front door is worth it.

The next build in this series points the same watchdog at a live flight: a self-terminating monitor that watches one departure, says nothing until the inbound aircraft turns up on radar, and deletes its own cron when the plane lands.

← All transmissions