API pública v0
Cada ficha verificada del registro está disponible como JSON. Sin clave, sin cuenta y sin cuotas que negociar: un endpoint, CORS abierto y respuestas cacheables. Hay un freno por IP contra las ráfagas, lo bastante amplio como para que el uso normal no lo toque nunca. Está pensada para ser la semilla de un estándar abierto, así que el contrato es pequeño y estable.
La API se estrena con la plataforma del registro. El contrato v0 aquí documentado es el del lanzamiento; después llegará una v1 con endpoints de listado y búsqueda. La v0 seguirá funcionando sin cambios cuando llegue la v1.
URL base
Sección titulada «URL base»https://platform.thehumanbehind.com/api/v0
Endpoint
Sección titulada «Endpoint»GET https://platform.thehumanbehind.com/api/v0/records/{slug}.json
sluges el identificador público de la ficha, el último segmento de la URL de su página (/r/<slug>). Se compara sin distinguir mayúsculas (se pasa a minúsculas antes de la búsqueda).- Método
GET. Sin autenticación, sin clave de API. Un preflight CORS
OPTIONSse responde con204y las mismas cabeceras de origen abierto.
Cabeceras de la respuesta
Sección titulada «Cabeceras de la respuesta»Toda respuesta, correcta o de error, lleva estas cabeceras:
| Cabecera | Valor |
|---|---|
content-type | application/json; charset=utf-8 |
access-control-allow-origin | * |
access-control-allow-methods | GET, OPTIONS |
cache-control | public, max-age=300 |
CORS:
access-control-allow-origin: *, puedes llamarla directamente desde un navegador en cualquier sitio.Caché:
cache-control: public, max-age=300. Las respuestas se cachean en el borde durante cinco minutos. El objetivo no es la frescura perfecta, sino la verificabilidad.
Petición de ejemplo
Sección titulada «Petición de ejemplo»curl https://platform.thehumanbehind.com/api/v0/records/laia.json
La forma de una respuesta 200 OK, application/json:
{
"registry_code": "THB-2026-00001",
"slug": "laia",
"avatar_name": "Laia",
"type": "avatar",
"scope": "general",
"operates_at": null,
"responsible_name": "AIGiner S.L.",
"status": "active",
"verification_level": "verified",
"verified_at": "2026-10-15T16:12:39.438015+00:00",
"registered_at": "2026-07-30T23:44:44.547749+00:00",
"content_hash": "23ec5dae1e4b330a1ebe266db49bf2d0ca88b7bb6fff0e0eeca1106bbbc5fb6d",
"url": "https://platform.thehumanbehind.com/r/laia",
"disputed_at": null
}
Ejemplo trabajado, con la forma exacta de una respuesta real. La API pública solo sirve fichas
verificadas, y la verificación de identidad todavía no está abierta, así que hoy el endpoint responde
404 para cualquier slug, y su cuerpo dice exactamente por qué (mira Códigos de estado).
Es el registro funcionando como está diseñado, no una caída: una ficha gratuita es privada, y solo la
verificación la hace pública.
La respuesta contiene únicamente campos públicos. El correo del titular y los datos de su cuenta no forman parte de la superficie de la API, de forma estructural, no solo por política: la API lee de una vista de base de datos que sencillamente no los contiene.
Campos de la respuesta
Sección titulada «Campos de la respuesta»| Campo | Tipo | Notas |
|---|---|---|
registry_code | string | El número THB, p. ej. THB-2026-00001. |
slug | string | El identificador público de la ficha. |
avatar_name | string | El nombre registrado. |
type | string |
|
scope | string |
|
operates_at | string o null | Dónde opera, o null si no se declaró. |
responsible_name | string | La persona que responde por él. |
status | string | Estado interno del ciclo de vida, |
verification_level | string | registered o verified. |
verified_at | string o null | Fecha de verificación en ISO-8601 UTC, o null. |
registered_at | string | Marca de tiempo del alta en ISO-8601 UTC. |
content_hash | string | SHA-256 del snapshot del alta, en hex minúsculas. |
url | string | Enlace canónico a la página legible por humanos. |
disputed_at | string o null | Fecha ISO-8601 UTC en que alguien discutió esta ficha, o
|
Dos campos merecen nota. verification_level te dice qué sello
lleva la ficha; cuando es verified, verified_at
lleva la fecha (ver Verificación).
Y content_hash es lo que hace posible la verificación
independiente: recalcúlalo con los propios campos de esta respuesta usando la
receta pública exacta.
El historial de una ficha
Sección titulada «El historial de una ficha»Una ficha no es un solo momento. Cada cambio en los ocho campos sellados escribe una versión, y cada versión conserva su huella y su fecha, ancladas las dos en Bitcoin. La respuesta de la ficha lleva un resumen de ese historial, y hay un endpoint dedicado para el historial entero:
GET https://platform.thehumanbehind.com/api/v0/records/{slug}/history.json
Las mismas reglas que el endpoint de arriba: CORS abierto, sin clave, cinco
minutos de caché, y un 404 cuyo cuerpo es byte a byte el que ya conoces, así
que preguntar por el historial de una ficha privada dice exactamente tan poco como
preguntar por la ficha.
El objeto history que viene dentro de la respuesta de la ficha es el resumen, y
llega como mucho con las veinte entradas más recientes. Sus dos contadores se
llaman metadata_versions_included e instruction_versions_included, y el
sufijo no es adorno: cuentan lo que ha venido en esa respuesta, no cuántas
versiones tiene la ficha. Cuando truncated es true hay más, y url apunta al
endpoint de arriba, que es el que pagina.
| Campo | Tipo | Notas |
|---|---|---|
versions[].kind | string | metadata (el contenido de la ficha) o instructions (las de un agente). |
versions[].version | number | La 1 es el alta. Los números no se reutilizan nunca. |
versions[].hashed_at | string | Cuándo se selló esa versión. |
versions[].content_hash | string | Solo en las versiones de contenido: la huella de ESA versión. |
versions[].snapshot | object | Solo en las versiones de contenido: el objeto exacto que se hasheó. Es lo que permite a cualquiera recomputar la huella de una versión antigua sin cuenta y sin fiarse de la fila que servimos hoy. |
truncated / next_offset | bool / number | Se pagina con ?offset= y ?limit=. |
Por qué aquí las instrucciones no llevan huella
Sección titulada «Por qué aquí las instrucciones no llevan huella»En una ficha de tipo agent, el historial dice cuántas veces cambiaron las
instrucciones y cuándo. No publica el texto, que es de quien lo escribió, y
tampoco publica su SHA-256.
Lo segundo es deliberado y merece explicarse, porque un hash parece que protege. Solo protege cuando su entrada es difícil de adivinar. Las instrucciones de un agente suelen ser plantillas que se repiten entre clientes, así que publicar el digest permitiría a cualquiera con una plantilla candidata confirmar que esa ficha la usa: un oráculo de confirmación, no una salvaguarda. Es el mismo fallo que tuvo la primera receta de anclaje del registro, y el motivo de que se sustituyera en un día.
La huella sí viaja al dueño de la ficha, en su constancia descargable y en su panel, donde es suya para compartirla o no.
Códigos de estado
Sección titulada «Códigos de estado»| Código | Significado | Cuerpo |
|---|---|---|
200 | Ficha encontrada y pública. | El objeto JSON de arriba. |
404 | No hay ficha pública con ese slug: nunca existió, o no es visible públicamente (solo registrada, despublicada o en revisión). El cuerpo es idéntico en todos esos casos, así que la API nunca revela si existe una ficha privada. | {
"error": "not_found",
"reason": "not_public_until_verified",
"message": "The public API only serves verified records. Identity verification is not open yet.",
"docs": "https://thehumanbehind.com/docs/api/"
} |
429 | Demasiadas peticiones desde una IP en un minuto. La caché de cinco minutos no cubre a quien varía la query string, así que debajo hay un freno. Espera y reintenta. | {"error":"rate_limited"} |
500 | Error inesperado del servidor. | { "error": "server_error" } |
204 | Preflight CORS (OPTIONS). | Vacío. |
Las fichas despublicadas y la API JSON. El endpoint JSON
devuelve 404 para una ficha despublicada, porque ya no está en
la vista pública. El aviso fechado 410 Gone, el que mantiene la
constancia, vive en la página legible por humanos /r/<slug>,
no en el endpoint JSON. El número THB no se reutiliza en ningún caso.
Desde JavaScript
Sección titulada «Desde JavaScript»const res = await fetch(
"https://platform.thehumanbehind.com/api/v0/records/laia.json",
);
const ficha = await res.json();
console.log(ficha.registry_code); // "THB-2026-00001"
console.log(ficha.url); // enlace a la página legible por humanos
Verificar el hash
Sección titulada «Verificar el hash»La razón de ser de la API es que no tengas que fiarte de ella. Descarga una
ficha y recalcula su content_hash tú mismo con la
receta pública. Aquí lo tienes de
principio a fin en Python (solo librería estándar):
import json, hashlib, urllib.request
SLUG = "laia"
url = f"https://platform.thehumanbehind.com/api/v0/records/{SLUG}.json"
# Con User-Agent propio: el de urllib por defecto ("Python-urllib/3.x") lo
# rechaza el Browser Integrity Check de Cloudflare con un 403. Cualquier
# cadena vale; identificarse es lo educado.
req = urllib.request.Request(url, headers={"User-Agent": "thb-verify/1.0"})
rec = json.load(urllib.request.urlopen(req))
# The eight snapshot fields; a missing operates_at becomes "".
fields = ["registry_code", "slug", "avatar_name", "type",
"scope", "operates_at", "responsible_name", "registered_at"]
snapshot = {k: (rec.get(k) or "") for k in fields}
# The timestamp is canonicalised before hashing: the registry seals
# "...862834Z", while the API returns the raw value "...862834+00:00".
# Skip this line and the digest will not match.
snapshot["registered_at"] = snapshot["registered_at"].replace("+00:00", "Z")
# Canonical form = PostgreSQL jsonb text: keys sorted by (length, then bytes),
# ": " after each key and ", " between pairs.
ordered = dict(sorted(snapshot.items(), key=lambda kv: (len(kv[0]), kv[0])))
canonical = json.dumps(ordered, separators=(", ", ": "), ensure_ascii=False)
digest = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
print(digest == rec["content_hash"]) # True
Versionado
Sección titulada «Versionado»El v0 de la ruta es una promesa: este contrato no cambiará bajo
tus pies. Las nuevas capacidades (listado, búsqueda, filtros) llegarán como
v1 en sus propias rutas, y las consultas de ficha por
v0 seguirán devolviendo exactamente la forma aquí documentada.
Uso razonable
Sección titulada «Uso razonable»La API es gratuita para cualquier uso razonable: incrustar una comprobación del sello en tu web, construir un buscador, investigar. Las respuestas se cachean en el borde; por favor, no bombardees el origen con tráfico que esquive la caché. Si necesitas acceso masivo o endpoints de listado, háblanos. Para eso está la v1.