Skip to content

Response envelope

Every v6 endpoint, successful or failed, returns the same three top-level keys: success, exactly one of data or error, and meta. One branch in your client, no shape guessing.

5 min readReviewed on

Successful response

200 OK
{
  "success": true,
  "data": {
    "pong": true,
    "apiVersion": "v6",
    "client": { "ip": "203.0.113.45" }
  },
  "meta": {
    "requestId": "b9b1704baffab211",
    "clientIp": "203.0.113.45",
    "timestamp": "2026-05-22T19:43:59+00:00",
    "responseTimeMs": 4.24
  }
}

data is the actual endpoint payload. Its shape varies per endpoint: the reference has the schema for each one.

Error response

401 Unauthorized
{
  "success": false,
  "error": {
    "code": "AUTH_REQUIRED",
    "legacyCode": 40002,
    "type": "https://developers.hablame.co/docs/v6/errors/auth-required",
    "message": "Authentication is required. Send your API key as `Authorization: Bearer <token>`.",
    "details": []
  },
  "meta": {
    "requestId": "a883f4341b91353d",
    "clientIp": "203.0.113.45",
    "timestamp": "2026-05-22T19:00:00+00:00",
    "responseTimeMs": 0.6
  }
}

Error fields

The meta block

Present on every response, successful or error, with these four keys always:

requestId
Request identifier, 16 hexadecimal characters, assigned by the server. It is the same value as the X-Request-Id header. Quote it when you write to support: with it we find your exact request.
clientIp
The IP the API saw your request come from. It helps when your key only accepts certain networks: compare this value with the list.
timestamp
Date and time in ISO 8601 format with a time zone offset. The moment the response body was built.
responseTimeMs
Time the server took to build the response, in milliseconds. It excludes the network: it measures only what we spent processing.

Your client can ignore any new key that shows up in meta or data: new keys are added without notice and existing ones do not change type.

Branching your client

The pattern is the same in any language: check success first, never inspect the HTTP status alone.

type Envelope<T> =
  | { success: true;  data: T;         meta: Meta }
  | { success: false; error: ApiError; meta: Meta }

const res  = await fetch(url, { headers: { Authorization: `Bearer ${key}` } })
const body = (await res.json()) as Envelope<PingData>

if (body.success) {
  console.log(body.data.pong)
} else {
  // ramifica sobre el code estable, NO sobre el status HTTP
  if (body.error.code === 'RATE_TPS_EXCEEDED') {
    const wait = Number(res.headers.get('Retry-After') ?? 5)
    await sleep(wait * 1000)
    return retry()
  }
  throw new Error(`${body.error.code}: ${body.error.message}`)
}

HTTP status versus error.code

They serve different purposes:

  • The HTTP status tells routers, CDNs and middleware whether the response is fine (2xx), recoverable (4xx) or terminal (5xx). Use it for transport-level decisions: retry or fail.
  • error.code tells your application which failure happened. Use it for product-level decisions: re-authenticate, refresh, show a specific screen.

Two different codes can share the same status. For example, AUTH_REQUIRED and AUTH_INVALID_KEY are both 401, but the code tells you whether the fix is "add the header" or "check the key value".

Do not parse error.message in code. The wording can change between releases without breaking the contract; the code is the contract.

Invariants you can rely on

  • The envelope shape never changes between versions of the same endpoint. New fields are added to data or meta (additive) and existing ones keep their type.
  • success and the presence of data versus error are mutually consistent: one is always there, the other always absent.
  • meta.requestId is unique per request and safe to log.
  • The error.code values are listed in the error catalog and are never reused nor renamed.

Need help?

Was this page helpful?

See all support channels