# Errors (/docs/api/operational/errors)



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 [#the-envelope]

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

```json
{
  "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.

<Callout type="info">
  Include `requestId` in every support ticket. It's the one field that lets us
  find your exact request in the haystack.
</Callout>

## Reading the table [#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 [#caller-bugs---fix-the-request]

| Code                   | HTTP | What happened                                                                                                                                                                                                                                                                                                                                                                                                                                                       | Retry?        | What to check                                                                                                                                                      |
| ---------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `UNAUTHENTICATED`      | 401  | Missing, malformed, or wrong API key.                                                                                                                                                                                                                                                                                                                                                                                                                               | No            | `Authorization: Bearer lp_…` header. Is it the current key? Has it been revoked? Run [`GET /v1/whoami`](/docs/api/reference/organizations/whoami) to sanity-check. |
| `FORBIDDEN_SCOPE`      | 403  | The key is valid but missing the scope this route needs. `details.requiredScope` tells you which.                                                                                                                                                                                                                                                                                                                                                                   | No            | Create a new key with the right scope, or ask your Layers contact to widen an existing one.                                                                        |
| `FORBIDDEN_FENCE`      | 403  | You sent a field that's gated behind a future release - for example, v2 engagement config fields before they ship.                                                                                                                                                                                                                                                                                                                                                  | No            | Drop the fenced fields. See [Versioning](/docs/api/operational/versioning).                                                                                        |
| `NOT_FOUND`            | 404  | The resource doesn't exist or isn't in your org. We return 404 for both on purpose - we don't leak cross-org existence.                                                                                                                                                                                                                                                                                                                                             | No            | Verify the id, verify the project belongs to your org, verify `customerExternalId` if you set one.                                                                 |
| `CONFLICT`             | 409  | State 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](/docs/api/concepts/organizations): patching / suspending / resuming an `archived` child, allocating credits into an archived child, or a [project migration](/docs/api/reference/organizations/migrate) blocked because a mapped project has an in-flight execution (the blocking project is named). | No            | Change state or fix the body. See [Idempotency](/docs/api/operational/idempotency) for body-id upserts.                                                            |
| `IDEMPOTENCY_CONFLICT` | 409  | Same `Idempotency-Key` was used earlier with a materially different request body. `details.originalRequestHash` and `details.currentRequestHash` let you diff the two.                                                                                                                                                                                                                                                                                              | No            | Create a new idempotency key for the new body. See [Idempotency](/docs/api/operational/idempotency).                                                               |
| `VALIDATION`           | 422  | The 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](/docs/api/reference/organizations/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.                                                                    | No            | Fix the fields called out in `details.issues`.                                                                                                                     |
| `UPLOAD_INCOMPLETE`    | 409  | [Direct-upload finalize](/docs/api/reference/content/finalize-upload) arrived before every presigned `PUT` landed. The message names the missing storage key.                                                                                                                                                                                                                                                                                                       | After the PUT | Complete every file's `PUT`, then call finalize again — finalize is idempotent.                                                                                    |
| `PAYLOAD_TOO_LARGE`    | 413  | An upload body exceeds its per-type byte cap — for content uploads: 100MB video / 30MB image; for [app media](/docs/api/reference/app-media/upload-app-media): the per-kind cap. `details.maxBytes` names the limit.                                                                                                                                                                                                                                                | No            | Export a smaller file. For video, a standard social MP4 preset lands well under the cap.                                                                           |

## Policy gates - change state, then retry [#policy-gates---change-state-then-retry]

| Code                     | HTTP | What happened                                                                                                                                                                                                                                                                                                                                                                                                      | Retry?                            | What to check                                                                                                                                                                                                                              |
| ------------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `APPROVAL_REQUIRED`      | 403  | Project has `requires_approval: true` and you tried to schedule or publish a container that isn't `approved`.                                                                                                                                                                                                                                                                                                      | After approval                    | Approve the container via [`POST /v1/content/:containerId/approve`](/docs/api/reference/approval/approve-content) or flip the project policy.                                                                                              |
| `CONTENT_REJECTED`       | 409  | Tried 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.                                                                                                                                                                                                                                                      | No                                | Call [`POST /v1/projects/:projectId/content`](/docs/api/reference/content/slideshow-builder) again with a fresh `hook` to produce a new container.                                                                                         |
| `RETURN_URL_NOT_ALLOWED` | 403  | OAuth `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.                                                                                                                                                                                                                                                 | No                                | Add the URL to your allowed return URLs, then create a fresh OAuth URL. Contact your Layers account manager to update the key.                                                                                                             |
| `KILL_SWITCH`            | 503  | Your key, your org, or the whole API integration has been flipped off. `details.scope` is one of `key`, `organization`, `global`.                                                                                                                                                                                                                                                                                  | Only on `global` after we un-flip | For `key` / `organization`, contact us. For `global`, wait and retry.                                                                                                                                                                      |
| `BILLING_EXHAUSTED`      | 402  | Out 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](/docs/api/concepts/credits) child hit its monthly `monthlyCreditCap` ceiling). [`allocate`](/docs/api/reference/organizations/credits/allocate) also returns `402` when the parent wallet can't cover the transfer, but with &#x2A;*no `details`**. | After top-up                      | Top up the wallet or wait for the monthly credit grant. For a child cap (`reason: "cap"`), raise `monthlyCreditCap` via [`PATCH …/credit-config`](/docs/api/reference/organizations/credit-config); for allocate, top up the parent first. |
| `MODERATION_BLOCKED`     | 422  | Our safety layer refused to ship the generated asset.                                                                                                                                                                                                                                                                                                                                                              | New call                          | Try again with a different `hook`. Don't loop.                                                                                                                                                                                             |
| `CREDENTIAL_INVALID`     | 422  | The platform credential we need expired or was revoked. `details.platform` + `details.socialAccountId` tell you what to reconnect.                                                                                                                                                                                                                                                                                 | After reconnect                   | Create a [reauth URL](/docs/api/reference/social-accounts/reauth-url) and push the user through it.                                                                                                                                        |
| `CIRCUIT_OPEN`           | 503  | A 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 `retryAfterSeconds`         | Wait out the cooldown. Retrying sooner will just extend it.                                                                                                                                                                                |
| `UPLOAD_QUOTA_EXCEEDED`  | 409  | The project's uploaded-content quota is full. `details` always carries `currentUploads`, `maxUploads`, `currentBytes`, `maxBytes` — usage and limits, never a bare refusal.                                                                                                                                                                                                                                        | After freeing quota               | Delete uploaded content you no longer need, or contact support to raise the limits.                                                                                                                                                        |

## Transient - retry with backoff [#transient---retry-with-backoff]

| Code           | HTTP | What happened                                                                                                                                                                                                                                                                               | Retry?           | What to check                                                                                               |
| -------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------- |
| `RATE_LIMITED` | 429  | Token bucket empty for `(api_key_id, endpoint_class)`. `Retry-After` header and `details.retryAfterMs` tell you when.                                                                                                                                                                       | Yes              | Honor `Retry-After`. See [Rate limits](/docs/api/operational/rate-limits).                                  |
| `INTERNAL`     | 500  | We 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 jitter | Retry two or three times with exponential backoff. If it keeps failing, open a ticket with the `requestId`. |

## Upstream - platform rejected us [#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`.

| Code             | HTTP | What happened                                                                                                                                                                                                             | Retry?               | What to check                                                                                                                           |
| ---------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `PLATFORM_ERROR` | 502  | Platform API returned an error. Act on `details.platformCode`.                                                                                                                                                            | Depends              | If the platform returned a retryable code (rate limit, 5xx), retry with backoff. If it's a content/policy rejection, fix the content.   |
| `SCRAPE_FAILED`  | 502  | A URL you gave us to fetch ([app media](/docs/api/reference/app-media/upload-app-media), [content upload](/docs/api/reference/content/upload-content)) returned an error or timed out. Expired signed URLs land here too. | After fixing the URL | Confirm the URL is reachable from the public internet and, if signed, that the signature hasn't expired — sign for at least 15 minutes. |

Example:

```json
{
  "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 [#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`.

```json
{
  "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](/docs/api/concepts/jobs) 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 [#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](/docs/api/operational/idempotency).

## See also [#see-also]

* [Rate limits](/docs/api/operational/rate-limits)
* [Idempotency](/docs/api/operational/idempotency)
* [Jobs](/docs/api/concepts/jobs)
