Request and response format
Content type
Requests with a body use JSON:
Content-Type: application/json
Responses are always application/json; charset=utf-8, including errors. There
is no XML, form-encoded variant, or content negotiation.
The one exception is the OAuth2 token endpoint, which accepts
application/x-www-form-urlencoded because that's required by the OAuth2
specification.
Methods
| Method | Use |
|---|---|
GET | Retrieve a resource or list. Never has side effects |
POST | Create a resource, or perform a named action |
PATCH | Partially update. Only the fields you send change |
DELETE | Rarely used. Most "deletions" are state changes, such as closing a card |
PUT is not used. Full replacement is a poor fit for objects with
server-managed fields and creates more problems than it solves.
Actions that don't fit normal CRUD use sub-paths instead of a type field in
the request body:
POST /v1/cards/{card_id}/freeze
POST /v1/cards/{card_id}/unfreeze
POST /v1/cards/{card_id}/replace
Object shape
Every object includes id, object, and created_at:
{
"id": "card_01HX8ZQ2K4M7P9RSTV",
"object": "card",
"status": "active",
"created_at": "2026-07-29T14:32:05Z",
"updated_at": "2026-07-29T15:01:44Z"
}
object tells you what kind of resource you're looking at, so you can dispatch
on the object type without parsing the ID prefix. This is especially useful for
polymorphic handling, such as when an object comes from a webhook payload or a
mixed search result.
Field conventions
| Rule | Example |
|---|---|
snake_case field names | cardholder_id, not cardholderId |
Timestamps suffixed _at | created_at, expires_at |
Booleans prefixed is_ or has_ | is_default, has_pin |
Enums lowercase snake_case | "review_required", never "REVIEW_REQUIRED" or 3 |
| Money is always an object | {"amount": 1050, "currency": "SGD"} |
| Nullable fields documented as such | Absent and null mean the same thing |
Fields that are null are omitted from responses. You don't need to distinguish
between a missing key and a null value.
Request headers
| Header | Required | Purpose |
|---|---|---|
Authorization | Yes | Bearer <access token> |
Content-Type | On requests with a body | application/json |
Idempotency-Key | Strongly recommended on POST | See idempotency |
Aleta-Version | No | Pin to a dated release |
Response headers
| Header | Always present | Purpose |
|---|---|---|
Aleta-Request-Id | Yes | Unique per request. Include it in every support ticket |
RateLimit-Limit | Yes | See rate limits |
RateLimit-Remaining | Yes | |
RateLimit-Reset | Yes | |
Retry-After | On 429 and 503 | Seconds to wait |
Deprecation / Sunset | On deprecated endpoints | See versioning |
Aleta-Request-Id is probably the most useful thing to log. When you have a
question about a specific request, that ID lets support find the call directly
instead of having to dig through a whole thread.
Unknown fields
Unknown fields in a request body are rejected with 400 and the error code
unknown_parameter. The offending field is included in param.
This is intentional. Silently ignoring an unknown field turns a simple typo,
like ammount instead of amount, into a card issued with the wrong limit
without giving you any indication that something went wrong.