Skip to content

Reliability & Limits

This page consolidates the runtime contract: how the API behaves under retries, partial failures, and load. Read it once when integrating; the defaults are designed to keep your client safe under network turbulence.


Idempotency-Key

Every fund-moving request requires an Idempotency-Key header:

  • POST /transactions/withdraw
  • POST /transactions/transfer
Idempotency-Key: 3f7c0a1e-9b22-4f8d-bd3e-2d91a7e9f201
Content-Type: application/json

Format: up to 64 characters, [A-Za-z0-9_-]. UUIDs, ULIDs, and stable business identifiers all fit. Choose one per logical operation, not per HTTP request — that way an SDK retry after a network blip replays the same key and gets the original response back.

Behavior matrix:

Same key, same body Same key, different body New key
Returns the original response (cached for 24h). No second withdrawal is submitted. Returns 400 Bad Request. Almost always a client bug — keys must be unique per logical request. Treated as a brand-new request.

The cache is keyed by (tenantId, endpoint, idempotencyKey), so the same key value can be reused across withdraw and transfer without collision. TTL is 24 hours; older keys roll off and become reusable.

curl -X POST $BASE_URL/transactions/withdraw \
  $HEADERS \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"sourceWalletId":"...","destinationAddress":"...","amount":"0.5"}'

Rate limits

We rate-limit to protect your key's request budget from a single hot loop and to keep shared infrastructure healthy for every partner. The partner API enforces exactly two global buckets — there is no per-tenant, per-scope, or per-endpoint layering on top.

Bucket Limit Partition key Over-budget behavior
Authenticated (signed API key) 120 req / min / key JWT claim api_key_id (set by the signed-API-key handler) Immediate 429; no queueing.
Anonymous (/health, optional /swagger/*) 60 req / min / IP Client remote IP (missing IP falls into a shared unknown bucket) Immediate 429; no queueing.

Each bucket uses a sliding window split into 4 segments of 15 seconds, so short bursts up to roughly 30 requests in any single 15-second slice are absorbed and smoothed across the full minute rather than tripping at the 31st request.

No per-endpoint throttles

Sensitive routes like POST /transactions/withdraw are not individually throttled on the partner API — they share the same 120/min/key budget as every other authenticated call. No endpoint on the partner surface (Api.Public) has its own limit.

When you exceed the limit:

HTTP/1.1 429 Too Many Requests
Retry-After: 7

The response body is empty — clients must handle a bare-status 429. Retry-After is an integer seconds value populated from the limiter's lease metadata; treat it as a hint, not a guarantee.

Client backoff guidance

on 429 response:
  1. if Retry-After header present → sleep that many seconds, then retry
  2. else                          → sleep 1s, then exponential backoff
                                     with jitter (cap at 30s) on each retry
  3. abort after N attempts (e.g. 6) and surface the failure to an operator
  4. never silently swallow a 429 — log it so you can size your workload
  5. DO NOT mint extra API keys to multiply the budget — this is
     enforcement bypass and the keys will be revoked

If your steady-state workload genuinely needs more than 120 req/min, batch reads first (use list endpoints with take up to the page-size max instead of fan-out GETs), then contact us about a higher limit for your account. See also Authentication and the 429 row in the error reference.


Replay protection

Each signed request is single-use within its drift window. Specifically, the API caches the tuple (keyId, timestamp, signature) for a short period and rejects any second request with the same triple as 401 Unauthorized — Request signature has already been used.

  • The drift window is 30 seconds (the X-Timestamp header must be within ±30s of server time).
  • The cache holds the tuple for 60 seconds — twice the drift window — so a captured request can't be replayed even if it was captured at the edge.

This means: if your client retries an unsuccessful request, regenerate the signature (new timestamp + new HMAC). Don't just resend the same bytes — that will fail with 401.


Page-size limits on list endpoints

Endpoint Default Max
GET /transactions take=20 take=200
GET /webhooks/deliveries pageSize=20 pageSize=200

Requesting a value above the maximum returns 400 Bad Request with a field-specific error in errors. Page through results — don't try to pull everything in one shot.


Webhook URL safety

When you configure a webhook URL via PUT /webhooks/outbound it must satisfy these rules — both at configuration time and at every delivery (DNS is re-resolved before each send):

  • Scheme must be https://. No http://, no file://.
  • Host must be public. Loopback (localhost, 127.0.0.1), RFC1918 private networks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), link-local (169.254.0.0/16 — including the cloud-metadata address), IPv6 ULA (fc00::/7), and multicast are all rejected.
  • Redirects are not followed. A 3xx from your endpoint is treated as a delivery failure.

If a webhook is delivered to a host that newly fails the DNS check (e.g. the partner moved their endpoint behind a private network), the entry is dead-lettered rather than retried indefinitely. Inspect via GET /webhooks/deliveries.


Outbound delivery semantics

  • At-least-once. Network errors and 5xx/timeouts are retried with exponential back-off (~10s → 30s → 60s → 5m → 15m). After 5 failed attempts a delivery is marked dead.
  • 5-second timeout per delivery attempt. Slow handlers are treated as failures and retried.
  • No delivery ordering guarantees across different events — each delivery is independent. Use eventType + entityId + timestamp to reconstruct order if you need it.
  • HMAC-SHA256 signature in X-Webhook-Signature; verify on every delivery (see Webhook Integration).

Error handling cheat sheet

Status Retry? Action
400 No Fix the request. Check errors.
401 No Bad signature / clock drift / replay. Re-sign and retry once.
403 No Missing scope / IP not in allowlist. Ask your admin.
404 No The resource ID is wrong or belongs to another tenant.
409 No Look up the existing resource.
429 Yes (back off) Honour Retry-After.
500 Yes (rarely) Server error. Retry sparingly.
502 Yes Custody backend hiccup. Retry with back-off.

See Error Handling for the full reference.