Skip to content

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 ​

typescript
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 ​

typescript
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 ​

typescript
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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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.

Built with purpose.