Skip to content

Overview

Viewing v2— current version

REST API for AI-powered game asset creation on the Layer platform.

Authentication

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):

  1. Log in to layer.ai
  2. Go to Settings > Personal Access Tokens
  3. Click Create Token, give it a name, and copy the token value
  4. Pass it as Authorization: Bearer <your-pat-token>

PATs are long-lived and ideal for scripts, CI pipelines, and partner integrations.

OAuth2 (browser-based)

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>.

Creative Units

Generations consume Creative Units (CUs). Use the estimate endpoints to check cost before executing. Check workspace balance via GET /v1/workspaces/{id}.

Pagination

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.

Errors

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.

Idempotency

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:

  • It succeeded (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.
  • It was rejected (4xx). Nothing was spent and the key is released, so a corrected retry executes.
  • It is still running, or it failed (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.

Rate limits

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.

Versioning and deprecation

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:

  1. It is marked deprecated: true in openapi.json.
  2. Every response from it carries 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.
  3. It keeps working until the sunset date, which is at least 90 days after the deprecation date.

Poll openapi.json or watch for Deprecation on your own traffic — both carry the same timeline.

Webhooks

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.

The payload

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.

Verifying a delivery

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 retries
  • webhook-timestamp — unix seconds when the attempt was signed
  • webhook-signature — one or more space-delimited v1,<base64> signatures

Verifying 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.

Responding

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.

  • Any non-2xx, or no response — retried.
  • 429 — retried, honouring Retry-After (in seconds) where it asks for longer.
  • 410 Gone — delivery stops permanently. This is how you retire an endpoint.
  • Redirects are not followed. A 3xx counts as a failure.

Retries and duplicates

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.

Before you go live

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.

Information

  • OpenAPI version: 3.1.0

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:

  • workspace:read - Read workspace
  • workspace:write - Read and change workspace
  • members:read - Read members
  • members:write - Read and change members
  • scim:read - Read scim
  • scim:write - Read and change scim
  • billing:read - Read billing
  • billing:write - Read and change billing
  • labels:read - Read labels
  • labels:write - Read and change labels
  • library:read - Read library
  • library:write - Read and change library
  • sessions:read - Read sessions
  • sessions:write - Read and change sessions
  • projects:read - Read projects
  • projects:write - Read and change projects
  • reference_sets:read - Read reference sets
  • reference_sets:write - Read and change reference sets
  • generation:read - Read generation
  • generation:write - Read and change generation
  • workflows:read - Read workflows
  • workflows:write - Read and change workflows
  • skills:read - Read skills
  • skills:write - Read and change skills
  • integrations:read - Read integrations
  • integrations:write - Read and change integrations