Reading and resolving are free and unauthenticated. Writing requires a Registration Agency credential.
GET /id/{id} | Canonical record page (HTML) |
|---|---|
GET /id/{id}.json | Machine-readable record |
GET /id/{id}/history | Append-only change history |
GET /id/{id}/avatar | Avatar image, served by APR |
GET /p/{slug} | Human-friendly alias — 302 to the canonical page |
GET /changes?since={cursor} | Ordered change feed — the sync primitive |
GET /healthz | Liveness |
curl https://artificialpeoples.com/id/3F7K9Q2MW6TN.json
Input is normalised before validation: hyphens and whitespace are stripped,
input is uppercased, and I/L map to 1 and
O to 0. A valid non-canonical form redirects (301) to
the canonical URL rather than rendering at two addresses.
Every record carries a subject_type:
persona (the default), voice,
visual_likeness, character, or work_set.
Omit it on registration and you get persona.
Active records carry a relations array, in both directions — from
a voice record you see what uses it, not only what it points at:
"relations": [
{
"direction": "incoming",
"type": "uses_voice",
"record_id": "TA765DBB9QFE",
"subject_type": "persona",
"display_name": "Night Choir",
"basis": "license",
"note": null
}
]
Assert one — you must be the agency of record for the source, and both records must be active:
curl -X POST https://artificialpeoples.com/personas/{id}/relations \
-H "Authorization: Bearer $APR_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": "9Q2MW6TN3F7K", "type": "uses_voice", "basis": "license" }'
Retract with DELETE /personas/{id}/relations/{relationId}. The
retraction is soft — the row survives so the history stays legible.
uses_voice | → a voice record |
|---|---|
uses_likeness | → a visual_likeness record |
portrays_character | → a character record |
includes_work_set | → a work_set record |
derived_from | → any record |
succeeds | → a record of the same subject type |
A relation is an assertion by the registering agency. Recording that a persona uses a voice says an agency asserted it — not that a licence exists, is valid, or is in force.
The one thing on a record that is not self-declared. A verification proves a
specific narrow proposition — currently
domain_control_verified: someone able to publish DNS records or web
content for a named domain completed a challenge. It establishes nothing about
identity, authority over any persona, or ownership of any right, and every
result carries that limitation.
The flow is the ACME pattern:
« 1 » create a challenge (attach to a record with persona_id, or leave it
free-standing for a later registration to consume)
curl -X POST https://artificialpeoples.com/verifications \
-H "Authorization: Bearer $APR_KEY" -H "Content-Type: application/json" \
-d '{ "proposition": "domain_control_verified", "method": "dns_txt",
"subject": "performer.example.com", "persona_id": "3F7K9Q2MW6TN" }'
« 2 » publish what the instructions say — a TXT record at
_apr-challenge.performer.example.com (dns_txt), or the token as the body
of https://performer.example.com/.well-known/apr-challenge/{id}
(https_well_known; redirects are not followed)
« 3 » run the check — retryable while DNS propagates
curl -X POST https://artificialpeoples.com/verifications/{id}/check \
-H "Authorization: Bearer $APR_KEY"
Verified results appear in the record's verifications array and
on its page, each naming its proposition, subject, method, dates, and
limitations. Pending challenges are never public. Results expire after a year —
control of a channel changes hands — and an expired verification is displayed as
expired, never silently dropped.
Registering a natural_voice or natural_likeness
record requires consuming a completed, unattached verification in the same
request:
curl -X POST https://artificialpeoples.com/personas \
-H "Authorization: Bearer $APR_KEY" -H "Content-Type: application/json" \
-d '{ "slug": "a-performers-voice", "subject_type": "natural_voice",
"display_name": "A Performer’s Voice", "persona_type": "voice",
"ai_disclosure": "natural", "verification_id": 42,
"bio": "No synthetic derivation without written consent.",
"controller": { "type": "individual", "email": "person@example.com" } }'
Three rules, enforced structurally: no biometric payload ever (these records
may not carry an avatar_url at all); the verification must be the
caller's own, completed, unexpired, and not already consumed; and the payload is
consent posture, not identification. A synthetic voice built from the person's
work links back with a derived_from relation, so the derivation
chain is visible from the performer's own record.
Each JSON response carries a legal_effect code and the
disclaimer text. Branch on status and
legal_effect, not on the presence of a field — a non-active
record returns a reduced representation.
registry_record | Active record: identifier plus submitter-supplied assertions |
|---|---|
identifier_pointer_only | Merged. Carries deprecated_in_favor_of; no profile |
tombstone_only | Withdrawn. Carries the date and a neutral reason category; no profile |
identifier_reserved; personal fields removed | Redacted at a person's request; no profile |
An issued ID always resolves. The only case a well-formed ID returns 404 is one that was never issued.
Responses carry ETag and Last-Modified. Send
If-None-Match and you will get a 304 when nothing has
changed. Treat the ETag as opaque. It is a cache validator, not
a version: it also covers state that is not an event — verifications lapse on
the clock, so a representation can change while record_version
stands still. For PATCH concurrency read the record_version field
from the JSON body.
Poll the change feed rather than re-fetching records:
curl "https://artificialpeoples.com/changes?since=0&limit=100"
Records get corrected, merged, withdrawn, and redacted. A stale copy of a
record that was withdrawn for impersonation, or redacted at someone's request,
actively causes the harm the change was meant to stop. If you cache display
fields, refresh within 24 hours and propagate deprecated,
withdrawn, and redacted states promptly.
A badge is a pointer, not a mirror. Always link to the canonical resolver URL.
Bearer credential, issued under a signed agreement.
POST /personas | Register a persona; returns a permanent ID |
|---|---|
PATCH /personas/{id} | Update mutable public fields of your own records |
GET /personas/lookup | Find your own record by external_ref or slug |
POST /personas/{id}/relations | Assert a relation to another record |
DELETE /personas/{id}/relations/{relationId} | Retract one |
POST /verifications | Create a control-verification challenge |
POST /verifications/{id}/check | Run the check (retryable while pending) |
DELETE /verifications/{id} | Revoke a challenge or verification |
curl -X POST https://artificialpeoples.com/personas \
-H "Authorization: Bearer $APR_KEY" \
-H "Content-Type: application/json" \
-d '{
"slug": "night-choir",
"subject_type": "persona",
"display_name": "Night Choir",
"persona_type": "musician",
"ai_disclosure": "fully_synthetic",
"bio": "A synthetic choral ensemble.",
"links": [{ "type": "official_site", "url": "https://example.com" }],
"external_ref": "your-own-stable-id",
"controller": {
"type": "organization",
"display_name": "Sable Labs",
"email": "ops@example.com"
}
}'
controller.email and controller.legal_name are
private: they are stored separately from the public record and never appear in
any response.
external_ref is your own retry key for the registration —
private to you, returned only on authenticated responses. It must be
opaque: a random or surrogate key, never an email address, a name, a username,
or anything else about the subject.
This is the one submitted field an erasure deliberately keeps. Clearing it
would make /personas/lookup miss on an erased record and invite
your own retry to register that person a second time — so retention protects
the subject, but only while the value says nothing about them. A UUID is the
right shape.
Registration issues a permanent identifier, so a blind retry is
dangerous. Send external_ref on every registration. If a retry
comes back 409 slug_taken or 409 external_ref_taken,
call GET /personas/lookup?external_ref=… — it answers only for your
own records — and confirm the existing record is your registration before
adopting it.
Send If-Match: <record_version> as a plain integer to make
an update a compare-and-set. On mismatch nothing is applied and you get
409 version_conflict with current_version. Without the
header the update applies unconditionally.
Read record_version from the response body — it
is returned on the 201, on /personas/lookup, and on
/id/{id}.json. Never derive it from an ETag.
The two are deliberately different things. If-Match on PATCH
takes a bare integer; an ETag is a quoted, opaque validator that
also covers state which is not an event — verifications lapse with the clock, so
the representation changes while record_version stands still.
Stripping the quotes gives you something like 7.0, which
If-Match rejects with 400 bad_if_match.
Merges, withdrawals, and redactions are registry operations, not agency ones, and verification state is never agency-writable. Request them through the correction and challenge process.
401 unauthorized | Missing or unknown credential |
|---|---|
403 not_agency_of_record | The record was registered by another agency |
409 slug_taken / external_ref_taken | Retry conflict — use lookup |
409 version_conflict | If-Match did not match |
409 record_not_active / record_redacted | Not updatable in its current state |
422 invalid_slug / invalid_url | Failed a semantic rule; detail names it |
422 invalid_relation / invalid_target | Relation points at the wrong subject type, or at itself |
409 relation_exists / target_not_active | Edge already asserted, or the other end is not active |
422 invalid_subject / verification_required / verification_not_usable / avatar_not_permitted | Verification rules; detail names the specific problem |
409 too_many_pending / challenge_expired | Complete or revoke existing challenges; expired ones cannot be revived |
429 | Rate limited — back off |
The contract is append-only: fields and error codes may be added, none is repurposed or removed. Treat unknown fields as forward compatibility. A machine-readable OpenAPI description is planned.