Skip to main content

The rules

  1. Every non-2xx response ships the standard error envelope.
  2. code is stable. Match on code, never on error (that string is for humans).
  3. Every response carries X-Request-Id. Log it on every error path so support can trace the call.

Pattern by status

400 (validation)

The request body failed schema validation. The issues[] array lists every offending path. Do not retry; fix and re-post.

401 (auth)

Missing or bad key, or the key was revoked. Do not retry. Rotate the secret in your config and alert the on-call team.

402 (payment)

Two distinct causes, distinguished by code:
  • TIER_DOWNGRADED: organization dropped below Growth. Surface a billing alert; the key is still valid once the org upgrades.
  • QUOTA_EXCEEDED: monthly quota exhausted. Wait for the reset on the first of the next month or upgrade tier.
Neither should be retried by an automated client.

422 (semantic)

The request was well-formed but cannot be executed. Common cases:
  • MISSING_HS_CODE: call /v1/classify first, then re-post.
  • UNSUPPORTED_DESTINATION: the country is outside coverage. Surface to a human.
  • DOMESTIC_SHIPMENT: origin equals destination. Filter these out upstream.

429 (rate limit)

Read Retry-After, sleep, retry once. If the retry also 429s, back off exponentially. See Rate limits for the full pattern. The SDKs implement this by default.

500 (internal)

Retry with exponential backoff. Two attempts total is sufficient. If both fail, log the request_id and page support. Never retry more than three times; the engine has already retried its own downstream calls before returning 500.

A production-grade retry wrapper

Idempotency and retries

Every mutating endpoint (calc, calc/bulk, classify, compare) accepts an Idempotency-Key header. Set it to a UUID v4 per logical operation. Re-sending the same key within 24 hours returns the original response, not a new calculation. This makes retries safe:
Different bodies with the same key return 409 IDEMPOTENCY_KEY_REUSED. Do not reuse keys across distinct requests.

Bulk partial-failure pattern

/v1/calc/bulk returns 200 even when some rows fail. Inspect aggregate.error_rows and errors[]; do not treat 200 as blanket success.

Timeouts

Recommend a 30-second client timeout on /v1/calc and /v1/classify, 60 seconds on /v1/calc/bulk and /v1/compare. The server enforces its own upper bound, but you want the client to give up first so your queue does not stall.