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)

Endpoints

MethodPathDescription
GET/healthLiveness
GET/v1/countries20 supported countries, weekend rules, data provenance
GET/v1/holidays?country=US&year=2026Holiday 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/etaCountry-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
  • with their key**, not our bare worker URL — otherwise widget traffic burns our 100k/day free quota and we can't attribute it.

  • It is a try-before-you-buy hook: the widget links back to the API listing.

Architecture

  • Hono (TypeScript) — no framework lock-in; the same code runs on
  • Cloudflare Workers, Node, Deno, Bun, or Fly.io.

  • Holiday data is bundled, not computed: data/holidays.json (88 KB raw,
  • ~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.

  • Weekend table (src/weekends.ts) is ported from the validated Python
  • table, including the 2026-10-10 corrections (Nepal Sat–Sun, Afghanistan Fri–Sat, Maldives Fri–Sat, Mauritania Sat–Sun, Djibouti/Somalia Friday-only).

  • Portability: wrangler.toml is intentionally binding-free — no KV,
  • Durable Objects, R2, AI, or any Cloudflare-only service. Nothing here can generate a surprise charge on any platform.

  • Port proof: test/regression.test.ts replays 49 fixtures generated by
  • 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.

CaseMeasured CPUHeadroom vs 10 ms
Typical ETA (2-day transit + trace)0.19 ms54x
Worst case (365-day transit + cutoff + full trace)3.1 ms3.3x
Single-day classification0.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 holidays package, run
  • 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: the source library computes CN holidays from general rules, but
  • China adjusts working weekends by annual announcement — treat CN as best-effort and refresh yearly.

  • Islamic-calendar holidays (SA/AE/MY): computed astronomically; real
  • observance can shift ±1 day with moon sighting.

  • Malaysia: national Sat–Sun weekend is used; some states observe
  • 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

← Back to the free calculator