Get API key
Guides / Rate limits

Rate limits

Each key is capped at 100 requests/second and 100,000 requests/day. The daily quota resets at 00:00 Hong Kong time (UTC+8). Above this we return 429 with a reason.

Caps

ScopeLimitWindow
Per key1001 second (fixed window)
Per key100,000Calendar day, resets 00:00 HKT
Per account—No additional account-wide cap today; limits apply per key.

Response headers

Every authenticated response includes:

http
HTTP/1.1 200 OK
X-RateLimit-Limit-Second:     100
X-RateLimit-Remaining-Second: 87
X-RateLimit-Limit-Day:        100000
X-RateLimit-Remaining-Day:    94120
X-RateLimit-Reset-Day:        1789142400     # unix seconds — next 00:00 HKT (16:00 UTC)
# On 429:
# Retry-After: 1     # seconds until the relevant bucket replenishes
# X-RateLimit-Reason: per_second | per_day

429 response body:

429 response body
{
  "status": 429,
  "sysCode": "1500004",
  "message": "Rate limit exceeded (per_day); retry after 9000 seconds",
  "data": {
    "reason": "per_day",
    "retry_after_seconds": 9000,
    "day_reset_at": "2026-09-11T16:00:00.000Z"
  }
}

Backoff strategy

On 429, read the Retry-After header (seconds). If absent, use exponential backoff with jitter: min(2^n * 100ms, 30s) + rand(0, 250ms). Cap at 5 retries. reason=per_second → wait 1 second; reason=per_day → wait until X-RateLimit-Reset-Day, don't retry at a fixed interval. A rejected 429 does not consume your daily quota.

Node — fetch wrapper
async function callWithBackoff(req, { max = 5 } = {}) {
  for (let n = 0; n <= max; n++) {
    const r = await fetch(req);
    if (r.status !== 429 && r.status < 500) return r;

    const retryAfter = Number(r.headers.get('retry-after'));
    const delay = retryAfter
      ? retryAfter * 1000
      : Math.min(2 ** n * 100, 30_000) + Math.random() * 250;
    await new Promise(res => setTimeout(res, delay));
  }
  throw new Error('exhausted retries');
}

Server-side caching for terminal shipments

For terminal shipments (delivered / cancelled / returned), GET /api/v1/tracking/{tracking_number} responses are served from a server-side cache for up to 6 hours (X-Merdi-Cache: hit | miss | bypass). Responses always carry Cache-Control: no-store — cache status is only reflected in X-Merdi-Cache. Prefer GET /api/v1/shipments?updated_after= for incremental polling, and stop polling a shipment once it reaches a terminal status.

Replay of repeated rejected requests

If the same key re-sends a payload with identical canonical-JSON content (object key order and whitespace don't matter; array order is preserved) within 1 hour of it being rejected by a carrier data rule (422 YUN_DISPATCH_REJECTED, sys_code 9900027), POST /api/v1/shipments replays the same 422 (data.negative_cache: true, X-Merdi-Negative-Cache: hit) instead of hitting the carrier again. Resending fixed data must use a NEW Idempotency-Key — reusing the old key returns 409 (sys_code 1500003); the existing 422 under the same key is itself replayed by the idempotency layer for 24 hours, and this 1-hour negative cache only applies when a new key resends the identical payload. This cache's key is independent of Idempotency-Key; account-state 402 rejections are not cached this way.

X-Merdi-Cache and X-Merdi-Negative-Cache only describe the cache layer THIS request actually went through. When a response is instead replayed from the 24-hour Idempotency-Key store, it returns the original stored body without either header.