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
{ "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
{ "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
| Field | Required | Notes |
|---|---|---|
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.
| Field | Notes |
|---|---|
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.codetells 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".
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
dataormeta(additive), existing fields keep their type. successand the presence/absence ofdatavserrorare mutually consistent: one is always present, the other is always absent.meta.requestIdis unique per request and safe to log.error.codevalues are listed in the error catalog and never reused or renamed.