Public API v0
Every verified record in the registry is available as JSON. No key, no account, no quota to negotiate: one endpoint, open CORS, cache-friendly. There is a per-IP brake against bursts, generous enough that ordinary use never meets it. It is designed to be the seed of an open standard, so the contract is small and stable.
The API ships with the registry platform. The v0 contract documented here is the launch contract; a v1 with list and search endpoints will follow. v0 will keep working unchanged when v1 arrives.
Base URL
Section titled “Base URL”https://platform.thehumanbehind.com/api/v0
Endpoint
Section titled “Endpoint”GET https://platform.thehumanbehind.com/api/v0/records/{slug}.json
slugis the record’s public identifier, the last segment of its page URL (/r/<slug>). It is matched case-insensitively (it is lowercased before lookup).- Method
GET. No authentication, no API key. A CORS preflight
OPTIONSis answered with204and the same open-origin headers.
Response headers
Section titled “Response headers”Every response, successful or not, carries the content type and the CORS headers below. The cache header is the one that differs: see the note under the table.
| Header | Value |
|---|---|
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: *, so you can call it straight from a browser on any site.Cache: a
200comes back withcache-control: public, max-age=300and is edge-cached for five minutes. Perfect freshness is not the point; verifiability is. A404, on the other hand, comes back withcache-control: no-store, and on purpose: a record that becomes verified must appear the moment it does, not five minutes later, and caching “this does not exist” is how a new record stays invisible to whoever is checking it.
Example request
Section titled “Example request”curl https://platform.thehumanbehind.com/api/v0/records/laia.json
The shape of a 200 OK response, 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
}
Worked example, with the exact shape of a real response. The public API only serves
verified records, and identity verification is not open yet, so today the endpoint answers
404 for every slug, and its body says exactly why (see Status codes). That is the
registry working as designed, not an outage: a free record stays private, and only verification makes
it public.
The response contains only public fields. The owner’s email and account data are not part of the API surface, structurally, not just by policy: the API reads from a database view that simply does not contain them.
Response fields
Section titled “Response fields”| Field | Type | Notes |
|---|---|---|
registry_code | string | The THB number, e.g. THB-2026-00001. |
slug | string | The record’s public identifier. |
avatar_name | string | The registered name. |
type | string |
|
scope | string |
|
operates_at | string or null | Where it operates, or null if not declared. |
responsible_name | string | The person who answers for it. |
status | string | Internal lifecycle state, |
verification_level | string | registered or verified. |
verified_at | string or null | ISO-8601 UTC date of verification, or null. |
registered_at | string | ISO-8601 UTC registration timestamp. |
content_hash | string | Lowercase hex SHA-256 of the registration snapshot. |
url | string | Canonical link to the human-readable record page. |
disputed_at | string or null | ISO-8601 UTC date on which someone contested this record, or
|
Two fields deserve a note. verification_level tells you which
seal the record carries; when verified, verified_at
carries the date (see Verification).
And content_hash is what makes independent verification
possible: recompute it from the very fields in this response using the
exact public recipe.
The history of a record
Section titled “The history of a record”A record is not a single moment. Every change to the eight sealed fields writes a version, and each version keeps its own hash and its own date, both anchored in Bitcoin. The record response carries a summary of that history, and there is a dedicated endpoint for the whole of it:
GET https://platform.thehumanbehind.com/api/v0/records/{slug}/history.json
Same rules as the endpoint above: open CORS, no key, cached five minutes, and a
404 whose body is byte for byte the one you already know, so asking about
the history of a private record tells you exactly as little as asking about the
record.
The history object inside the record response is the summary, capped at the
twenty newest entries. Its two counters are named
metadata_versions_included and instruction_versions_included, and the
suffix is not decoration: they count what came back in that response, not
how many versions the record has. When truncated is true there are more,
and url points at the endpoint above, which is the one that paginates.
| Field | Type | Notes |
|---|---|---|
versions[].kind | string | metadata (the record’s content) or instructions (an agent’s). |
versions[].version | number | 1 is the registration. Numbers are never reused. |
versions[].hashed_at | string | When that version was sealed. |
versions[].content_hash | string | Content versions only: the hash of THAT version. |
versions[].snapshot | object | Content versions only: the exact object that was hashed. This is what lets anyone recompute an old version’s hash without an account and without trusting the row we serve today. |
truncated / next_offset | bool / number | Paginate with ?offset= and ?limit=. |
Why the instructions have no fingerprint here
Section titled “Why the instructions have no fingerprint here”For a record of type agent, the history says how many times the instructions
changed and when. It does not publish the text, which belongs to whoever wrote
it, and it does not publish the text’s SHA-256 either.
That second one is deliberate and worth explaining, because a hash looks like it protects. It only does when its input is hard to guess. Agent instructions are frequently templates that repeat across clients, so publishing the digest would let anyone holding a candidate template confirm that this record uses it: a confirmation oracle, not a safeguard. It is the same failure the registry’s first anchoring recipe had, and the reason it was replaced within a day.
The fingerprint does travel to the record’s owner, in their downloadable proof and in their panel, where it is theirs to share or not.
Status codes
Section titled “Status codes”| Status | Meaning | Body |
|---|---|---|
200 | Record found and public. | The JSON object above. |
404 | No public record with that slug: it never existed, or it is not publicly visible (only registered, unpublished or under review). The body is identical in every one of those cases, so the API never reveals whether a private record exists. | {
"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 | Too many requests from one IP in a minute. The five-minute cache does not cover a caller who varies the query string, so there is a brake underneath it. Wait and retry. | {"error":"rate_limited"} |
500 | Unexpected server error. | { "error": "server_error" } |
204 | CORS preflight (OPTIONS). | Empty. |
Unpublished records and the JSON API. The JSON endpoint
returns 404 for a record that was unpublished, because it is
no longer in the public view. The dated 410 Gone notice, the
one that keeps the trace, lives on the human-readable record page
/r/<slug>, not on the JSON endpoint. The THB number is
never reused either way.
From JavaScript
Section titled “From JavaScript”const res = await fetch(
"https://platform.thehumanbehind.com/api/v0/records/laia.json",
);
const record = await res.json();
console.log(record.registry_code); // "THB-2026-00001"
console.log(record.url); // link to the human-readable page
Verifying the hash
Section titled “Verifying the hash”The whole point of the API is that you do not have to trust it. Fetch a
record and recompute its content_hash yourself with the
public recipe. Here it is end to end
in Python (standard library only):
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
Versioning
Section titled “Versioning”The v0 in the path is a promise: this contract will not change
under you. New capabilities (listing, search, filters) will arrive as
v1 on their own paths, and v0 record lookups will
keep returning exactly the shape documented here.
Fair use
Section titled “Fair use”The API is free for any reasonable use: embedding a seal check on your site, building a lookup tool, research. Responses are edge-cached; please do not hammer the origin with cache-busting traffic. If you need bulk access or list endpoints, talk to us. That is what v1 is for.