Response envelope

Every Hablame v6 endpoint, success or failure, returns the same three top-level keys: success, exactly one of data / error, and meta. One branch in your client. Zero shape-sniffing.

Successful response

200 OK
{
  "success": true,
  "data": {
    "pong":       true,
    "apiVersion": "v6",
    "account": { "id": 10000003 }
  },
  "meta": {
    "requestId":      "b9b1704baffab21150213c02fd853975",
    "timestamp":      "2026-05-22T19:43:59+00:00",
    "responseTimeMs": 4.24
  }
}

data is the actual payload of the endpoint. The shape varies per endpoint; see the reference for the schema of 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 `.",
    "details":    []
  },
  "meta": {
    "requestId":      "a883f4341b91353de262ce9812d270aa",
    "timestamp":      "2026-05-22T19:00:00+00:00",
    "responseTimeMs": 0.6
  }
}

Error fields

FieldRequiredNotes
code Yes Stable UPPER_SNAKE_CASE identifier. Branch your client on this. Stays the same across versions and message edits.
legacyCode No Numeric code from the v5 catalog. Present only when a bridge exists; codes introduced in v6 omit it. Used by clients still mapping from v5.
type Yes URL to the catalog entry for this code. Opens directly on the right section of /docs/v6/errors.
message Yes Human-readable, English. Safe to surface to engineers. Not safe for end users: translate or generalize before showing.
details Yes (may be empty) Array of structured context objects. Most codes leave it empty; validation errors fill it with per-field issues.

The meta block

Present on every response, success or error.

FieldNotes
requestId Server-side trace id assigned by nginx. Quote it in support tickets: it lets ops find your exact request in the logs.
timestamp ISO-8601 with timezone offset. The server-side moment the response body was built.
responseTimeMs Server-side build time in milliseconds. Excludes network; measures only what we spent processing.
warnings Optional. Array of { code, message } objects. Appears only when the request succeeded but produced non-fatal warnings (e.g. deprecation notices).

Branching your client

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

TypeScript

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);          // narrowed to PingData
} else {
  // branch on the stable code, NOT the HTTP status
  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}`);
}

Python

import time, httpx

r    = httpx.get(url, headers={"Authorization": f"Bearer {key}"})
body = r.json()

if body["success"]:
    print(body["data"]["pong"])
else:
    code = body["error"]["code"]
    if code == "RATE_TPS_EXCEEDED":
        time.sleep(int(r.headers.get("Retry-After", "5")))
        return retry()
    raise RuntimeError(f"{code}: {body['error']['message']}")

HTTP status vs error.code

They serve different purposes:

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

Two different codes can share the same HTTP status. For example, both AUTH_REQUIRED and AUTH_COST_CENTER_DISABLED are 401. The code tells you whether the fix is "add the header" or "ask the admin to re-enable the cost center".

i

Don't parse error.message in code. The wording can change between releases without bumping 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), existing fields keep their type.
  • success and the presence/absence of data vs error are mutually consistent: one is always present, the other is always absent.
  • meta.requestId is unique per request and safe to log.
  • error.code values are listed in the error catalog and never reused or renamed.