Build on the Licensed-Likeness API: A Tutorial
A hands-on licensed-likeness API tutorial: read boundary sets, dry-run, generate and verify provenance. Handle refusals properly from day one.
A hands-on licensed-likeness API tutorial: read boundary sets, dry-run, generate and verify provenance. Handle refusals properly from day one.
Photo via Unsplash
Most image APIs take a prompt and return a picture. The licensed likeness API adds a step that changes the integration shape entirely: every request is evaluated against the creator's live boundary set server-side, and a request outside it is refused with the specific rule it broke — before any model is called.
This tutorial walks through a real integration in four steps: discover creators, read a boundary set, dry-run a request, then generate and verify. It also covers refusal handling properly, because that is where most integrations are weakest. Full reference lives at /docs/api and the machine-readable spec at /api/openapi.json.
What you'll build
- A client that reads consent rules before requesting, not after being refused.
- Refusal handling that branches on the rule, not on a string match.
- Provenance verification that re-checks licence status rather than caching it.
- The same flow over MCP for agent clients, with no separate enforcement path.
Create an API key from the dashboard. Keys are stored hashed — SHA-256 at rest — so the plaintext is shown once and never again. There is no anonymous path to generation.
Authenticate with a bearer token:
Authorization: Bearer lw_live_...
The base path is /api/v1. The spec at /api/openapi.json is OpenAPI 3.1 and static, so you can generate a client from it directly.
GET /api/v1/creators returns the public directory: slug, display name, whether the profile is currently licensable, and a boundary summary.
Two things to internalise early. First, the directory legitimately returns an empty list when no creators have onboarded — that is an honest state, not an error, and your client should render it as "no creators available" rather than retrying. Second, licensable is live. A creator who has revoked is still listed but not licensable, and requests against them return creator_unavailable.
GET /api/v1/creators/{slug}/boundaries returns the machine-readable permission itself:
permittedContexts — an allow-list. Anything absent is refused.forbiddenDepictions — the deny-list, including the four platform-wide prohibitions.maxRealism — illustrative, stylised or photoreal.conversationLicensed — boolean.version — integer, append-only.Read this first. An agent that requests blindly and learns the rules from refusals burns latency and looks incompetent to the user. An agent that reads the boundary set can say "this creator does not license gaming avatars" before spending anything.
Cache the boundary set briefly if you must, but treat the version as the cache key. Creators publish new versions without warning, which is the point.
POST /api/v1/check takes the same body as generation and runs the identical evaluation without charging or producing anything.
The request body schema is small:
| Field | Type | Notes |
|---|---|---|
creator | string | The creator slug |
prompt | string | Up to 2,000 characters |
context | enum | Must be one of the taxonomy context IDs |
realism | enum | Defaults to illustrative |
mode | enum | image or conversation, defaults to image |
reference | string | Optional; echoed back on the manifest |
Table: the generation request schema, shared by the check and generate endpoints. An invalid body returns 400 with per-field issues and a link to the spec.
Note realism defaults to illustrative rather than to the creator's ceiling. That is deliberate — the safe value is the default, and asking for more is an explicit act.
POST /api/v1/generate with the same body. Three outcomes matter:
200 with decision: "allow" — you get the generation id, any advisories, the boundary summary that authorised it, credit accounting and the render state.
403 with decision: "refuse" — the body carries a refusal object naming the rule, a boundary summary, a human-readable detail, and a documentation link to /boundaries. This is a successful evaluation, not a malformed request, and 403 is the honest status: the client asked for something it is not permitted to have.
400 — the body did not match the schema. The response lists the offending paths.
One behaviour to design around: on a deployment without image-provider credentials configured, an authorised request is still evaluated, authorised and manifest-signed, but render.state comes back as unavailable and no credit is charged. You never receive a placeholder image dressed up as a result. Check render.state rather than assuming a 200 carries pixels.
Credits are reserved atomically before the provider call and refunded on failure. We proved this with five concurrent requests against a single credit: one allowed, four refused, balance zero. No negative balances, no double spend.
GET /api/v1/provenance/{id} returns the manifest and, critically, two separate fields:
signatureValid — has the record been altered since issue?licensed — is the permission still in force?These come apart the moment a creator revokes. A revoked generation returns licensed: false with a licence object carrying status, timestamp and reason; the output URL is withheld; and the response is not cached.
If you cache verification results, cache the signature check and re-fetch licence status. We explained why this distinction exists — and how we shipped it wrong first — in content provenance explained.
The single biggest quality difference between integrations:
Branch on the refusal reason, not on the message text. Reasons are stable identifiers: context_not_permitted, realism_exceeds_ceiling, conversation_not_licensed, platform_prohibition, age_verification_required, creator_unavailable. Messages are written for humans and will be reworded.
Do not retry a boundary refusal. It is not transient. Retrying a platform_prohibition with rephrasing is, functionally, an evasion attempt — and the text normaliser folds accents, homoglyphs, full-width characters, spaced letters and digit substitutions before matching, so it will fail anyway while making your traffic look adversarial.
Surface the reason to your user. "That creator does not license photoreal output" is actionable. "Request failed" is not.
Handle age_verification_required as a flow, not an error. The account needs verification; send the user to /verify rather than showing a stack trace.
"The integrations we like are the ones that read the boundary set first. They make about a tenth as many refused requests, and their users never see a generic failure — they see a specific, true reason." — LikeWard engineering desk
For agent clients there is an MCP server exposing six tools over the same enforcement path: list_creators, get_boundary_set, check_request, generate, verify_provenance and describe_taxonomy. Setup is at /docs/mcp.
describe_taxonomy is worth calling once at session start — it returns the closed vocabulary of contexts and depictions, so the agent constructs valid requests instead of guessing enum values and collecting 400s.
There is no separate, weaker enforcement for MCP. Same runGeneration, same refusals, same manifests. That is a deliberate architectural constraint rather than a policy statement, and it is why we can test one path and make claims about three.
That single-path constraint is not only tidy engineering. Platform duties under the TAKE IT DOWN Act and the Online Safety Act 2023 apply to the service, not to whichever surface a request arrived on — so an API with weaker checks is a compliance gap, not a convenience.
Treating 403 as an auth problem. It usually is elsewhere. Here it means a rule fired. Read the body.
Skipping the check endpoint. It costs nothing and prevents most refusals reaching your user.
Caching licensed. Revocation is the one state change that must not be stale.
Assuming 200 means an image exists. Check render.state.
Ignoring version on the boundary set. Rules change; a stale cache produces confident, wrong pre-flight advice.
Hard-coding context strings. Call describe_taxonomy or read the spec. The vocabulary is closed but not frozen.
Building agent-facing tooling? See the agent integrations use case. If you want the enforcement model rather than the endpoints, start with the boundary set configuration guide.