Human ID
Developer API

Ask before you render.

One call answers whether a person's face or voice is licensed for what you are about to generate — for this company, this purpose, this platform, this territory, right now. The answer is derived from the signed license itself, so it changes the moment consent does.

API keys

Sign in with your wallet to register an integration and mint a key. A key is shown once and stored only as a hash — we cannot show it to you again, and neither can anyone else.

The shape of an integration

Two calls, at two different moments. Everything else is detail.

  1. Before you generate — POST /api/v1/authorize with the Human ID, the likeness you need, and what you intend to do with it.
  2. If allowed is false, stop. The reason says whether this is fixable — a platform outside the grant is — or final.
  3. If allowed is true, keep the licenseId. It is the document your permission stands on.
  4. After you publish — POST /api/v1/usage with that licenseId and what you made. That is the record the person whose face it is gets to read.

An integration that only ever calls /authorize leaves a permission trail with no usage in it. The person learns that somebody asked, not that anything was made.

Authentication

Every /api/v1 request carries a key. Send it as a bearer token, or in X-API-Key where a custom header is easier to set.

Authorization: Bearer hid_sk_8f3a92…
# or, where a custom header is easier to set:
X-API-Key: hid_sk_8f3a92…

A key is shown once, at the moment it is minted, and stored only as a SHA-256 digest. No endpoint can read one back: a lost key is replaced, not recovered.

Keys carry scopes — authorize, read and usage. An integration that only checks permissions should not be able to file usage against somebody's license, and a scoped key cannot.

Rate limits

Per key, per minute, in a fixed window. Every response carries the remaining budget, not only the ones that refuse you: a client that first hears about a limit when it hits it has no way to slow down before it does.

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 1788000060

# and, on a refusal:
HTTP/1.1 429
Retry-After: 12
{ "error": { "code": "RATE_LIMITED", "message": "…" } }

Refused calls are recorded and then left out of the count, so retrying against a closed door cannot extend your own lockout. The window is counted from the request log itself, which makes the limit and the audit trail the same fact seen twice.

New keys start at 60 requests per minute.

POST

/api/v1/authorize

scope: authorize

The question this platform exists to make answerable. Every field in the body is a claim about what you intend to do, and none of them can widen a permission: sending aiGeneration false does not switch AI generation on, it says you are not doing it, and the license still decides.

platform and territory are optional, and an omitted one is skipped rather than failed — a caller who did not name a platform did not ask about one. Sending them is what makes the answer specific. A refusal about the license is a 200 with allowed false; a 4xx means the request could not be understood, and an unknown Human ID is a 404 rather than a quiet no.

curl -X POST https://your-host/api/v1/authorize \
  -H "Authorization: Bearer hid_sk_…" \
  -H "Content-Type: application/json" \
  -d '{
    "humanId": "HID-A82F91",
    "assetType": "face",
    "company": "Nike",
    "purpose": "commercial_advertising",
    "platform": "instagram",
    "territory": "KZ",
    "aiGeneration": true,
    "modelTraining": false
  }'

{
  "allowed": true,
  "licenseId": "HID-2026-8F3A92",
  "expiresAt": "2026-11-19T00:00:00.000Z",
  "reason": "VALID_LICENSE",
  "detail": "",
  "humanId": "HID-A82F91",
  "assetType": "face",
  "checkedAt": "2026-08-28T09:41:02.118Z",
  "verifyUrl": "https://your-host/verify/HID-2026-8F3A92"
}

purpose is a closed list. An unrecognised value is refused rather than guessed at, because the only guess available is a permissive one — made silently, on the exact question the license exists to settle.

commercial_advertising
brand_endorsement
entertainment
editorial
education
research
internal_testing
other
GET

/api/v1/license/:id

scope: read

The document behind an answer: what it grants, where, until when, and the two hashes that let you check it has not moved since you last read it. expiresAt is the term actually in force after any renewal; signedExpiration is the date on the original signed document, which never changes.

curl https://your-host/api/v1/license/HID-2026-8F3A92 \
  -H "Authorization: Bearer hid_sk_…"

{
  "license": {
    "licenseId": "HID-2026-8F3A92",
    "status": "ACTIVE",
    "asset":       { "face": true, "voice": false },
    "permissions": { "aiGeneration": true,
                     "modelTraining": false,
                     "commercialAdvertising": true,
                     "sublicensing": false },
    "platforms": ["Instagram", "TikTok"],
    "territories": "Kazakhstan, CIS",
    "startDate": "2026-08-21T00:00:00.000Z",
    "expiresAt": "2026-11-19T00:00:00.000Z",
    "signedExpiration": "2026-11-19T00:00:00.000Z",
    "version": 1,
    "revokedAt": null,
    "pausedAt": null,
    "documentHash": "<64 hex>",
    "scopeHash": "<64 hex>",
    "anchored": { "txSignature": "…", "network": "devnet" },
    "verifyUrl": "https://your-host/verify/HID-2026-8F3A92"
  },
  "inForce": true
}
GET

/api/v1/human/:humanId/status

scope: read

Two things /authorize deliberately does not tell you. Whether this person has actually been checked — a Human ID is free to claim and a verified one is not. And their standing position on AI use of their likeness, which is what they say to somebody who has not asked yet.

That standing position never overrides a license. A signed document is an instrument and a passport stance is a disposition; letting the second void the first would mean a forgotten toggle could cancel a contract. Read it as context before you negotiate, not as permission.

curl https://your-host/api/v1/human/HID-A82F91/status \
  -H "Authorization: Bearer hid_sk_…"

{
  "humanId": "HID-A82F91",
  "rightsStatus": "LICENSABLE",
  "verified": true,
  "checks": {
    "identity": { "status": "VERIFIED", "demo": false },
    "face":     { "status": "VERIFIED", "demo": false },
    "voice":    { "status": "UNVERIFIED", "demo": false }
  },
  "permissions": {
    "commercial":    "ASK_FIRST",
    "ai_training":   "PROHIBITED",
    "advertising":   "ALLOWED",
    "entertainment": "ASK_FIRST"
  },
  "licenses": { "active": 2, "paused": 0, "expired": 1, "revoked": 0 },
  "registeredAt": "2026-08-21T11:02:44.000Z",
  "profileUrl": "https://your-host/human/HID-A82F91"
}
POST

/api/v1/usage

scope: usage

The other half of asking. File one receipt per piece of content you generated, pointing at the licenseId /authorize handed you. The verdict is derived from the license rather than from your body — there is no field here that can make a receipt say it was authorized.

The license has to name either your registered developer name or the company in the request as its agency or client. That is attribution rather than proof: a platform files on behalf of its customers, so what closes the gap is that every receipt permanently records which developer filed it.

curl -X POST https://your-host/api/v1/usage \
  -H "Authorization: Bearer hid_sk_…" \
  -H "Content-Type: application/json" \
  -d '{
    "licenseId": "HID-2026-8F3A92",
    "company": "Nike",
    "contentTitle": "Summer campaign — 15s cutdown",
    "contentType": "VIDEO",
    "assetType": "FACE",
    "purpose": "commercial_advertising",
    "platform": "Instagram",
    "territory": "Kazakhstan",
    "contentUrl": "https://example.com/ad/8f3a92",
    "contentHash": "<sha256 of the file>",
    "aiModel": "internal-diffusion-3",
    "generatedAt": "2026-09-02T10:14:00.000Z",
    "publishedAt": "2026-09-03T08:00:00.000Z"
  }'

{
  "receiptId": "RCP-8F3A92C1",
  "status": "AUTHORIZED",
  "receiptHash": "<64 hex>",
  "licenseId": "HID-2026-8F3A92",
  "humanId": "HID-A82F91",
  "url": "https://your-host/receipt/RCP-8F3A92C1"
}

Reasons

Ten, and only ten. Switch on the reason, never on the sentence beside it.

VALID_LICENSE
A license covers this use, and it is in force today.
NO_LICENSE
No license names this company for this likeness — or the one that does is not signed by both parties, or its term has not started yet.
LICENSE_EXPIRED
The term has ended. What follows an expiry is a new license, not an extension.
LICENSE_REVOKED
Consent was withdrawn. Terminal — nothing walks this back.
LICENSE_PAUSED
Consent is suspended. Reversible, and not your call to make.
PLATFORM_NOT_ALLOWED
The license names platforms, and this is not one of them.
TERRITORY_NOT_ALLOWED
The license names territories, and this is not one of them.
COMMERCIAL_USE_NOT_ALLOWED
The license does not permit commercial advertising.
AI_GENERATION_NOT_ALLOWED
The license does not permit AI-generated content.
MODEL_TRAINING_NOT_ALLOWED
The license does not permit training a model on this likeness.

When several licenses could apply, one that permits the use wins outright. Otherwise you get the refusal from the license that came closest, rather than whichever row the database returned first.

Errors

One envelope, one code per failure. The message is for the human reading your log; the code is what your program branches on.

400  INVALID_REQUEST     the body did not parse, or a field is wrong
401  UNAUTHORIZED        no key, or a key that is revoked, expired or unknown
403  FORBIDDEN           the key lacks the scope this endpoint needs
403  SUSPENDED           the developer account has been suspended
404  UNKNOWN_HUMAN_ID    no such Human ID
404  UNKNOWN_LICENSE     no such license
429  RATE_LIMITED        too many calls this minute
500  SERVER_ERROR        our fault; the message never carries internals

{ "error": { "code": "INVALID_REQUEST",
             "message": "purpose must be one of …",
             "fields": [ { "field": "purpose", "message": "…" } ] } }

What the API never returns

Nothing personal, on any endpoint, for any key. This is a machine surface a key holder can call in a loop, which makes it a different thing from the public certificate at /verify — that page does show the parties, to somebody handed one license identifier who opened it deliberately.

  • No names — not the person's, not the agency's, not the client's.
  • No wallet addresses, no email, no country, no photo.
  • No fee, no campaign name, no restrictions text.
  • No biometric data of any kind, and no reference face or voice hashes.

The request log keeps the route pattern rather than the URL, so no identifier reaches it through a query string, and the caller's address only as a salted digest. Entries are kept for thirty days.