Information
- OpenAPI version:
3.1.0
Viewing v1— superseded by v2
REST API for AI-powered game asset creation on the Layer platform.
Every endpoint requires a Bearer token in the Authorization header, except the ones a
client reads while deciding whether to integrate at all: openapi.json, versioning, and
the interactive docs.
Authorization: Bearer <token>
The simplest way to authenticate is with a Personal Access Token (PAT):
Authorization: Bearer <your-pat-token>PATs are long-lived and ideal for scripts, CI pipelines, and partner integrations.
For browser-based applications, use the Auth0 OAuth2 flow. Redirect users to
the Layer login page, exchange the authorization code for a JWT access token,
and use it as Authorization: Bearer <jwt>.
Generations consume Creative Units (CUs). Use the estimate endpoints to check
cost before executing. Check workspace balance via GET /v1/workspaces/{id}.
List endpoints support cursor-based pagination via limit and cursor query
parameters. Responses include a pagination object with next_cursor,
has_more_results, and total_count. Cursors are opaque — do not parse or
construct them.
All errors return RFC 9457 Problem Details with
content type application/problem+json:
{
"type": "https://api.layer.ai/errors/ERROR_CODE",
"title": "Human-readable title",
"status": 404,
"detail": "Detailed description of what went wrong."
}
Branch on type, not on title or detail — those are prose and may be reworded. type
is a stable opaque identifier whose last path segment is the machine-readable error code; it
is not a live URL and does not resolve to a page. Documentation lives at
layer.ai/docs.
Common status codes: 401 (unauthenticated), 403 (forbidden), 404 (not found),
422 (invalid input), 429 (rate limited).
Creative Units are not checked when a run is submitted, so an underfunded workspace
does not fail at submission time. Call the estimate endpoint and read
has_sufficient_creative_units to check up front; otherwise the run is accepted and
then reaches a terminal FAILED state with error_code INSUFFICIENT_BALANCE.
A lost response is indistinguishable from a request that never arrived, and retrying a generation spends Creative Units again. Send an idempotency key on any unsafe request so a retry returns the first result instead of executing again, within the limits below:
Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7
The first request with a given key executes. A later request with the same key returns
that first response, with Idempotent-Replayed: true added, without executing anything,
for as long as the rules below keep it.
Any opaque string up to 255 characters works; a UUID per logical operation is the usual
choice. Keys are scoped to your credential’s owner.
A retry is the same request: same method, same path, same query parameters (in any order), and a byte-identical body. The body is compared as raw bytes, not as parsed JSON, so keep the bytes you sent and resend them; re-serializing the same object can change whitespace or key order and turn a retry into a different request.
What a retry with the same key gets depends on what the server did with the first request, which a lost response does not tell you. Retry with the same key and the server answers from whichever case applies:
2xx). The response is stored and a retry replays it for 24 hours,
even if the original response never reached you. Storing is best effort: a response too
large to store, or one the store could not accept, is covered only by the 120-second
claim below.4xx). Nothing was spent and the key is released, so a corrected
retry executes.5xx). When the first request is received, its key
is claimed for 120 seconds, and the claim is not extended while the request runs. A retry
inside that window returns 409 IDEMPOTENT_REQUEST_IN_PROGRESS with Retry-After;
wait that long and try again. A 5xx may already have spent Creative Units, so its key
is neither stored nor released and stays claimed until the 120 seconds run out.A 409 therefore means the outcome is not known yet: the original may still be running, or
it may have failed after spending. Keep retrying through 409 with the same key to get the
stored response once the original succeeds. Once the 120 seconds run out without a stored
response, the next retry with the same key executes as a new request: it spends again, and
it may run alongside an original that is still in flight. Decide before that point whether
a second run is acceptable.
Idempotency is best effort throughout, not only for stored responses. If the key store
cannot be reached when a request arrives, the request runs without protection, and a
claim or stored response can disappear before its time is up. An earlier 409 or replay
therefore does not guarantee that a later retry cannot execute again.
A different request under a key that is still claimed or stored returns 422
IDEMPOTENCY_KEY_REUSED. The key covers the request it was first used with, so this is a
client bug rather than a retry, and returning some other request’s result would be worse
than an error.
Keys on safe methods are ignored: GET is already repeatable.
Quotas are per user and apply to the compute operations that spend Creative Units: starting a generation and starting a training run.
Every response describes the quotas in force, including one that rejected you for having no credential — a client that has not authenticated yet is the one that most needs to know what budget it will be planning against:
RateLimit-Policy: "generation";q=60;w=60, "generation-video";q=30;w=60, "training";q=10;w=3600
A quota named after a modality narrows the one it extends: a video generation spends from
both generation and generation-video, so the tighter of the two decides.
Once you are authenticated, responses from the operations a quota governs also report what is left of it:
RateLimit: "generation";r=57;t=3
RateLimit-Limit: 60
RateLimit-Remaining: 57
RateLimit-Reset: 3
RateLimit-Policy and RateLimit are the standard structured fields — q is the quota,
w the window in seconds, r the units left, t the seconds until the quota is fully
restored. The RateLimit-Limit/-Remaining/-Reset triple repeats the tightest quota for
clients that parse the older convention.
Exceeding a quota returns 429 with a Retry-After header; wait that many seconds before
retrying.
The API version is the first path segment. /v1 and /v2 are both stable and served
together; v1 routes are unaffected by v2.
/v2 is a complete surface — every resource is reachable under it, so a client can pin
/v2 and never fall back. It differs from v1 in the generation surface: inference runs
take reference sets — trained subjects, styles, and characters — alongside a base model,
and the v1 /models catalog is split into /base-models and /reference-sets.
Every other resource is the same endpoint under both prefixes: identical handler, request
body, and response shape. Their v2 operation ids carry a V2 suffix so generated clients
get one symbol per published path.
Backward-compatible additions — a new endpoint, a new optional request field, a new response field — ship into an existing version without notice. Anything that could break a client gets a new version instead.
Nothing is withdrawn without notice. When an operation or a version is deprecated:
deprecated: true in openapi.json.Deprecation
(RFC 9745) with the date it was deprecated,
Sunset (RFC 8594) with the earliest date it
may stop working, and a Link header with rel="deprecation" to this policy and
rel="successor-version" to its replacement.Poll openapi.json or watch for Deprecation on your own traffic — both carry the same
timeline.
Long-running requests can push their result to you instead of being polled for it. Pass a
webhook object on POST /v2/.../inferences, POST /v1/.../training-runs,
POST /v1/.../scores, or POST /v1/.../workflows/{workflow_id}/runs
(POST /v1/.../inferences is poll-only — move to /v2 to be pushed results):
{
"webhook": {
"url": "https://example.com/hooks/layer",
"metadata": {"job": "hero-splash-42"}
}
}
Every response to those endpoints carries expected_events — the deliveries that request will
produce. Wait for those and no others: a generation in a workspace with automatic scoring rules
produces a second, separate event for its scores, and one without produces nothing further.
Every delivery has the same envelope, and data is exactly what the matching GET endpoint
returns for that job — no follow-up request is needed.
{
"id": "evt_7c9e6679…",
"type": "inference.completed",
"created_at": "2026-08-25T09:41:02Z",
"data": { "inference_id": "…", "status": "complete", "outputs": [ … ] },
"metadata": {"job": "hero-splash-42"}
}
data only ever gains fields. Ignore ones you do not recognise rather than rejecting the body.
Deliveries carry Standard Webhooks headers, so any Standard Webhooks library verifies them with no code of your own:
webhook-id — the event id, stable across retrieswebhook-timestamp — unix seconds when the attempt was signedwebhook-signature — one or more space-delimited v1,<base64> signaturesVerifying by hand: HMAC-SHA256 over {webhook-id}.{webhook-timestamp}.{raw body}, keyed by the
base64-decoded secret (without its whsec_ prefix), compared in constant time. Sign the raw
request body — parsing and re-serializing produces different bytes and will not verify.
More than one signature means a rotation is in its grace window; a delivery is authentic if
any signature matches. Read the secret from GET /v1/workspaces/{workspace_id}/webhook-secret
and rotate it with POST /v1/workspaces/{workspace_id}/webhook-secret/rotate.
Reject deliveries whose webhook-timestamp is far from now — a few minutes’ tolerance — so a
captured delivery cannot be replayed at you later.
Answer with any 2xx within 10 seconds. The body is ignored. Acknowledge first and do your work afterwards; a receiver that finishes its processing before answering will time out.
429 — retried, honouring Retry-After (in seconds) where it asks for longer.410 Gone — delivery stops permanently. This is how you retire an endpoint.3xx counts as a failure.Five attempts over roughly an hour: immediately, then after ~10s, ~1m, ~10m and ~45m. If they
are all exhausted, nothing is lost — the job’s GET endpoint returns the same body for as long
as the job exists, which is what makes a short retry window safe.
Delivery is at-least-once and unordered. Deduplicate on webhook-id, which does not change
between attempts of the same event, and use created_at to order events for one job. In
particular inference.completed and its scoring_run.completed can arrive in either order.
Every attempt is recorded on the job itself: GET the run and read its webhook block for the
attempt count, the receiver’s last status code, and the last error.
POST /v1/workspaces/{workspace_id}/webhook-tests sends a signed sample to any URL and tells
you synchronously what came back — reachability, latency, status code. It costs nothing and
creates no run, so it is the cheapest way to check a tunnel or a signature check.
Callback URLs must be public HTTPS endpoints. http://, private addresses and localhost are
rejected when the request is submitted, so use a tunnel while developing.
Bearer authentication with a Layer Personal Access Token (PAT), prefixed pat_. Send it as Authorization: Bearer <token>. Create one in the app under Settings → Personal Access Tokens. An Auth0 session JWT is also accepted for first-party browser use.
Security scheme type: http
OAuth 2.0 sign-in for a person, against Layer’s authorization server. Discovery metadata for this resource is published at https://api.app.layer.ai/.well-known/oauth-protected-resource (RFC 9728), and the authorization server’s own metadata at https://auth.app.layer.ai/.well-known/oauth-authorization-server (RFC 8414). A person’s token carries no scope: it acts with the person’s role.
Security scheme type: oauth2
Flow type: authorizationCode
Authorization URL: https://auth.app.layer.ai/authorize
Token URL: https://auth.app.layer.ai/oauth/token
Refresh URL: https://auth.app.layer.ai/oauth/token
A service account’s access token, from the client credentials grant: the account’s id as client_id, one of its keys as client_secret. A key is also accepted directly as a bearer token. The account acts with its role, limited to the token’s scopes.
Security scheme type: oauth2
Flow type: clientCredentials
Token URL: https://api.app.layer.ai/api/oauth/token
Scopes: