Delivery ETA API
Cross-border delivery-date math for logistics and e-commerce: given an origin country, a destination country, a ship date, and a transit time in business days, return the delivery date accounting for both countries' public holidays and country-specific weekends — plus carrier cutoff times, timezones, and a day-by-day explainable trace.
The wedge: generic holiday APIs give you raw holiday lists. This API answers the actual business question — "if it ships from the US to Saudi Arabia on Thursday with 2-day transit, when does it arrive?" — with the reasoning shown.
Definitions (explicit, no silent assumptions)
- Mutual business day (lane): a calendar day that is a business day in
day_zero="exclude"(default): transit day 1 is the first mutualship_date: calendar date in the origin's local time.cutoff_time(default"17:00", origin local): ifbooked_atis- Every response carries
trace— one entry per day in the window with - Every ETA response carries a
disclaimer: this is a *calendar-based
both origin and destination — not a weekend and not a public holiday in either country. Transit days only ever count mutual business days.
business day after the effective ship date. "include": the effective ship date itself counts as transit day 1. The convention used is echoed in every response.
provided and falls after the cutoff, the parcel misses pickup and the effective ship date rolls to the next mutual business day. The trace shows exactly why.
per-country status (business_day/weekend/holiday), the holiday name where applicable, whether the day counted, and a note.
estimate*. It knows nothing about weather, customs holds, strikes, carrier performance, or address-level exceptions. It is not a carrier guarantee, and the listing says so too.
Endpoints
| Method | Path | Description |
| GET | /health | Liveness |
| GET | /v1/countries | 20 supported countries, weekend rules, data provenance |
| GET | /v1/holidays?country=US&year=2026 | Holiday list for a country/year |
| GET | /v1/business-days/is-business-day?date=…&country=… | Classify one day |
| GET | /v1/business-days/add?date=…&days=…&country=… | Add N business days (single country) |
| POST | /v1/eta | Country-pair delivery estimate (the product) |
POST /v1/eta
{
"origin": "US",
"destination": "SA",
"ship_date": "2026-11-05",
"transit_days": 2,
"cutoff_time": "17:00",
"booked_at": "2026-11-05T18:30:00-05:00",
"origin_tz": "America/New_York",
"destination_tz": "Asia/Riyadh",
"day_zero": "exclude"
}
origin, destination, ship_date, transit_days (1–365) are required. Everything else is optional. booked_at must fall on ship_date in origin_tz, or the request is rejected — dates that disagree are a data error, not something to guess about.
Response (abridged):
{
"delivery_date": "2026-11-10",
"effective_ship_date": "2026-11-08",
"cutoff_applied": true,
"calendar_days_elapsed": 5,
"weekend_days_skipped": 3,
"holidays_skipped": [],
"trace": [
{"date": "2026-11-05", "origin_status": "business_day",
"destination_status": "business_day", "counted_as_transit_day": false,
"note": "booked after 17:00 America/New_York carrier cutoff"},
{"date": "2026-11-08", "origin_status": "weekend",
"origin_detail": "weekend (Sunday + Saturday)",
"destination_status": "business_day", "counted_as_transit_day": false,
"note": "effective ship date (day_zero=exclude: transit counting starts on the next mutual business day)"}
],
"disclaimer": "Calendar-based estimate only: ..."
}
Copy-paste examples
curl
curl -X POST https://<worker>.workers.dev/v1/eta \
-H 'Content-Type: application/json' \
-d '{"origin":"US","destination":"DE","ship_date":"2026-12-23","transit_days":2}'
JavaScript
const res = await fetch("https://<worker>.workers.dev/v1/eta", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
origin: "US", destination: "JP",
ship_date: "2026-12-23", transit_days: 3,
booked_at: new Date().toISOString(),
origin_tz: "America/New_York", cutoff_time: "17:00",
}),
});
const eta = await res.json();
console.log(`Delivery: ${eta.delivery_date} (${eta.calendar_days_elapsed} calendar days)`);
Python
import requests
r = requests.post("https://<worker>.workers.dev/v1/eta", json={
"origin": "US", "destination": "DE",
"ship_date": "2026-12-23", "transit_days": 2,
})
eta = r.json()
print(eta["delivery_date"], "|", eta["disclaimer"][:60], "...")
for t in eta["trace"]:
if not t["counted_as_transit_day"]:
print(" skipped", t["date"], "-", t["note"])
Embeddable widget (acquisition hook, prototype included)
examples/widget.html is a ~50-line merchant-pasteable snippet: it calls POST /v1/eta and renders "Order within 3h 12m — get it by Mon, Dec 28" on a product page, with the cutoff countdown computed live. Effort to ship: trivial (done). Caveats, stated in the example itself:
- The snippet should point at the **merchant's own RapidAPI-proxied endpoint
- It is a try-before-you-buy hook: the widget links back to the API listing.
with their key**, not our bare worker URL — otherwise widget traffic burns our 100k/day free quota and we can't attribute it.
Architecture
- Hono (TypeScript) — no framework lock-in; the same code runs on
- Holiday data is bundled, not computed:
data/holidays.json(88 KB raw, - Weekend table (
src/weekends.ts) is ported from the validated Python - Portability:
wrangler.tomlis intentionally binding-free — no KV, - Port proof:
test/regression.test.tsreplays 49 fixtures generated by
Cloudflare Workers, Node, Deno, Bun, or Fly.io.
~9 KB gzipped) generated from the pinned holidays Python package 0.106 — see scripts/generate_holidays.py. Lookups are O(1); per-request CPU stays near zero. No network calls, no API keys, no per-request cost.
table, including the 2026-10-10 corrections (Nepal Sat–Sun, Afghanistan Fri–Sat, Maldives Fri–Sat, Mauritania Sat–Sun, Djibouti/Somalia Friday-only).
Durable Objects, R2, AI, or any Cloudflare-only service. Nothing here can generate a surprise charge on any platform.
the validated Python engine (scripts/generate_fixtures.py) and asserts byte-identical results — weekends for all 20 countries, 21 classification cases, 8 add-business-days cases, and the full 2026 US+DE holiday sets.
Benchmarks (measured 2026-10-10, Node 24 — proxy for Worker CPU)
Cloudflare Workers free plan (verified against official docs 2026-10-10): 100,000 requests/day, 10 ms CPU per invocation, no egress charges, no paid features enabled in wrangler.toml.
| Case | Measured CPU | Headroom vs 10 ms |
| Typical ETA (2-day transit + trace) | 0.19 ms | 54x |
| Worst case (365-day transit + cutoff + full trace) | 3.1 ms | 3.3x |
| Single-day classification | 0.002 ms | ~500k calls/s |
The worst case fits with room to spare. If transit_days ever grows past 365, re-run test/perf.test.ts first — the 10 ms wall is the one hard limit on the free plan.
Also verified: wrangler dev boots the worker locally and serves all endpoints correctly (no account needed for local dev).
Deploy (one command — Colby's step)
npx wrangler login # one-time: opens a browser for YOUR Cloudflare account
npx wrangler deploy # builds and publishes to <name>.workers.dev — that's it
Nothing is deployed until those run. There is no CI hook and no account configured in this repo.
Holiday data: update cadence & known limits
- Regenerate yearly in January: bump the
holidayspackage, run - China: the source library computes CN holidays from general rules, but
- Islamic-calendar holidays (SA/AE/MY): computed astronomically; real
- Malaysia: national Sat–Sun weekend is used; some states observe
npm run gen:holidays, re-run the full suite (the regression tests will catch dataset drift), redeploy. The API 422s on any date outside the generated range, so stale data can never silently produce wrong answers.
China adjusts working weekends by annual announcement — treat CN as best-effort and refresh yearly.
observance can shift ±1 day with moon sighting.
Fri–Sat (not modeled in the MVP — documented, not hidden).
Development
npm install # install deps
npm test # vitest: 77 tests (regression + ETA + routes + perf)
npm run typecheck # strict tsc
npm run dev # wrangler dev (local, no login)
npm run gen:holidays # regenerate data/holidays.json (yearly)
npm run gen:fixtures # regenerate Python parity fixtures