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
{
"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
{
"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
| Field | Required | Notes |
|---|---|---|
code | Yes | Stable UPPER_SNAKE_CASE identifier. Branch your client on this one. It does not change between versions nor when the message is rewritten. |
legacyCode | No | Numeric code from the v5 catalog. Present only when a bridge exists; codes introduced in v6 omit it. |
type | Yes | Address that explains the error: when you open it, it takes you to its entry in the error catalog. It always has the form https:. |
message | Yes | Text for whoever integrates. Depending on the service it comes in Spanish or English, and it may change: do not compare it or branch on it, branch on code. Show it to engineers, not to end users. |
details | Yes (may be empty) | Array of objects with structured context. Most codes leave it empty; validation errors fill it with per-field issues. |
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-Idheader. 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}`)
}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']}")<?php
$body = json_decode($raw, true);
if ($body['success']) {
echo $body['data']['pong'];
} else {
$code = $body['error']['code'];
if ($code === 'RATE_TPS_EXCEEDED') {
sleep((int) ($headers['retry-after'] ?? 5));
return retry();
}
throw new RuntimeException($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.tells your application which failure happened. Use it for product-level decisions: re-authenticate, refresh, show a specific screen.code
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. 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
dataormeta(additive) and existing ones keep their type. successand the presence ofdataversuserrorare mutually consistent: one is always there, the other always absent.meta.is unique per request and safe to log.requestId - The
error.values are listed in the error catalog and are never reused nor renamed.code