In September, we launched Fast Delivery, a checkout pilot in Los Angeles: orders ship through DashLink, DoorDash's parcel network, and arrive in one to three days. Day one brought 224 orders. For patients, this is a new experience as Fullscript promises delivery measured in days, weekends included. This helped get supplements much faster. The hard engineering behind that delivery window is tracking, not checkout.

Until this launch, all our package tracking worked one way: ask the carrier on a schedule. A job polls seven carriers' APIs about all active packages, as often as every 30 minutes. For ground shipping that takes five to seven days, a status that runs a few hours behind is fine.

A DashLink package can be delivered the day after it ships. If polled on a schedule, it could show "shipped" while the box sat on the doorstep. So, DashLink became the first carrier we track by webhooks. The carrier tells us when something happens instead of waiting to be asked, and the patient sees it moments later. This post covers the pipeline, the fallback behind it, what broke in week one, and the pattern we'd recommend to anyone making the same move.

The challenge

Polling wastes almost every call. A package in transit for five days gets hundreds of status requests, and changes status around six times. The cost (API fees, rate limits, queue load) scales with the number of packages in flight, not with how much is actually happening.

Freshness is capped by the schedule. When a package goes out for delivery at 9:05, the status changes at 9:05, but the patient keeps seeing the old status until our next sweep. The wait isn't for the information to exist, it's for us to ask. That's how an "arriving today" email loses a race with the doorbell. For a 1-3 day carrier, the moment customers care about are hours apart. A status can come and go entirely between two polls.

The tracking link can't be computed at all. For every carrier we have integrated before, the patient's tracking link is a fixed URL with the tracking number dropped in, built on the fly, never stored. DashLink mints a unique link per delivery, and it exists only inside the event payloads. Polling-era assumptions had nothing to build it from.

The solution: an event pipeline with a daily safety net

The pipeline
Here’s how it goes: DashLink sends an event, the endpoint verifies the signature, checks a per-carrier switch, puts the event on a queue, and responds immediately. A background job translates DashLink's vocabulary into our shared statuses through a small adapter and updates the package's tracker record, the single record that the patient's order page, the shipping emails, and our support tools all read. End to end, it only takes a few seconds. The moment something changes, the patient's view changes with it; nobody waits for the next scheduled check. The adapter is the only carrier-specific code in the path, so the next carrier is an adapter, a status mapping, and a switch.

Misbehaving webhooks
Events can arrive twice, late, and out of order. We made that harmless instead of trying to prevent it: a package's status only moves forward, so a stale "in transit" after "out for delivery" changes nothing. And every event we receive but don't apply is counted with a named reason: switch off, unknown event type, no tracking number, or no tracker record yet. Those counts feed a per-carrier dashboard. It sounds like bookkeeping; it's what saved us in week one.

The safety net
Webhook-first doesn't mean webhook-only. Once a night we still ask DashLink about each active package, through the same polling machinery our other carriers use, writing through the same forward-only rule. If the webhooks did their job, the check changes nothing. If one was missed, it heals within a day, and the check also backfills tracking links. One call per package per day as insurance, instead of dozens as the mechanism.

What broke in week one

Two days in, the dashboard showed about 700 events per day dying with "no tracker record yet" while everything else looked healthy. Tracing one tracking number explained it. DashLink starts sending events the moment a label prints, but we only create the tracker record once the warehouse confirms the shipment back through our systems, which took about four hours on launch day. The retry policy for "no tracker yet" was a standard backoff: eight attempts that sound generous and are exhausted in 78 minutes. Every early event for every package died in the gap. Three early events across 230 packages is your 700.

The fix was one line:

retry_on TrackerNotFound, wait: 1.hour, attempts: 7

Six flat hourly retries covering six hours, one fewer attempt than before. The standard backoff was the wrong shape, not the wrong size: its first five retries all fire within twelve minutes, useless when the thing you're waiting for takes hours. Retry windows should be sized to the upstream process, and the real question ("how long until the warehouse confirms a shipment?") hadn't been asked. The lesson: counting ignored events with reasons turned this into a two-hour fix instead of a support ticket three weeks later.

Results

  1. Patients stop waiting on our schedule. A status change reaches the patient in under a minute. Under polling, that same change would sit invisible until the next sweep, even though it had already happened. For a carrier this fast, that's the difference between "out for delivery" being useful and being trivial. Launch day processed all 224 orders' worth of events with zero pipeline failures.
  2. The economics flipped. A DashLink package generates six to eight events over its whole life, plus one nightly check. Polling the same package every 30 minutes would cost up to 48 calls a day, nearly all answering "no change." The events also carry the per-delivery tracking link patients use, and there’s no way to get that by polling.
  3. Every carrier gets the same observability. One dashboard shape per carrier: what arrived, what applied, what was ignored and why. When something drifts, the graph names it.

How any team can implement this

Answer fast and work async. Signature check, kill switch, queue, 200. Nothing else belongs in the webhook endpoint.

Keep source-specific code in adapters. The core pipeline stays generic, and each new source costs a mapping and a switch instead of a project.

Make the apply step forward-only. Duplicates and out-of-order delivery are the normal case. Designing writes to be safely repeatable beats trying to prevent redelivery.

Count what you ignore, with reasons. "Received minus processed" with no explanation is a debugging dead-end. The named reason is what turns a graph into a diagnosis.

Keep one reconciliation poll. A single daily pass through the same write path turns any missed event into a one-day problem instead of a mystery.

Size “retries” to the upstream gap, and store only what events carry. Find the slowest legitimate gap between "events start" and "you're ready," and cover it with room to spare. If a piece of data only exists in the event (like a per-delivery link), save it the moment it flies past.

Pitfalls we learned from experience: don't assume tracking links can be derived from tracking numbers; don't let a default backoff shape decide your coverage window; and don't ship a webhook source without a switch to turn just that source off.

Conclusion

The core insight is small: being told beats having to ask. Every step away from "ask on a schedule," and toward "apply events as they happen," made tracking faster for patients, cheaper to run, and easier to debug, and the fallback poll shrank from the whole mechanism to a once-a-day safety net.

The pattern proves itself at carrier number two. USPS webhooks are next, and if the design holds, that integration is an adapter, a mapping, and a switch, with everything else unchanged. If you're weighing polling against webhooks, reach out. Happy to compare notes.