Likeness Clearance API · Beta
Content ID for faces and voices
Ask Persona Grata before you generate a real person. Every answer comes from an identity-verified member, and a yes can be turned into a paid license with a public proof in one more call. This documentation is in English for all developers.
Overview
- Generator
- Clearance API
- Decision
- License
- Payout 85 / 15
- Proof
Authentication
Send your organisation's API key as a bearer token. Create and revoke keys in the developer console under Clearance. Keys are shown once and stored only as a hash.
Authorization: Bearer pgc_live_…Demo key for the fictional demo set: pgc_demo_lumen_video_ai. It only answers for fictional members such as Maya Laurent (PG-7F3A9C) and Jonas Brandt (PG-2B81D4).
POST /api/v1/clearance
The subject is one of { "pg_id" }, { "handle", "platform" } or { "face_match_token" }. The use is face_generation, voice_generation, ai_training or advertising.
face_match_token is reserved for biometric matching partners and on the roadmap. It is accepted today and always returns unknown_person.
curl -X POST https://thepersonagrata.com/api/v1/clearance \
-H "Authorization: Bearer pgc_demo_lumen_video_ai" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: gen-2026-000123" \
-d '{
"subject": { "pg_id": "PG-7F3A9C" },
"use": "face_generation",
"territory": ["EU"],
"duration_days": 30,
"requester_reference": "job-000123"
}'{
"clearance_id": "CLR-8K2M4Q7Z",
"decision": "license_required",
"pg_id": "PG-7F3A9C",
"identity_verified": true,
"license_offer": {
"id": "OFR-3J9X1C5V",
"price_cents": 50000,
"currency": "EUR",
"expires_at": "2026-11-03T10:00:00.000Z",
"terms_url": "https://thepersonagrata.com/terms/likeness-license"
},
"checked_at": "2026-10-27T10:00:00.000Z"
}Decision values
- allowed
- The person allows this use without a license. You may generate within the requested territory and duration.
- license_required
- The person licenses this use. A license_offer with price and expiry is included. Accept it before generating.
- not_permitted
- The person forbids this use. Do not generate. Political use is always not_permitted.
- unknown_person
- No identity-verified member matches the subject. This is not a permission. Apply your own policy.
- pending_manual_approval
- The person reviews each request. You receive the webhook clearance.decided when they decide.
POST /api/v1/license-offers/{id}/accept
Accepting an offer creates the license, a consent entry in the talent's ledger, a payment and a public campaign proof. Persona Grata keeps 15 percent, the talent receives 85 percent. In test mode the payment status stays pending until it is confirmed.
curl -X POST https://thepersonagrata.com/api/v1/license-offers/OFR-3J9X1C5V/accept \
-H "Authorization: Bearer pgc_demo_lumen_video_ai" \
-H "Idempotency-Key: accept-OFR-3J9X1C5V"{
"license_id": "LIC-7T2W9P4N",
"consent_id": "CNS-LUMEN-4H8K2D",
"proof_url": "https://thepersonagrata.com/proof/PRF-6Y3B8R1M",
"payment_status": "pending"
}Full flow in JavaScript
const headers = {
Authorization: `Bearer ${process.env.PG_API_KEY}`,
'Content-Type': 'application/json',
}
const clearance = await fetch('https://thepersonagrata.com/api/v1/clearance', {
method: 'POST',
headers: { ...headers, 'Idempotency-Key': 'gen-2026-000123' },
body: JSON.stringify({
subject: { handle: '@mayalaurent', platform: 'instagram' },
use: 'face_generation',
territory: ['EU'],
duration_days: 30,
requester_reference: 'job-000123',
}),
}).then((r) => r.json())
if (clearance.decision === 'license_required') {
const license = await fetch(
`https://thepersonagrata.com/api/v1/license-offers/${clearance.license_offer.id}/accept`,
{ method: 'POST', headers: { ...headers, 'Idempotency-Key': `accept-${clearance.license_offer.id}` } },
).then((r) => r.json())
// { license_id, consent_id, proof_url, payment_status }
console.log(license.proof_url)
} else if (clearance.decision !== 'allowed') {
// not_permitted, unknown_person or pending_manual_approval: do not generate
}Errors
Every error has the same JSON shape.
{ "error": { "code": "rate_limited", "message": "Too many requests. Retry after 60 seconds." } }| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | Body is not valid JSON or a field is missing or malformed. |
| 401 | unauthorized | Missing, invalid or revoked API key. |
| 403 | forbidden | The key is valid but may not perform this action. |
| 404 | unknown_offer | The license offer does not exist or belongs to another organisation. |
| 409 | offer_expired | The license offer expired or was already accepted. |
| 429 | rate_limited | More than 60 requests per minute. |
| 500 | server_error | Unexpected error. Retry with the same Idempotency-Key. |
Rate limits and idempotency
Default limit: 60 requests per minute per organisation, configurable on request. Send an Idempotency-Key header on every POST. Repeating a request with the same key returns the original response and never creates a second license or charge.
Webhooks
Register an HTTPS endpoint in the developer console and choose events. Every delivery is signed with your endpoint secret.
- clearance.requested
- clearance.decided
- license.created
- license.paid
- license.expired
- consent.revoked
- consent.revocation_cancelled
- mandate.granted
- mandate.ended
- mandate.scope_changed
Headers: PG-Timestamp (Unix seconds) and PG-Signature (v1= HMAC-SHA256 of `${timestamp}.${rawBody}`). Reject timestamps older than five minutes. When a talent revokes a license consent, you receive consent.revoked and must stop using the likeness.
import { createHmac, timingSafeEqual } from 'node:crypto'
// Use the raw request body, exactly as received.
export function verifyPersonaGrata(rawBody, headers, secret) {
const timestamp = headers['pg-timestamp']
const signature = headers['pg-signature'] // "v1=<hex>"
if (!timestamp || !signature) return false
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false
const expected = 'v1=' + createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(signature)
return a.length === b.length && timingSafeEqual(a, b)
}