Royall Herald

Royall Connect — connect a person's vault to your product and ask permission before you use their likeness. Read the partner integration guide →
The authorization API for human likeness

Ask Royall whether your platform may use a person's voice or face — right before you generate — and get back ALLOW or BLOCK plus a signed, offline-verifiable clearance. One call: POST /v1/authorize.

Zero ML on the request path An Asset ID in → a consent decision out Ed25519-signed grant Verify offline, no callback curl · Python · Node · Go
How it works. A creator registers their likeness with Royall and gets a public Asset ID (RYL-…). They set which uses they'll allow. Your platform calls /v1/authorize with that Asset ID + your intended use; Herald reads the standing consent and answers without running a model or calling anything else, so it is fast enough to sit in front of a generation. On ALLOW you also get a grant — a portable proof-of-clearance you can embed in your output and anyone can verify against Royall's public key. There is no biometric matching on this path, so it never inherits model-inference latency.

Products

Five partner surfaces, one platform. Herald is documented in full on this page; Royall Connect, the detections feed, voice matching and the song catalogue are specified in the partner integration guide.

Royall Herald  live

The authorization API. One call — POST /v1/authorize with an Asset ID and your intended use; you get back ALLOW or BLOCK plus an Ed25519-signed grant anyone can verify offline.

Start with the quickstart →

Royall Connect  early access

A hosted consent handoff: you send your user to Royall, they consent and record their likeness there, and you get a durable connection you can check on every use.

Read the integration guide →

Detections feed  early access

Powered by Sentinel and delivered through Connect: a feed of things we spotted that look like the person — each carrying a link, a platform, a date, a confidence band, and, once you have registered a catalogue, a work block naming which song we think it renders. No verdict on the person: matches, never accusations.

Read §8 of the Connect guide →

Voice matching  by agreement

Submit a recording and ask whether it is the voice of the person behind an Asset ID. It uses your ordinary rh_live_ Herald key on POST /v1/match — not your Connect keys — but it is enabled per agreement and only for assets your company licenses. It has its own, much tighter allowance than the rest of the API — ask us for your number — and is never available to a sandbox key.

Read §9 of the Connect guide →

Song catalogue  early access

Register the songs you administer, with a reference recording for each. We then match a detection by sound rather than by the uploader's title, and tell you which of your songs it renders — which is what lets you file rather than only demand removal.

Read §12 of the Connect guide →

Sentinel

Sentinel is Royall's monitoring layer. A creator captures their likeness — voice, face, body — in their Royall vault, and Sentinel watches platforms for it from there. Nothing outward happens on a match by default: detections land in the creator's own inbox. Nothing is ever filed on a match alone — a filing is either made by the creator, or sent automatically only where that creator has signed a standing sworn authorization in advance.

What a partner sees of Sentinel. One surface: the detections feed, delivered through Royall Connect and specified in §8 of the integration guide. It is a list of things we spotted that look like the person — it is not a list of things done without their permission. Each detection carries a link, a platform, a date and a confidence band, and no verdict; a status field appears only if the creator chose to share it. You may be shown your own authorized output.

The feed is in early access and off by default: enabling it is a deliberate act on your partner account, and the person must separately agree to it, per connection, on the consent screen. An empty feed is therefore not a clean bill of health — read §8 before you build on it.

Quickstart

The same call in every language. Pick your language once (left) and every snippet on this page follows.

# set your key once — never inline a secret in a shell history
export ROYALL_HERALD_KEY="rh_live_..."

curl -sX POST https://vault.royall.ai/v1/authorize \
  -H "Authorization: Bearer $ROYALL_HERALD_KEY" \
  -H "Content-Type: application/json" \
  -d '{"asset_code":"RYL-V-2026-DEM0V01C-8","use_case":"ads","depth":"synthesis"}'

# -> {"status":"ALLOW","tier":"commercial_synthesis","grant_id":"grant_…","signed_token":"…", ...}
# -> or {"status":"BLOCK","reason":"block:no_offer:commercial_synthesis/ads"}   (BLOCK is HTTP 200, not an error)
# Royall Python SDK — not on a package registry yet; we send you the source (see "SDKs" below). The API is plain HTTP; an SDK is optional.
from royall_herald import HeraldClient

herald = HeraldClient(api_key="rh_live_...")   # base_url defaults to vault.royall.ai

d = herald.authorize("RYL-V-2026-DEM0V01C-8", use_case="ads", depth="synthesis")
if d.allowed:                       # BLOCK is a normal result, not an exception
    proof = d.signed_token
else:
    print("blocked:", d.reason)

assert d.verify(herald.fetch_public_key())    # offline Ed25519 — zero callbacks
// Royall Node SDK — not on a package registry yet; we send you the source (see "SDKs" below). Zero dependencies; Web Crypto for verify.
import { HeraldClient } from "royall-herald";

const herald = new HeraldClient({ apiKey: "rh_live_..." });

const d = await herald.authorize("RYL-V-2026-DEM0V01C-8", "ads", { depth: "synthesis" });
if (d.allowed) {
  const proof = d.signedToken;
} else {
  console.log("blocked:", d.reason);
}

const ok = await d.verify(await herald.fetchPublicKeyRaw());   // offline
// Royall Go SDK — not on a package registry yet; we send you the source (see "SDKs" below). Package: herald. Standard library only.
c, _ := herald.NewClient(herald.Options{APIKey: "rh_live_..."})

d, err := c.Authorize(ctx, "RYL-V-2026-DEM0V01C-8", "ads",
    herald.AuthorizeOptions{Depth: "synthesis"})
if err != nil { /* auth(401) / rate-limit(429) / server(5xx) / network */ }
if d.Allowed() {              // BLOCK is not an error
    proof := d.SignedToken
} else {
    fmt.Println("blocked:", d.Reason)
}

pub, _ := c.FetchPublicKeyRaw(ctx)
ok := d.Verify(pub)                // offline crypto/ed25519

No key yet? Everything below works against the sandbox with an rh_test_ key — no live data required.

Authentication

Every request carries your Herald key as a bearer token:

Authorization: Bearer rh_live_<secret>

Two key kinds

  • rh_live_… — production. Decides against real registered assets + licenses.
  • rh_test_… — sandbox. Runs the real decision logic against a synthetic, fully-licensed asset. Never touches live data.

Keys are issued by your Royall admin. The secret is shown once — store it server-side.

Base URL

All requestshttps://vault.royall.ai

One base URL for everything. To test without touching live data, use an rh_test_ sandbox key against this same URL — same endpoint, synthetic asset.

A missing/malformed/invalid key returns 401 with a detail of missing_bearer_token, invalid_key_format, or invalid_or_revoked_key.

Sandbox

Ask your Royall contact for an rh_test_ key to exercise the endpoint and the SDKs without a live asset, creator, or license. A sandbox key runs the real decision against a synthetic, fully-licensed asset, so your ALLOW/BLOCK handling exercises the real decision logic. Every response carries "sandbox": true. The asset_code you send is ignored — the synthetic menu is fixed:

A sandbox grant is deliberately NOT interchangeable with a production one. This is the one place the sandbox is not a faithful mirror, and it is intentional:

  • The signed payload carries "env": "sandbox". It is inside the signature, so it survives stripping the HTTP envelope.
  • It is signed with a separate key (its kid starts sbx-), published at GET /api/royall-sandbox-pubkey. A verifier holding only /api/royall-pubkey cannot verify it — not "will not", cannot.
  • Every official SDK refuses sandbox grants by default. Accepting one takes an explicit opt-in and the sandbox key: allow_sandbox / allowSandbox / AllowSandbox.

So: do not treat a sandbox grant as clearance, and never add the sandbox key to a production verifier. A test key must not be able to produce something a production system accepts as consent — which is exactly what this separation prevents.

use_casedepthresult
adssynthesisALLOW commercial_synthesis + signed grant
ads / audiobook / gamesinferenceALLOW commercial_inference
gamessynthesisBLOCK block:no_offer:commercial_synthesis/games
any, with abuse_category set to one of the five floor categoriesanyBLOCK block:universal_floor:…
Any other abuse_category value is not a floor hit — the synthetic asset carries no creator exclusions, so it will ALLOW.
# the demo Asset ID is optional — the decision always uses the synthetic menu
export ROYALL_HERALD_KEY="rh_test_..."
curl -sX POST https://vault.royall.ai/v1/authorize \
  -H "Authorization: Bearer $ROYALL_HERALD_KEY" -H "Content-Type: application/json" \
  -d '{"asset_code":"RYL-V-2026-SANDB0X0-N","use_case":"ads","depth":"synthesis"}'
# -> {"status":"ALLOW","tier":"commercial_synthesis","sandbox":true,"signed_token":"…", ...}

POST /v1/authorize

Decide + (on ALLOW) issue a signed grant. Both ALLOW and BLOCK return HTTP 200 — BLOCK is a valid answer, not an error. Read status.

POST /v1/authorize

Request body

fieldtypenotes
asset_codestringrequiredThe Royall Asset ID (RYL-…) of the voice/face/body.
use_casestringrequiredYour intended use-case slug, matched against what the creator offered. Use cases are per-kind — voice: ads, audiobook, narration, podcast, elearning, games, ai_voicebot, documentary; face and body: advertising, editorial, publishing, corporate, film, avatar; either: research, children. Note ads is voice-only and advertising is face/body-only. Royall maintains this taxonomy, so treat it as data rather than hardcoding it.
depthstringoptionalOne of inference (default), synthesis, training. Synthesis/training require a license that permits generation.
commercialbooleanoptionalWhether the use is commercial. Default true.
abuse_categorystringoptionalDeclare a sensitive category. If it hits the universal floor the call is BLOCKed unconditionally.

ALLOW response

status"ALLOW"
tierthe consent tier that cleared it
grant_idgrant_<24hex>
expires_atISO-8601 Z (~15 min; ≤ 1 h max)
resolver…/id/<asset_code>
signed_tokenthe portable signed grant
reasonallow:<tier>

BLOCK response

status"BLOCK"
reasona block:* string →

No grant fields on a BLOCK. (A sandbox key adds "sandbox": true to either response.)

Every block:* reason

block:protected_defaultThe creator hasn't offered this use (default state).
block:no_offer:{tier}/{use_case}Offered other uses, but not this tier+use-case — e.g. block:no_offer:commercial_synthesis/ads. (The interpolated value is the resolved tier, not the raw depth.)
block:no_license:{tier}Tier is offered but your company holds no active, in-term license — e.g. block:no_license:commercial_synthesis.
block:universal_floor:{category}Hit the non-overridable floor. Never authorizable.
block:creator_exclusion:{category}The creator excluded this category (alcohol, gambling, political, …).
block:tier_gated:commercial_trainingTraining (T4) is per-deal admin-approved only — never blanket.
block:creator_unavailableThe identity isn't available on buyer surfaces (opaque by design).
block:managed_not_clearedThis identity is managed under a direct agreement and isn't cleared for self-serve authorization. Contact us.
block:asset_not_foundNo registered, non-revoked asset for that code.
block:asset_not_liveRegistered, but the asset is withdrawn or suspended.
block:invalid_asset_codeMalformed Asset ID, or one that fails its checksum.
block:master_consent_revokedThe person withdrew consent platform-wide.
block:identity_unverifiedThe person has not completed identity verification, and this integration requires it.
block:no_grantConnect lane only — this person has no active grant to your partner account.
block:partner_sandbox_unavailableConnect lane only — a sandbox key cannot stand in for a real consent decision.
block:asset_unminted · block:invalid_use:unknown_depthMalformed/edge states — fail-closed.
block:signing_unavailableOurs, not yours. Every would-be ALLOW becomes this when our signing key is unavailable. Nothing about your request is wrong — retry, and tell us if it persists.

Consent tiers

What a creator can authorize, from most-protected to most-permissive. Your use_case + depth resolve to exactly one tier; if the creator offered it (and your company holds any required license) you get ALLOW.

tierdepthmeaning
protectedT0—The default. Registry + enforcement only — nothing is authorized.
fan_noncommercialT1inferenceFree, non-commercial use of the creator’s recorded likeness samples, with attribution. Revocable. No license required.
commercial_inferenceT2inferencePaid commercial use of the creator’s recorded likeness samples or models trained on them. No new content generated. Requires a license.
commercial_synthesisT3synthesisPaid commercial generation of new content in the likeness (TTS / image). Requires a license.
commercial_trainingT4 gatedtrainingPersistent model training/fine-tune. Per-deal admin approval only — a creator can never self-offer it.

Creators may also attach exclusions — categories they won't allow regardless of tier: alcohol, gambling, tobacco, cannabis, adult, political, religious, competitors. A hit returns block:creator_exclusion:<category>.

The universal floor

Five categories are never authorizable — no tier, payment, or creator setting overrides them. They're enforced in code, checked against both fields of your request that can carry a category — abuse_category and use_case. Declaring one via abuse_category (or a matching use-case) returns block:universal_floor:<category>.

illegalAny unlawful use.
nciiNon-consensual intimate / sexual imagery.
fraud_impersonationScams, fake authority, deception.
defamation_harassmentDefamation or targeted harassment.
political_manipulationFalsely-attributed endorsements / deepfake misinformation.

The signed grant

On ALLOW, signed_token is a portable, Ed25519-signed proof-of-clearance binding the asset, tier, use, your company, a grant_id, and a short expiry (~15 min, ≤ 1 hour). Embed it in your output (e.g. as a C2PA assertion). Anyone can verify it offline against Royall's public key — every SDK's verify() does this with zero calls back to Royall, using a canonical-bytes derivation that's byte-identical across Python, Node, and Go.

GET /api/royall-pubkey

Returns {alg:"ed25519", kid, public_key_pem, public_key_b64, signed_endpoints, c2pa_cert_chain_url}. Re-authorize at generation time — a short TTL means a withdrawn grant flips to BLOCK by architecture, not by polling.

GET /api/royall-sandbox-pubkey

The separate key for sandbox grants. Returns {alg:"ed25519", kid, public_key_b64, env:"sandbox", warning}; the kid is prefixed sbx-. For integration testing only — never load this key into a production verifier. Its whole purpose is that production cannot accept what it signs.

What verify() checks

In this order, failing closed at every step: signature present → the signed env selects which key must have signed it (an unrecognised or missing env is rejected, not assumed) → signature → principal parses → version == 4 (exact — reject anything else) → identity_verified is a real boolean → expiry → status == "ALLOW" → and finally, a sandbox grant is refused unless you explicitly opted in. The signature is checked before the environment is acted on, so a forged env cannot route around it.

What verify() does not check

A passing verify() proves Royall issued the grant. It does not prove the grant was issued to you, for this asset, or for this operation. Royall signs with one production key, so a valid grant belonging to another platform, another Asset ID or another use case passes every check above. verify() takes no caller expectation — after it passes, compare the signed claims to the values you expected:

claimcompare against
principalyour own account's identity — a flat string whose shape depends on your credential (company:<id> for Herald keys, partner:prt_… for Connect partners). It belongs in your configuration; deriving it from the token you are checking asserts nothing.
asset_codethe canonical Asset ID of the job you are about to run.
tier and use_casethe operation you are actually performing.
identity_verifiedyour own policy, if you require verified identity.

Skipping this comparison makes the token a proof that somebody was authorized for something.

Resolver & badge

A Royall Asset ID is a public, printable handle (like a phone number). The code is public; what it resolves to is consent-gated.

GET /id/{code}

Machine JSON. 422 on a mistyped code; otherwise HTTP 200 with a status of active, revoked, deleted, or unknown. Anonymous responses carry no PII — id, status, kind, issued, verified_human, assertion for an active asset, plus demo on the documentation demo asset; a revoked tombstone carries since); a valid Herald key additionally sees its own rights + authorized_for for that asset.

GET /verify/{code}

A shareable “Verified Human by Royall” badge page — a human-readable view of the same resolution. See it cleared → or Unverified →.

Asset IDs look like RYL-V-2026-7K3F9Q2X-N — namespace · kind (V/F/B) · year · random designation · a Luhn-mod-32 check character. Case-insensitive and hyphen-tolerant.

Embed the badge

Drop a live “Verified Human by Royall” badge into your own product — it reflects the current consent status and links to the full /verify/{code} page. The badge is a cue, not proof: the authoritative check is the resolver + the signed grant, so the badge always links back to /verify.

Verified Human by Royall · example    Unverified

Plain HTML — no JavaScript

Works anywhere an image does: a web page, a README, an email.

<a href="https://vault.royall.ai/verify/RYL-V-2026-7K3F9Q2X-N">
  <img src="https://vault.royall.ai/badge/RYL-V-2026-7K3F9Q2X-N.svg"
       alt="Verify with Royall" height="28">
</a>

Web component — live, one tag

A custom element that renders the badge (Shadow-DOM isolated) and links to /verify. Load the script once, then use <royall-badge> anywhere.

<script src="https://vault.royall.ai/badge.js" async></script>

<royall-badge code="RYL-V-2026-7K3F9Q2X-N"></royall-badge>

The SVG is short-cached (status is live). Endpoints: GET /badge/{code}.svg (the image) and GET /badge.js (the component). Both are public and need no key. Embed them as an image and a script tag — they do not send CORS headers, so a cross-origin fetch() is blocked (and is not how you would use them).

Errors & rate limits

  • ALLOW / BLOCK are both 200. A BLOCK is a decision, not a failure — branch on status.
  • 401 — bad/missing key (missing_bearer_token / invalid_key_format / invalid_or_revoked_key).
  • 422 — a mistyped Asset ID at /id/{code} ({"error":"invalid_checksum"}).
  • 429 — over quota (rate_limit_exceeded). /v1/authorize is limited per key over a rolling 24-hour window. Allowances are agreed per partner rather than published, so ask us for yours. The /v1/connect/* endpoints are charged to a per-partner daily budget, not to an IP. Respect the Retry-After header rather than hard-coding a delay.
  • 5xx — transient; retry with backoff. The decision path holds no ML, so it stays fast.

SDKs

No SDK is required. Herald is one JSON endpoint over HTTPS — the curl tab above is the whole integration, and grant verification is standard Ed25519 you can do with any crypto library.

If you'd rather not write it yourself, we maintain three SDKs. Each gives you a typed authorize() returning a Decision (BLOCK is not an error) plus offline grant verify(). Canonical grant bytes are byte-identical across all three, so a grant signed for one verifies in the others.

languagepackagenotes
Pythonroyall_herald3.9+. Offline verify needs the cryptography extra.
Node / TypeScriptroyall-heraldZero dependencies. Node 18+, browsers, Deno, Bun, Workers.
GoheraldStandard library only — no external modules.

These are not yet on public package registries. Ask your Royall contact and we'll send you the SDK for your language, or integrate directly against the HTTP endpoint above — both are fully supported.

Royall Connect
Loading the guide…

Machine-readable source: /developers/connect.md. Questions: support@royall.ai.