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/withdrawPOST /transactions/transfer
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:
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-Timestampheader 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://. Nohttp://, nofile://. - 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
3xxfrom 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 markeddead. - 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+timestampto 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.