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_7d18b9a1... is not approved.",
    "requestId": "req_01HXZ9G7KMV2QX8Y1S5RJW3B7T",
    "details": {
      "containerId": "cnt_7d18b9a1-8b2c-4f3e-a4d5-6e7f8a9b0c1d",
      "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 mutate an existing resource. details.reason narrows it. For sub-orgs: patching / suspending / resuming an archived child, allocating credits into an archived child, or a project migration blocked because a mapped project has an in-flight execution (the blocking project is named).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. For sub-org credit-config: a one-sided auto-refill change (only refillThreshold or only refillAmount) carries details.code: REFILL_REQUIRES_THRESHOLD_AND_AMOUNT — set both or clear both.NoFix the fields called out in details.issues.
UPLOAD_INCOMPLETE409Direct-upload finalize arrived before every presigned PUT landed. The message names the missing storage key.After the PUTComplete every file's PUT, then call finalize again — finalize is idempotent.
PAYLOAD_TOO_LARGE413An upload body exceeds its per-type byte cap — for content uploads: 100MB video / 30MB image; for app media: the per-kind cap. details.maxBytes names the limit.NoExport a smaller file. For video, a standard social MP4 preset lands well under the cap.

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/projects/:projectId/content again with a fresh hook to produce a new container.
RETURN_URL_NOT_ALLOWED403OAuth returnUrl not on your key's allowlist. details.returnUrl and details.host (when the URL parsed) echo what you sent so you can confirm what was tested.NoAdd the URL to your allowed return URLs, then create a fresh OAuth URL. Contact your Layers account manager to update the key.
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 credits. On a credit-spending call the body carries details.reason: "insufficient" (the wallet can't cover this generation) or "cap" (a sub-org child hit its monthly monthlyCreditCap ceiling). allocate also returns 402 when the parent wallet can't cover the transfer, but with no details.After top-upTop up the wallet or wait for the monthly credit grant. For a child cap (reason: "cap"), raise monthlyCreditCap via PATCH …/credit-config; for allocate, top up the parent first.
MODERATION_BLOCKED422Our safety layer refused to ship the generated asset.New callTry again with a different hook. 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.
UPLOAD_QUOTA_EXCEEDED409The project's uploaded-content quota is full. details always carries currentUploads, maxUploads, currentBytes, maxBytes — usage and limits, never a bare refusal.After freeing quotaDelete uploaded content you no longer need, or contact support to raise the limits.

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.YesHonor Retry-After. See Rate limits.
INTERNAL500We broke. requestId lets us find it. The message is intentionally generic ("An unexpected error occurred. Please contact support with the requestId.") — server-side details are logged against your requestId so we can find them, but they're never round-tripped to your response.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.
SCRAPE_FAILED502A URL you gave us to fetch (app media, content upload) returned an error or timed out. Expired signed URLs land here too.After fixing the URLConfirm the URL is reachable from the public internet and, if signed, that the signature hasn't expired — sign for at least 15 minutes.

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 exceeds the max 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

Async jobs 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" }
  }
}

Jobs use the same error vocabulary. The extra field is data.stage: the job stage active when the failure occurred.

One additional code is specific to the job path: WORKFLOW_START_FAILED (500). The request was accepted, but the job could not start. Retry with the same Idempotency-Key.

Retry guidance

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

  • RATE_LIMITED - honor Retry-After. Don't add jitter; the server already staggered your bucket reset.
  • INTERNAL - exponential backoff with a small attempt cap.
  • 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