The error object
Every failure returns the same object. There is no second error shape, and no endpoint that fails differently.
{
"error": {
"type": "invalid_request_error",
"code": "card_invalid_state",
"message": "Card card_01HX8ZQ2K4M7P9RSTV is not frozen.",
"param": "card_id",
"doc_url": "https://developer.aletaplanet.com/api-reference/errors/error-code-catalog#card_invalid_state",
"request_id": "req_01HX8ZQ2K4M7P9RSTV"
}
}
Fields
| Field | Always present | Purpose |
|---|---|---|
type | Yes | Broad category. Use to decide how to handle |
code | Yes | Stable, machine-readable identifier. Branch on this |
message | Yes | Human-readable. For logs and engineers, not end users |
param | No | The offending field, when one can be identified |
doc_url | Yes | Deep link to this specific error |
request_id | Yes | Matches the Aleta-Request-Id header |
Types
type | HTTP | Meaning |
|---|---|---|
invalid_request_error | 400, 404, 409 | The request is malformed or contradicts current state |
authentication_error | 401 | Credentials missing, invalid, or expired |
permission_error | 403 | Credentials valid but not permitted |
validation_error | 422 | Well-formed but fails a business rule |
rate_limit_error | 429 | Too many requests |
api_error | 500, 502, 503 | Something failed on our side |
Branch on code, not message
code is part of the contract and will not change. message is prose and may
be reworded at any time for clarity.
// Correct
if (err.code === 'insufficient_funds') { promptTopUp(); }
// Wrong — breaks the moment the wording improves
if (err.message.includes('insufficient')) { promptTopUp(); }
Do not show message to end users
message is written for the engineer reading the log. It names internal object
IDs and assumes familiarity with the object model. Map code to your own
user-facing copy, in your own voice and language.
Multiple validation failures
When several fields fail at once, 422 returns them together in errors. The
top-level code falls back to the generic invalid_parameter — with more than
one failure there is no single code to promote, so errors[] carries the
detail. A 422 with exactly one failure promotes that failure's own code
instead:
{
"error": {
"type": "validation_error",
"code": "invalid_parameter",
"message": "The request failed validation.",
"doc_url": "https://developer.aletaplanet.com/api-reference/errors/error-code-catalog#invalid_parameter",
"request_id": "req_01HX8ZQ2K4M7P9RSTV",
"errors": [
{
"code": "invalid_parameter",
"param": "address.country",
"message": "address.country must be an uppercase ISO 3166-1 alpha-3 country code, for example SGP."
},
{
"code": "missing_parameter",
"param": "date_of_birth",
"message": "This field is required for individual cardholders."
}
]
}
}
What never appears in an error
Errors from the platform never expose the internals of systems underneath it. You will not see a raw downstream status code, a downstream identifier, or a vendor name in any field.
Where an underlying system fails in a way we cannot map to something actionable,
you get a clean 502 with code: "processor_unavailable" and a request_id. That
request_id is enough for us to trace the whole call — quoting it in a support
ticket is all that is needed.
This matters for your own error handling: the set of codes you must handle is finite, documented, and stable, rather than an open-ended passthrough of whatever a downstream system decided to emit.
Always log request_id
Log it on every response, not just failures. It is the difference between "a card creation failed sometime yesterday afternoon" and a single lookup that returns the exact request, its body, and its response.
It is also mandatory in support tickets — see contacting support.