Layers
Partner APIOperational

Errors

Every error code the API returns, what it means, and what to do about it.

View as Markdown

Errors are a flat set of stable codes. The string identifies the kind of failure; the HTTP status tells you what class of fix it needs. Pick branches on code, not on the message — messages are for humans.

The envelope

Every 4xx and 5xx response carries the same shape:

{
  "error": {
    "code": "APPROVAL_REQUIRED",
    "message": "Content container cnt_01HXZ9... is not approved.",
    "requestId": "req_01HXZ9G7KMV2QX8Y1S5RJW3B7T",
    "details": {
      "containerId": "cnt_01HXZ9G7KMV2QX8Y1S5RJW3B7T",
      "approvalStatus": "pending"
    }
  }
}
  • code is stable. We'll add new codes; we won't rename existing ones.
  • message is a human sentence. Log it, don't branch on it.
  • requestId appears on every response, error or not. Keep it in your logs.
  • details is present when a code carries structured context. Fields documented per code below are the ones we guarantee.

Include requestId in every support ticket. It's the one field that lets us find your exact request in the haystack.

Reading the table

We group by behavior, not by HTTP status:

  • Caller bugs — your request is wrong. Fix the request; don't retry.
  • Policy gates — we refused on purpose. Change state (approve the container, reconnect the account, ask us for a quota bump), then retry.
  • Transient — something slipped. Retry with backoff.
  • Upstream — a platform (TikTok, Meta, Apple) rejected us. Read details.platformCode and act on the platform's terms.

The "Retry?" column is advice, not a command. A PLATFORM_ERROR for an expired access token isn't worth retrying until you reconnect; a PLATFORM_ERROR for a flaky upload is.

Caller bugs — fix the request

CodeHTTPWhat happenedRetry?What to check
UNAUTHENTICATED401Missing, malformed, or wrong API key.NoAuthorization: Bearer lp_… header. Is it the current key? Has it been revoked? Run GET /v1/whoami to sanity-check.
FORBIDDEN_SCOPE403The key is valid but missing the scope this route needs. details.requiredScope tells you which.NoCreate a new key with the right scope, or ask your Layers contact to widen an existing one.
FORBIDDEN_FENCE403You sent a field that's gated behind a future release — for example, v2 engagement config fields before they ship.NoDrop the fenced fields. See Versioning.
NOT_FOUND404The resource doesn't exist or isn't in your org. We return 404 for both on purpose — we don't leak cross-org existence.NoVerify the id, verify the project belongs to your org, verify customerExternalId if you set one.
CONFLICT409State collision. A resource is already in the target state, or a body-id upsert would silently mutate an existing row. details.reason narrows it.NoChange state or fix the body. See Idempotency for body-id upserts.
IDEMPOTENCY_CONFLICT409Same Idempotency-Key was used earlier with a materially different request body. details.originalRequestHash and details.currentRequestHash let you diff the two.NoCreate a new idempotency key for the new body. See Idempotency.
VALIDATION422The body parsed but failed schema or business-rule validation. details.issues[] lists every field that failed with a path and message.NoFix the fields called out in details.issues.

Policy gates — change state, then retry

CodeHTTPWhat happenedRetry?What to check
APPROVAL_REQUIRED403Project has requires_approval: true and you tried to schedule or publish a container that isn't approved.After approvalApprove the container via POST /v1/content/:containerId/approve or flip the project policy.
CONTENT_REJECTED409Tried to schedule or publish a container whose approval_status is rejected. Rejected content can't be promoted — approval is a one-way gate once refused.NoCall POST /v1/content/:containerId/regenerate with a revised brief, or create a fresh container.
RETURN_URL_NOT_ALLOWED400OAuth returnUrl not on your key's allowlist.NoAdd the URL via ladmin, then create a fresh OAuth URL.
KILL_SWITCH503Your key, your org, or the whole partner API has been flipped off. details.scope is one of key, organization, global.Only on global after we un-flipFor key / organization, contact us. For global, wait and retry.
BILLING_EXHAUSTED402Out of generation credits or ads wallet balance. details.resource tells you which.After top-upTop up the wallet or wait for the monthly credit grant.
MODERATION_BLOCKED422Our safety layer refused to ship the generated asset.RegenerateAdjust the brief and regenerate. Don't loop.
CREDENTIAL_INVALID422The platform credential we need expired or was revoked. details.platform + details.socialAccountId tell you what to reconnect.After reconnectCreate a reauth URL and push the user through it.
CIRCUIT_OPEN503A circuit breaker on one of our platform integrations is open (e.g., App Store scraping after repeated failures). details.resource + details.retryAfterSeconds tell you what and when.After retryAfterSecondsWait out the cooldown. Retrying sooner will just extend it.

Transient — retry with backoff

CodeHTTPWhat happenedRetry?What to check
RATE_LIMITED429Token bucket empty for (api_key_id, endpoint_class). Retry-After header and details.retryAfterMs tell you when.YesSleep at least Retry-After seconds. See Rate limits.
INTERNAL500We broke. requestId lets us find it.Yes, with jitterRetry two or three times with exponential backoff. If it keeps failing, open a ticket with the requestId.

Upstream — platform rejected us

PLATFORM_ERROR is a single code for every upstream failure. The platform lives in details.platform; the platform's own error code lives in details.platformCode; the raw message is details.platformMessage. The upstream is always exactly one of tiktok, meta, apple, instagram, appstore.

CodeHTTPWhat happenedRetry?What to check
PLATFORM_ERROR502Platform API returned an error. Act on details.platformCode.DependsIf the platform returned a retryable code (rate limit, 5xx), retry with backoff. If it's a content/policy rejection, fix the content.

Example:

{
  "error": {
    "code": "PLATFORM_ERROR",
    "message": "TikTok rejected the upload: video too long.",
    "requestId": "req_01HXZ9G7...",
    "details": {
      "platform": "tiktok",
      "platformCode": "video_duration_exceeds_limit",
      "platformMessage": "Video duration 185s exceeds max 180s for this account.",
      "retryAfterMs": null
    }
  }
}

retryAfterMs is populated when the platform hands us a retry window. When it's null, treat the error as non-transient until you've addressed the underlying issue.

Job failures

Jobs (async ops) use the same error shape inside the terminal GET /v1/jobs/:jobId response, with one wire-level difference: the structured-context field is named data (not details) on the nested job error, and there is no requestId on it — the outer HTTP response still carries X-Request-Id.

{
  "status": "failed",
  "finishedAt": "2026-04-18T14:22:10Z",
  "error": {
    "code": "MODERATION_BLOCKED",
    "message": "Generated asset failed safety review.",
    "data": { "stage": "finalizing", "safetyCategory": "violence" }
  }
}

The codes are the same table above — you don't learn a second vocabulary for jobs. The extra field is data.stage: the job stage we were on when it failed.

One additional code is specific to the job path: WORKFLOW_START_FAILED (500) — we accepted the POST and allocated the job row, but the underlying workflow start failed. Retry with the same Idempotency-Key.

Retry guidance

Safe defaults, if you're writing this once and forgetting it:

  • RATE_LIMITED — sleep Retry-After seconds exactly. Don't add jitter; the server already staggered your bucket reset.
  • INTERNAL — exponential backoff starting at 1s, cap 30s, max 3 attempts.
  • PLATFORM_ERROR with retryAfterMs — honor it exactly, then retry once.
  • PLATFORM_ERROR without retryAfterMs — don't retry. Surface the error to your caller.
  • CIRCUIT_OPEN — wait retryAfterSeconds, then make one request to see if the breaker closed. If it's still open, wait again.
  • Everything else in the "Caller bugs" and "Policy gates" tables — don't retry until you've fixed state.

Every retry you do should reuse the original Idempotency-Key. That's the whole point of idempotency — cached responses replay, the underlying work doesn't run twice. See Idempotency.

See also

On this page