LikeWard
Academy7 August 20266 min read

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.

By LikeWard Engineering Desk
Source code displayed on a screen, representing an integration with the licensed likeness API

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.

Steps

Before you start

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.

Step 1: list creators

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.

Step 2: read the boundary set

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.
  • maxRealismillustrative, 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.

Step 3: dry-run with check

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:

FieldTypeNotes
creatorstringThe creator slug
promptstringUp to 2,000 characters
contextenumMust be one of the taxonomy context IDs
realismenumDefaults to illustrative
modeenumimage or conversation, defaults to image
referencestringOptional; 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.

Step 4: generate

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.

Step 5: verify provenance

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.

Handling refusals well

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

The MCP route

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.

Integration pitfalls

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.

Frequently asked questions

Why does a refusal return 403 rather than 200 with a decision field?
Because integrators branch on status codes long before they read response bodies, and a refusal returned as 200 gets treated as success by every naive client and most retry wrappers. The body still carries the full decision object with the rule that fired, so nothing is lost — you get both the coarse signal a HTTP client understands and the fine-grained reasoning a careful one wants.
Can I check whether a request would be allowed without spending credits?
Yes — that is what the check endpoint is for. It runs the identical evaluation and returns the same decision object, and it never charges or produces an image. Agents that plan multi-step work should dry-run before committing, both to avoid wasted spend and to give the user a specific reason when something is not going to work.
Are the API rules looser than the website's?
No, and the architecture is what makes that a fact rather than a promise: web, API and MCP all call the same runGeneration function, so a refusal is identical in reasoning whichever surface asked. We test refusals from each surface precisely because 'the API is the loose path' is such a common weakness elsewhere.
What happens to my credits if generation fails after authorisation?
Credits are reserved atomically before the provider call and refunded if it fails, so a failure does not leave you charged for nothing. Requests that are refused on a boundary rule never reach the reservation step at all, because credits are evaluated last — a breach is never billed.
Do I need to re-check provenance records I have already verified?
Yes, if you cache. A signature stays valid forever, but a licence can be revoked at any time, and the verification response reports those separately for exactly that reason. Treat licence status as live state with no useful TTL rather than as a property of the record.