← Developers

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

  1. Generator
  2. Clearance API
  3. Decision
  4. License
  5. Payout 85 / 15
  6. 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.

Header
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
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"
  }'
Response 200
{
  "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
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"
Response 201
{
  "license_id": "LIC-7T2W9P4N",
  "consent_id": "CNS-LUMEN-4H8K2D",
  "proof_url": "https://thepersonagrata.com/proof/PRF-6Y3B8R1M",
  "payment_status": "pending"
}

Full flow in JavaScript

fetch
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 body
{ "error": { "code": "rate_limited", "message": "Too many requests. Retry after 60 seconds." } }
StatusCodeMeaning
400invalid_requestBody is not valid JSON or a field is missing or malformed.
401unauthorizedMissing, invalid or revoked API key.
403forbiddenThe key is valid but may not perform this action.
404unknown_offerThe license offer does not exist or belongs to another organisation.
409offer_expiredThe license offer expired or was already accepted.
429rate_limitedMore than 60 requests per minute.
500server_errorUnexpected 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.

Verify a signature (Node.js)
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)
}