You're building the "Where's my order?" page: a status badge, a tracking timeline, an ETA, maybe a map pin. You need shipping-shaped JSON β and every real shipping API puts an account between you and your first fake response. Verified with plain unauthenticated curls, September 2026:
api.goshippo.com answers {"detail":"Authentication credentials were not provided."}. Test tokens exist, but only after dashboard signup.{"error":{"code":"UNAUTHORIZED","message":"Unable to access the requested resource, authorization failed."}}. Test-mode keys live behind an account.{"errors":[{"error_code":"unauthorized","message":"Access denied."}]}. Sandbox keys require registration.{"code":401,β¦"Access token is invalid."}. Even the tracking-only API wants a token first.Those test modes exist to verify their integration β carrier accounts, label purchases, rate quotes. If all you need today is a stateful shipping-shaped API for a frontend, a demo, a workshop, or CI, this guide builds one in a single paste: hosted, CORS-open, HTTPS, no account, no key.
BASE=https://mockbird.mockbird.workers.dev
P=$(curl -s -X POST $BASE/api/projects -H 'content-type: application/json' \
-d '{"name":"fake-shipping","blank":true}')
PID=$(echo $P | sed 's/.*"id": *"\([^"]*\)".*/\1/')
KEY=$(echo $P | sed 's/.*"adminKey": *"\([^"]*\)".*/\1/')
curl -s -X POST $BASE/api/projects/$PID/resources -H "x-admin-key: $KEY" \
-H 'content-type: application/json' -d '{
"name":"shipments",
"fields":[
{"name":"tracking_number","type":"uuid"},
{"name":"carrier","type":"oneOf","values":["usps","ups","fedex","dhl_express"]},
{"name":"service","type":"oneOf","values":["ground","two_day","overnight","international"]},
{"name":"status","type":"oneOf","values":["pre_transit","in_transit","out_for_delivery","delivered","exception"]},
{"name":"origin_city","type":"city"},
{"name":"destination_city","type":"city"},
{"name":"eta","type":"futureDate"},
{"name":"weight_oz","type":"number"}
],
"seed":8
}'
curl -s -X POST $BASE/api/projects/$PID/resources -H "x-admin-key: $KEY" \
-H 'content-type: application/json' -d '{
"name":"tracking_events",
"fields":[
{"name":"shipmentId","type":"oneOf","values":[1,2,3,4,5,6,7,8]},
{"name":"status","type":"oneOf","values":["pre_transit","in_transit","out_for_delivery","delivered","exception"]},
{"name":"location","type":"city"},
{"name":"description","type":"sentence"},
{"name":"ts","type":"pastDate"}
],
"seed":30
}'
Two tricks worth stealing: shipmentId is a numeric oneOf [1..8], so every seeded scan points at a shipment that actually exists β no dangling joins β and the vocabulary is the industry's own (pre_transit, out_for_delivery, exception are the states Shippo/EasyPost use), so a later swap to real data touches less code.
A shipment looks like this (live output, IDs will differ):
curl "https://mockbird.mockbird.workers.dev/m/<PID>/shipments/2"
{
"id": 2,
"tracking_number": "b5f3448b-d1e1-43c3-8c26-2a26370bfb5b",
"carrier": "dhl_express",
"service": "ground",
"status": "delivered",
"origin_city": "Nairobi",
"destination_city": "Zagreb",
"eta": "2026-12-10T01:46:33.772Z",
"weight_oz": 206
}
# the tracking timeline for one shipment (json-server-style nested route)
curl "$BASE/m/$PID/shipments/2/tracking_events"
# look a shipment up by its tracking number (exact-match field filter)
curl "$BASE/m/$PID/shipments?tracking_number=b5f3448b-d1e1-43c3-8c26-2a26370bfb5b"
# every delivered shipment / every exception to alert on
curl "$BASE/m/$PID/shipments?status=delivered"
curl "$BASE/m/$PID/shipments?status=exception"
# newest scans first, joined to their shipment
curl "$BASE/m/$PID/tracking_events?sortBy=ts&order=desc&limit=5&_expand=shipment"
Range filters compose too β ?ts_gte=2026-08-01 for "scans since August", ?weight_oz_gte=160 for heavy parcels. Need CSV for a spreadsheet demo? ?mock_format=csv&select=carrier,status,eta returns the same list as RFC-4180 CSV.
The real test of a tracking UI is change: a new scan arrives, the status flips, the timeline grows. These are real writes, visible to every subsequent read:
# a new carrier scan comes inβ¦
curl -s -X POST $BASE/m/$PID/tracking_events -H 'content-type: application/json' \
-d '{"shipmentId":2,"status":"delivered","location":"Zagreb",
"description":"Delivered, signed by resident","ts":"2026-09-16T04:00:00Z"}'
# β {"id": 31, β¦}
# β¦and the shipment itself flips to delivered
curl -s -X PATCH $BASE/m/$PID/shipments/2 -H 'content-type: application/json' \
-d '{"status":"delivered"}'
curl "$BASE/m/$PID/shipments/2/tracking_events" # timeline now includes scan 31
/track/:number endpointCarrier tracking pages accept any number in the URL. One custom route gives you the same shape β templated per request, no resource needed:
curl -s -X POST $BASE/api/projects/$PID/routes -H "x-admin-key: $KEY" \
-H 'content-type: application/json' -d '{
"method":"GET","path":"/track/:number",
"body":{"tracking_number":"{{params.number}}","carrier":"usps",
"status":"in_transit","checked_at":"{{now}}"}
}'
curl "$BASE/m/$PID/track/9405511899223197428490"
{
"tracking_number": "9405511899223197428490",
"carrier": "usps",
"status": "in_transit",
"checked_at": "2026-09-16T03:47:05.208Z"
}
Real tracking endpoints rate-limit, time out, and 503 at peak season β exactly what your retry/backoff and stale-data UI need to survive. Deterministically:
# first two polls fail, third succeeds β assert your retry logic rides it out
curl -i "$BASE/m/$PID/shipments/2?mock_seq=503,503,200"
# a slow carrier: 2 seconds before the response
curl "$BASE/m/$PID/track/TEST123?mock_delay=2000"
# peak-season chaos: 30% of requests randomly fail
curl "$BASE/m/$PID/tracking_events?mock_chaos=0.3"
mock_seq is per-client and resettable β full recipes in testing loading & error states.
EasyPost and Shippo push tracking updates to your server by webhook; so does Mockbird. Register a URL and every created/updated/deleted record fires a signed POST (X-Mockbird-Signature: sha256=β¦, HMAC of the raw body) β so the code path that reacts to "package moved" runs for real in dev and CI:
curl -s -X PUT $BASE/api/projects/$PID/webhook -H "x-admin-key: $KEY" \
-H 'content-type: application/json' -d '{"url":"https://your-app.example.com/hooks/tracking"}'
# then play the carrier: POST a tracking_event (section 3) β your endpoint
# receives a signed tracking_events.created delivery
Verification snippet and the queryable delivery log in sending test webhooks.
| You wanted | Shippo / EasyPost / ShipEngine / 17TRACK | Mockbird |
|---|---|---|
| First request without an account | β signup + API key, even for test mode | β one anonymous curl |
| Call it straight from a browser | β secret keys β backend required | β CORS-open GETs |
| Your own statuses, fields, edge cases | β³ their schemas, their test fixtures | β it's your data β POST/PATCH anything |
An exception shipment on demand | β³ special test tracking numbers | β seed it, or PATCH any record |
| Deterministic latency & failure sequences | β | β mock_delay, mock_seq, chaos |
| Reset to a known dataset per test | β³ varies by provider | β snapshots |
| Buy real labels, quote real rates, track real parcels | β that's their job | β fake by design |
The clean split: Mockbird for the order-tracking frontend, demos, portfolio projects, and CI. A real provider the moment code touches labels, rates, or real parcels. They compose β point your app at the mock in dev via an env base URL, the real API in prod.
orders can reference shipments.The paste block in section 1 is the whole setup β or open the dashboard and click it together. The same project also serves GraphQL, exports OpenAPI + TypeScript types, and ejects to db.json anytime β no lock-in.