Error Handling¶
The platform uses standard HTTP status codes and a single JSON error format across every endpoint.
HTTP status codes¶
| Code | Meaning | When it occurs |
|---|---|---|
200 OK |
Success | Successful GET / PUT / PATCH requests |
201 Created |
Resource created | Successful POST that creates a new entity |
204 No Content |
Success, no body | Successful DELETE |
400 Bad Request |
Validation or business-rule error | Bad input, missing required fields, domain-rule violations |
401 Unauthorized |
Authentication failed | Missing, invalid or expired API key; bad signature; clock drift > 30s |
403 Forbidden |
Insufficient permissions | API key lacks the required scope; IP not in the allowlist; cross-tenant access |
404 Not Found |
Resource does not exist | The ID does not exist or belongs to another tenant |
409 Conflict |
Uniqueness conflict | E.g. a wallet for that asset is already issued |
429 Too Many Requests |
Rate limit exceeded | Per-key or per-IP request budget exhausted. See Reliability & Limits → Rate limits. |
500 Internal Server Error |
Server error | Unhandled exception (logged server-side) |
502 Bad Gateway |
Custody backend failure | The underlying custody backend returned an error or is unreachable |
Error response format¶
Every error response uses the same envelope:
{
"statusCode": 400,
"message": "A human-readable description of the error.",
"errors": {
"generalErrors": [
"A human-readable description of the error."
]
}
}
| Field | Type | Description |
|---|---|---|
statusCode |
integer | HTTP status code |
message |
string | Short summary |
errors.generalErrors |
string[] | One or more error messages |
For validation errors (from FastEndpoints), the response may also include field-specific errors:
{
"statusCode": 400,
"message": "One or more errors occurred.",
"errors": {
"amount": ["'Amount' must be greater than 0."],
"sourceWalletId": ["'Source Wallet Id' must not be empty."]
}
}
500-level responses¶
For security, 500-level errors do not expose internal details — the body always returns a generic message:
{
"statusCode": 500,
"message": "One or more errors occurred.",
"errors": {
"generalErrors": ["An unexpected error occurred."]
}
}
Handling strategies¶
Transient errors — retry¶
502 Bad Gateway from the custody backend is almost always temporary.
Retry with exponential back-off (e.g. 1s, 2s, 4s, 8s, 16s) up to ~5
attempts. If 502 persists past that, raise an alert.
Authentication errors — don't retry¶
401 usually means one of:
- Client clock has drifted more than 30 seconds from server time — synchronize via NTP.
- The signature was computed incorrectly — re-check the algorithm. See Authentication for the canonical-string and HMAC steps.
- The API key was revoked — ask your administrator to issue a new one.
- Request IP is outside the configured allowlist for that key.
403 means "the key doesn't have the scope required for this endpoint" or
"the IP isn't in the allowlist". Ask your administrator for a key with the
appropriate set of scopes.
Validation errors — fix the input¶
400 always returns machine-readable errors in the response — surface
them to the user or use them to repair the request before retrying.
Conflicts — handle idempotently¶
409 typically means you tried to create something that already exists
(duplicate wallet for the same asset, address-book entry that's already
allow-listed, etc.). Idempotent code looks the existing resource up
instead of creating a new one.
Domain-rule errors¶
Some 400 responses encode business-logic violations rather than
validation failures, e.g.:
Other examples:
"Source and destination wallets must be for the same asset.""Insufficient balance."
These won't go away with a retry — fix the request or the underlying state and resubmit.
Example error handling (Node.js)¶
async function callApi(method, path, body) {
const headers = signRequest(method, path, body); // see Authentication
headers["Content-Type"] = "application/json";
const response = await fetch(`${BASE_URL}${path}`, {
method,
headers,
body: body || undefined,
});
if (response.ok) return response.json();
const error = await response.json();
switch (response.status) {
case 400: // bad input — surface error.errors to the user
throw new ValidationError(error);
case 401: // bad key / signature / clock drift — DO NOT retry
case 403: // missing scope / IP not allowed — DO NOT retry
throw new AuthError(error);
case 404:
throw new NotFoundError(error);
case 409:
throw new ConflictError(error);
case 502: // transient custody backend issue — retry with back-off
await sleep(2000);
return callApi(method, path, body);
default:
throw new Error(error.message);
}
}