Response Envelope
Every response from the Chainabit API follows a consistent envelope format. This makes it straightforward to handle responses generically in your client code, regardless of which endpoint you are calling.
Envelope Structure
interface ApiResponseEnvelope<T> {
data?: T;
meta?: ApiMeta;
error?: ApiError;
}A successful response includes data, and includes meta whenever the endpoint has pagination info or other metadata to report — meta is otherwise omitted, not null. An error response includes error and omits data.
Meta Object
interface ApiMeta {
requestId?: string; // Present on most responses; see "Request correlation" below
limit?: number; // Page size (list endpoints only)
offset?: number; // Current offset (offset-paginated endpoints only)
total?: number; // Total record count (offset-paginated endpoints only)
hasNextPage?: boolean; // Whether more records exist (list endpoints only)
nextCursor?: string; // Cursor for next page (cursor-paginated endpoints only)
}meta is assembled per endpoint — a single-record GET typically has no meta at all, while a list endpoint adds whichever pagination fields apply to it (offset-style or cursor-style, never both). There is no server-side timing field in the body; use the X-Request-Id header and your own client-side timing if you need to measure or report request duration.
Request correlation
Every request that reaches your endpoint's guards and handler gets a X-Request-Id response header, and — for error responses specifically — the same value in error and meta.requestId. A request rejected before authentication (for example, a missing bearer token) has no request ID yet at that point, so both the header and meta are absent on that response. Include your own correlation ID in a X-Request-Id request header if you need one even for rejected requests — the API doesn't generate one until the request has passed initial auth.
Error Object
interface ApiError {
code: string; // Machine-readable error code, e.g. "bad_request", "not_found"
message: string; // Human-readable description
details?: any; // Additional context — shape varies by error type
}code is one of a fixed set of snake_case values keyed to the HTTP status — bad_request, unauthorized, forbidden, not_found, request_timeout, conflict, unprocessable_entity, rate_limited, bad_gateway, service_unavailable, gateway_timeout, internal_server_error — unless a specific endpoint documents its own more precise code for a business-level failure (for example capability_unavailable). Codes are always lowercase snake_case, never SCREAMING_SNAKE_CASE.
Examples
Single Record Success
GET /api/v1/ai/sessions/{id} returns 200 OK:
{
"data": {
"id": "sess-abc-123",
"title": "Research Assistant",
"status": "active",
"createdAt": "2026-01-15T08:00:00.000Z"
}
}Paginated List Success (Offset-based)
GET /api/v1/ai/sessions?limit=2&offset=0 returns 200 OK:
{
"data": [
{
"id": "sess-abc-123",
"title": "Research Assistant",
"status": "active"
},
{
"id": "sess-def-456",
"title": "Developer Pipeline",
"status": "active"
}
],
"meta": {
"limit": 2,
"offset": 0,
"total": 12,
"hasNextPage": true
}
}Cursor-Paginated List Success
GET /api/v1/billing/invoices?limit=2 returns 200 OK:
{
"data": [
{
"id": "inv_001",
"amount": 1999,
"currency": "usd",
"status": "paid",
"createdAt": "2026-03-01T00:00:00.000Z"
},
{
"id": "inv_002",
"amount": 1999,
"currency": "usd",
"status": "paid",
"createdAt": "2026-02-01T00:00:00.000Z"
}
],
"meta": {
"limit": 2,
"hasNextPage": true,
"nextCursor": "eyJpZCI6Imludl8wMDIifQ=="
}
}To fetch the next page, include the cursor: GET /api/v1/billing/invoices?limit=2&cursor=eyJpZCI6Imludl8wMDIifQ==
Validation Error (400)
A request that fails body validation (a missing required field, a value that violates its constraints, or an unrecognized field) returns 400 Bad Request:
{
"error": {
"code": "bad_request",
"message": "password must be shorter than or equal to 128 characters; password should not be empty",
"details": {
"message": [
"password must be shorter than or equal to 128 characters",
"password should not be empty"
],
"error": "Bad Request",
"statusCode": 400
}
},
"meta": {
"requestId": "4dc21131-282f-4c72-b127-f9a4227e266a"
}
}message is every violation joined with ; . details.message carries the same violations as a list of individual, human-readable strings — there is no per-field structured breakdown, so parse details.message if you need to inspect individual failures programmatically.
Rate Limit Error (429)
Endpoints with a rate limit return 429 Too Many Requests once it's exceeded:
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded",
"details": {
"message": "Rate limit exceeded",
"error": "Too Many Requests",
"statusCode": 429
}
},
"meta": {
"requestId": "5aab717f-98ed-471f-bd1e-700752b13622"
}
}The retry delay is not included in the response body — read the Retry-After HTTP header (seconds until you can retry) and the X-RateLimit-Limit / X-RateLimit-Remaining headers.