Every Coinbax API response, success or failure, arrives in the same envelope. Write one parser and it handles every endpoint and every outcome.
The envelope
{
"success": true,
"data": { "id": "9f8b3c2a-...", "status": "escrowed" },
"meta": {
"timestamp": "2026-07-16T12:34:56.789Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000"
},
"error": null
}
| Field | Type | Meaning |
|---|---|---|
success |
boolean | true on success, false on error |
data |
object or null | The payload; null on error |
meta |
object | Timestamp, request ID, pagination, and (on errors) path and method |
error |
object or null | Error details; null on success |
List endpoints add pagination to meta:
{
"meta": {
"timestamp": "...",
"requestId": "...",
"pagination": {
"page": 2,
"limit": 20,
"total": 150,
"totalPages": 8,
"hasNextPage": true,
"hasPreviousPage": true
}
}
}
Error responses
On failure, success is false, data is null, and error carries the
detail:
{
"success": false,
"data": null,
"meta": {
"timestamp": "2026-07-16T12:34:56.789Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"path": "/api/v1/transactions",
"method": "POST"
},
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"statusCode": 400,
"details": { "errors": ["amount: must be a positive number"] }
}
}
The error object:
| Field | Type | Meaning |
|---|---|---|
code |
string | Stable machine-readable code; branch on this, not on message |
message |
string | Human-readable summary |
statusCode |
number | The HTTP status, mirrored into the body |
details |
object, optional | Field-level validation errors or additional context |
Error codes by status
| HTTP status | Code | When you see it |
|---|---|---|
| 400 | VALIDATION_ERROR |
A request field failed validation; details names the field |
| 400 | INVALID_REQUEST |
Malformed request body or parameters |
| 401 | UNAUTHORIZED |
Missing, invalid, or expired credential |
| 403 | FORBIDDEN / INSUFFICIENT_SCOPES |
Valid credential, but it lacks a required scope |
| 404 | NOT_FOUND |
The resource does not exist or belongs to another workspace |
| 409 | CONFLICT |
Duplicate resource or an invalid state transition (for example refunding a completed transaction) |
| 422 | UNPROCESSABLE_ENTITY |
The request is well-formed but fails a business rule |
| 429 | RATE_LIMIT_EXCEEDED |
Too many requests; honor Retry-After (see rate limits) |
| 500 | INTERNAL_SERVER_ERROR |
Unexpected server error |
| 503 | SERVICE_UNAVAILABLE |
Temporary outage; retry with backoff |
500-level responses are sanitized: they never leak stack traces or internal
detail in production. Everything you need for support is in
meta.requestId.
One parser for every outcome
Because the shape never varies, error handling collapses to a single function:
async function coinbax(path, init) {
const res = await fetch(`${BASE}${path}`, init);
const body = await res.json();
if (!body.success) {
const err = new Error(body.error.message);
err.code = body.error.code;
err.status = body.error.statusCode;
err.requestId = body.meta.requestId;
throw err;
}
return body.data;
}
Branch on error.code, never on message text. Messages are for humans and
may change; codes are stable.
Request IDs and support
Every response carries a meta.requestId (also returned in the
X-Request-ID response header). It traces the request through the whole
platform.
- Log it alongside your own request logs on every error.
- Send it when opening an integration to make your requests traceable
end to end: pass your own ID in the
X-Request-IDrequest header and the API will echo it back instead of generating one. - Quote it when contacting support. A request ID plus a timestamp lets us find the exact request, its control executions, and its outcome.
Next steps
- Rate limits: handling 429 with backoff
- API scopes: resolving 403 responses
- Payments API reference: per-endpoint error documentation