Get API key
Release notes

Changelog

Every developer-facing change lands here. Subscribe via RSS or the email digest for breaking-change advance notice.

v1.8
Current

IOSS ownership check on shipment create

An ioss_number override on POST /v1/shipments must now belong to your account. Numbers saved in settings are auto-filed with the carrier.

  • changedPOST /api/v1/shipments ioss_number override is now ownership-checked: it must be your account's saved IOSS (settings 報關資料) or a marketplace platform IOSS registered to your account — anything else returns 400 IOSS_NOT_OWNED. Previously only the format was validated. Orders without an override (account default) are unaffected.
  • addedSaving an IOSS in settings now auto-files (备案) it with the carrier — a carrier-side prerequisite before EU orders can carry the number. Filing normally takes effect immediately. To register a marketplace platform IOSS (e.g. Amazon's) on your account, contact support.
v1.7

Service-pricing model v2 — Product Codes, Service Codes, live rate quotes

The service catalog moved to a three-layer model: Product Code (entitlement container, e.g. HKASTD) → Service Code (the orderable unit you put in service_tier_id, e.g. HKASTDGGSTCM) → versioned rate tables. Rates are live, service_tiers is entitlement-scoped, and five legacy tier codes are retired.

  • changedLegacy tier codes RETIRED (breaking): US-GEN-PUB-001, US-GEN-PUB-002, HK-ST-GG-01, HK-US-SE-GG-01 and HKUSSTCS01 were deleted on 2026-06-10 — shipments referencing them now fail. Migrate to HKASTDGGSTCM (standard line) or the matching line from GET /api/v1/service_tiers. The only legacy code still live is HKUSSEGG01. See the new Service Codes guide for the mapping.
  • addedPOST /api/v1/rates is LIVE (the docs previously listed it as not-implemented/501). Entitlement-scoped quotes wrapping the internal billing engine: AU destinations price by postal-code zone (destination_postal_codezone; without it zoned options return available:false + reason), per-option opt_in_availability gates the SIGNATURE/INSURANCE checkboxes, surcharge_conflicts lists opt-ins that couldn't apply, and EU lanes carry an ioss_notice block. Engine rejections return 400 with the engine's real reason and data.field classification (rate_card / declared_value_hkd / parcels).
  • changedGET /api/v1/service_tiers is now entitlement-scoped: it returns only the Service Codes your account can book (Product Code entitlements + assigned rate cards), not the whole platform catalog — matching what the docs always promised. Tiers now expose product_code, service_options[] (cargo/air/last-mile dimensions) and is_standard_service. Note product_code (Merdi container, e.g. HKASTD) ≠ carrier.product_code (the upstream carrier's own reference). No entitlements → 200 + empty list.
  • addedPOST /api/v1/shipments request fields documented: selected_surcharges (e.g. ["SIGNATURE","INSURANCE"] — eligibility is per service code + destination; on the US priority line signature and insurance are mutually exclusive) and ioss_number (IM + 10 digits; overrides the account-level IOSS default; EU destinations only — shipped 2026-06-10 with the EU IOSS work, documented now).
  • addedNew Service Codes guide: the three-layer model, code anatomy (HKASTD + cargo GG/CS + air ST/PR + last-mile CM/PS), standard vs value-added lines, ETA conventions (standard ~8-12 business days, priority ~6-10), fetching your codes, and the two-product_codes disambiguation.
v1.6

Last-mile tracking number on shipment detail

The final-mile carrier number (e.g. USPS) is now on the shipment-detail API — the number you actually push to marketplaces.

  • addedGET /api/v1/shipments/{id} now returns last_mile_tracking_number — the final-mile carrier's own number (e.g. USPS 9214…), populated once the upstream carrier hands the parcel to the last-mile carrier. Null until that handoff. This is typically the number to push to eBay/Amazon and show the buyer, since the delivering carrier's tracker recognises it.
  • changedThe "which number do I use?" guide now lists three numbers — tracking_number (Merdi canonical), waybill_number (first-leg carrier, e.g. Yun YT…), and last_mile_tracking_number (final-mile, e.g. USPS). Rule of thumb: prefer the last-mile number for marketplaces once it's non-null, else fall back to the waybill.
v1.5

Wallet balance, webhook redeliver, integration-feedback hardening

Driven by a real third-party integration's feedback — proactive balance monitoring, self-serve redelivery, and docs that match the wire.

  • addedGET /api/v1/wallet/balance — read your prepaid wallet { balance, available, held, currency }. Poll available to top up before a batch hits insufficient funds, instead of discovering it mid-run. (available = balance − open holds.)
  • addedPOST /api/v1/webhooks/{id}/deliveries/{delivery_id}/redeliver — re-send a previously-failed delivery (re-fires the original event_id so your receiver can dedupe). Also surfaced as a 「重發」 button on the dashboard delivery viewer.
  • addedWebhook event payload schema is now published in the docs + OpenAPI (WebhookEventEnvelope): the delivered body is { event_id, event_type, livemode, created_at, data } — you no longer have to reverse-engineer it from a sample. Full event-type catalog included.
  • changedIdempotency-Key is now OPTIONAL on naturally-idempotent operations — DELETE /webhooks/{id} and POST /shipments/{id}/cancel no longer 400 when it's absent (still honoured for replay if you send it). It remains REQUIRED on create endpoints. The idempotency guide now has a per-endpoint requirement table.
  • fixedDocs ↔ wire consistency: corrected every remaining prose reference to the webhook signature header — it is X-Merdi-Signature (some guide paragraphs dropped the X- prefix). A CI lint now blocks this drift from recurring.
  • addedDocs clarity: a "which tracking number do I push to marketplaces?" section (carrier-native waybill_number for eBay/Amazon vs Merdi-canonical tracking_number), the buyer-facing tracking page URL, the dispatch-is-synchronous timing contract, and the base URL https://merdiexpress.com/api/v1/ made prominent in Quickstart.
v1.4

Offline webhook verification, X-Request-Id, idempotency normalisation

Webhook integration hardening — verify your signature handling end-to-end before production, even from private staging or CI.

  • addedGET /api/v1/webhooks/{id}/sample — returns a genuinely-signed sample event WITHOUT delivering it anywhere. Feed the returned raw_body + X-Merdi-Signature straight into your receiver's verification code and confirm — before production — that your HMAC check accepts a real Merdi signature. Works behind NAT / on localhost / in CI, where POST .../test (which actually delivers) can't reach you.
  • addedX-Request-Id response header on every API response (including 401s and 5xx). Echo your own X-Request-Id request header and we preserve it. Quote it when reporting an issue and we can pull the exact request from our logs.
  • changedIdempotency-Key payload matching now normalises JSON key order. Retrying the same logical request with fields in a different order (e.g. {a,b} vs {b,a}) is correctly treated as the SAME request — no more spurious 409 Conflict. Array order is still significant (a parcel list ≠ its reverse).
  • addedWebhooks guide expanded: a prominent ⚠ warning to verify against the RAW request body (the #1 integration bug), an at-least-once deduplication section (dedup on X-Merdi-Event-Id, stable across retries), and an offline-verification walkthrough using the new sample endpoint.
  • addedEvery public response shape is now in the OpenAPI spec (/openapi.json → 30 schemas). Shipment detail/summary, rate quotes, service tiers, and all webhook shapes are now fully documented + Postman-importable, not just the request bodies.
v1.3

Webhook rotation, sandbox carrier, Postman collection, live try-it console

Big polish pass — most asked-for endpoints + tooling so first-touch integration takes 5 minutes, not 5 hours.

  • addedPOST /api/v1/webhooks/{id}/rotate — rotate a webhook signing secret in place. Endpoint id, url, events, history all preserved. New plaintext secret returned EXACTLY ONCE. Recommended pattern: accept both old + new for an overlap window during rollover.
  • addedSandbox carrier (carrier_code = "sandbox"). Generates a real-looking waybill_number + a placeholder label_pdf_url so you can exercise the full label-fetch path in integration tests without paying for a real shipment. Works in both live and test modes.
  • addedPostman collection auto-published at https://merdiexpress.com/postman-collection.json. One click → import all endpoints with {{base_url}} + {{api_key}} variables ready to fill. Linked from the docs footer.
  • added/developers/api try-it console now hits the real API live (was a mock with a Math.random success rate). Use a mk_test_* key from your dashboard to exercise endpoints without leaving the docs site.
  • addedSelf-serve webhook delivery viewer at /dashboard/developers/webhooks/{id}/deliveries. State / event type / HTTP status / latency / response body excerpt per attempt. Cursor-paginated 20 at a time. Lets you self-debug receiver issues without filing a support ticket.
  • changedRemoved stale @merdiexpress/node SDK references from the quickstart + landing samples. That package was vapourware — the docs now show raw fetch / requests / curl. A real SDK is in the roadmap.
  • fixedRate-limit guide listed generic header names (X-RateLimit-Limit); corrected to our actual X-RateLimit-Limit-Second, X-RateLimit-Limit-Day, X-RateLimit-Reset-Day etc. Existing header behaviour unchanged.
  • security5xx outage alerter — internal change. Server errors now post to an ops Slack channel via ERROR_ALERT_WEBHOOK_URL (throttled per-route, per-5-min). Future prod outages should get noticed in minutes, not after a customer complains.
v1.2

Labels, base-URL hardening, secret-at-rest encryption

Polish pass driven by the first external integration feedback. Spec is easier to parse, labels are easier to retrieve, and customer signing secrets are encrypted at rest.

  • addedlabel_pdf_url and waybill_number now returned on GET /api/v1/shipments/{id} (and surfaced in the OpenAPI schema). Recommended polling pattern: 1s × up to 30s after the synchronous POST until label_pdf_url !== null or dispatch_status === "failed". URL is presigned OSS — fetch directly, no Authorization header.
  • addedNew /developers/guides/labels guide covering when the PDF is ready, the polling pattern, fetching + caching the PDF, and test-mode behaviour (dispatch skipped, dispatch=null).
  • addedBase URL is now embedded inside every paths key in the OpenAPI spec (e.g. /api/v1/shipments), with servers: [{url: "https://merdiexpress.com"}] at root. SDK generators that ignore the servers field now resolve correctly.
  • addedCompat redirect: any request to https://merdiexpress.com/v1/* returns HTTP 308 Permanent Redirect → /api/v1/* (preserves method + body — works for POST with Idempotency-Key). If your SDK config has the wrong base URL it will still work, but please update — 308 is cacheable.
  • securityWebhook signing secrets are now encrypted at rest in our database (AES-256-GCM envelopes, per-row IVs). A DB backup leak no longer exposes signing material. The secret you receive on POST /api/v1/webhooks is unchanged; this is a server-side hardening, no client action needed.
  • securityDependency security sweep: cleared 45 of 46 open vulnerability advisories on our build (the remaining one is bundled inside the Next.js framework itself and waits on the upstream release). Includes a major nodemailer bump and removal of unused auth chains.
  • fixedDeploy crashloop guard. If a build half-completes, the next deploy now refuses to start a broken process; the previously-good build keeps serving until a clean build replaces it. Closes a ~15 min / returning 502 incident on 2026-05-28.
  • changedSpec version bumped 1.0.0 → 1.1.0. info.description now leads with an explicit "BASE URL: every endpoint is served from https://merdiexpress.com/api/v1/" note.
v1.1

Shipment creation goes live

Order placement + cancel on the API surface. Wallet-debited live mode, free-and-safe test mode.

  • addedPOST /v1/shipments — create a shipment server-side: quote engine re-priced bit-for-bit, pricing snapshot frozen into the record, label-created tracking event emitted. Requires Idempotency-Key.
  • addedPOST /v1/shipments/{id}/cancel — state-machine cancellation for pre-pickup shipments (label_created only). Live mode refunds the wallet by the original total; test mode never deducts so refund is always 0. Cancelling twice returns the same snapshot.
  • addedPOST /v1/rates — real billing-engine quote (was a 501 placeholder in v1.0). Accepts weight_kg, destination_country, goods_category + optional surcharges; returns freight + reg fee + per-line surcharges + total HKD.
  • addedCORS preflight on /v1/* so browser clients (try-it consoles, single-page integrations) work without a proxy.
  • addedWebhook delivery worker with exponential backoff retries (5 attempts over ~30 min) + auto-disable after 10 consecutive failures.
  • addedSelf-serve key + webhook management in the dashboard at /dashboard/developers/{keys,webhooks} — no more admin-CLI dependency for onboarding.
  • fixedGET /v1/shipments/{id} and GET /v1/shipments (list) now correctly resolve account → client ID; previously returned 404 for own-shipment lookups.
v1.0

Phase A/B foundation launch

First public release. Tracking, shipments (read-only), service tiers, webhooks. Rates endpoint reserved.

  • addedBearer auth with mk_live_* and mk_test_* key prefixes; per-key rate limit (100/sec, 10k/day).
  • addedIdempotency-Key header support on all POST endpoints; safe to retry for 24h.
  • addedWebhooks: HMAC-SHA256 signatures on X-Merdi-Signature header (Stripe-style t=,v1= format); signing_secret returned once on create.
  • addedEndpoints: shipments list+detail, tracking by number, service tiers, countries, webhooks CRUD + test-fire.
  • changedWire format adopts snake_case across all request and response bodies.
v0.9.3

Pre-launch beta — partner program

Beta cohort: 8 HK 3PLs. Feedback loop into v1.0 surface.

  • addedTest-mode keys (mk_test_* prefix) — same endpoints and same merdiexpress.com/api host as live, but no carrier dispatch and no wallet deduct. Mint from /dashboard/developers/keys.
  • changedRenamed pickup_etaeta on tracking responses.
  • fixedCheckpoint ordering now stable when two scans share a timestamp.
v0.9.0

Internal dogfood

API surface frozen pending external review.

  • addedOpenAPI 3.1 spec published internally.
  • securityKey prefixes (mk_test_, mk_live_) introduced to prevent accidental cross-env writes.
You've reached the bottom of history.