Layers
Partner APIConcepts

Content items

Containers are the unit of content. Each container holds media, captions, and scheduling metadata for one creative — generated by Layers or uploaded by you.

View as Markdown

A content item - or, to use the field name, a content container - is one creative. It holds the media, the captions, the hook, the format, the influencer that voiced it (when generated), and the approval state. When we talk about "a piece of content" in this API, we mean a container.

Containers come from two origins, distinguished by creativeType:

  • generated — Layers produced the media. Created by the generate endpoints below.
  • uploaded — you supplied finished media via one of the upload transports. Uploaded containers arrive completed (no job to poll), publish byte-for-byte, and are born adsEnrollment: "opted_out".

Everything downstream — approval, scheduling, publishing — treats both origins the same.

Generation is async. You POST a format and a hook, you get back a 202 envelope with a jobId and a containerIds[] array (one entry per variant — see Variants below), and you poll either the job envelope for progress or each container for the final shape.

Lifecycle

Container status values (from the ContainerStatus enum):

  • queued - job accepted, not yet running.
  • processing - generation job is running. Media assets are empty until complete.
  • completed - generation finished. Media and captions are populated. If the project's approval policy requires review, the container's approvalStatus will be pending.
  • failed - generation failed. Check lastError on the container.
  • canceled - container was canceled (by explicit cancel or project archive).

Approval status is independent of container status - it lives on approvalStatus and takes values not_required, pending, approved, or rejected.

Creating one

POST /v1/projects/:projectId/content
Content-Type: application/json
Authorization: Bearer $LAYERS_API_KEY
Idempotency-Key: 7c2f1a3e-0b4c-4a11-9f7e-33c0a2c1bd55

{
  "format": "slideshow-builder",
  "variantCount": 1,
  "hook": "wait for it...\nthis simple habit changed everything about my mindset 🧠",
  "references": {
    "mediaIds": ["med_01HX9Y6K7EJ4T2ABCDEF"]
  }
}
202
{
  "jobId": "job_01HX9Y6K7EJ4T2ABCDEF01234",
  "kind": "content_generate",
  "status": "running",
  "stage": "queued",
  "projectId": "prj_254a4ce1-f4ca-42b1-9e36-17ca45ef3d39",
  "containerIds": ["cnt_7d18b9a1-8b2c-4f3e-a4d5-6e7f8a9b0c1d"],
  "format": "slideshow-builder",
  "locationUrl": "/v1/jobs/job_01HX9Y6K7EJ4T2ABCDEF01234",
  "startedAt": "2026-04-18T12:04:11.000Z"
}

Pair Idempotency-Key with every generate call. A retry inside the replay window returns the original 202; a conflicting body returns 409 IDEMPOTENCY_CONFLICT rather than billing you for a duplicate job. Don't pass id in the body — the API mints all resource ids. See Idempotency.

Formats

Layers internally produces four content formats. Two are partner-callable in v1; the other two are documented here so partners can plan against the full surface, but currently return 422 UNSUPPORTED_FORMAT.

format is required at the partner surface. Pick one of the supported types — there is no auto fallback. Each format has its own preconditions; if they aren't met the call returns a structured 422 rather than silently substituting a different format.

formatWhat it producesRequired preconditionsStatus
slideshow-builderMulti-image vertical slideshow built from brand context (9:16).Project has app_name + app_description.Available.
ugc-remixUGC video remixed from a partner-uploaded app-demo clip + Layers' reaction-template pool.Project has at least one media-library row with media_role: "app-demo".Available once asset upload (A27) ships.
video-remixShort-form video remixed from a discovered third-party source clip.Source-post selection endpoints not yet exposed to partners.Reserved (v2). See "Roadmap: source-coupled formats" below.
slideshow-remixImage slideshow remixed from a discovered third-party slideshow source.Source-post selection endpoints not yet exposed to partners.Reserved (v2). See "Roadmap: source-coupled formats" below.

Partner-asked formats with no internal equivalent return 422 UNSUPPORTED_FORMAT: image, carousel, short_form_video. (Carousels are an Instagram distribution concept; our slideshow-builder outputs render as carousels at distribution time.)

Roadmap: source-coupled formats

video-remix and slideshow-remix need a source post as the basis for the remix. Internally Layers' UI lets a customer point at a TikTok video by ID, but the Partner API doesn't yet expose the surface partners would need:

  • A way to discover candidate source posts scoped to the project's brand or competitor set.
  • A canonical contract for passing a source-post identifier on the generate call.

Until that endpoint pair lands, calling these formats returns 422 UNSUPPORTED_FORMAT with a roadmap hint. There is no feature flag or scope that bypasses this — the surface simply isn't there yet. Track the changelog for the unlock.

When preconditions fail, the response body carries the specific MISSING_* reason — see Errors:

CodeCause
UNSUPPORTED_FORMATFormat name unknown or reserved for v2.
MISSING_APP_NAMEslideshow-builder requires appName on the project. Set it via POST /v1/projects or PATCH /v1/projects/:id.
MISSING_APP_DESCRIPTIONslideshow-builder requires projects.app_description.
MISSING_APP_DEMOugc-remix requires a media_role: "app-demo" row in the project's media library.

Variants

variantCount > 1 returns N independent containers — same hook, parallel jobs, fan-out billing (N × per-variant cost). Default 1. The containerIds[] array carries every container in document order; each one polls independently via GET /v1/content/:containerId or via the shared jobId.

Reading a container

GET /v1/content/:containerId
→ 200
{
  "id": "cnt_7d18b9a1-8b2c-4f3e-a4d5-6e7f8a9b0c1d",
  "projectId": "prj_254a4ce1-f4ca-42b1-9e36-17ca45ef3d39",
  "status": "completed",
  "approvalStatus": "approved",
  "format": "slideshow-builder",
  "influencerId": "inf_4a8e1bc2-3d4f-46a8-9b0c-1d2e3f4a5b6c",
  "hook": "wait for it...\nthis simple habit changed everything about my mindset 🧠",
  "preview": {
    "kind": "slideshow",
    "primaryUrl": "https://media.layers.com/.../slide-01.jpg",
    "thumbnailUrl": "https://media.layers.com/.../slide-01.jpg",
    "imageUrls": [
      "https://media.layers.com/.../slide-01.jpg",
      "https://media.layers.com/.../slide-02.jpg",
      "https://media.layers.com/.../slide-03.jpg"
    ],
    "aspectRatio": "9:16"
  },
  "assets": [
    {
      "assetId": "ast_01HX9Y6K7EJ4T2ABCDEF",
      "kind": "image",
      "url": "https://media.layers.com/.../slide-01.jpg",
      "width": 1080,
      "height": 1920
    }
  ],
  "captions": [
    { "platform": "tiktok", "text": "..." }
  ],
  "lastError": null,
  "createdAt": "2026-04-18T12:04:11.000Z",
  "updatedAt": "2026-04-18T12:07:33.000Z"
}

The preview object is the canonical "render this in my UI" surface — partners should not infer media type from assets[] shape. See Preview object for the full per-kind field-population matrix.

Asset descriptors use assetId (not id). Media URLs on assets[].url are long-lived CDN paths. For direct asset addressing, use GET /v1/content/:id/assets/:assetId.

Rejecting

  • POST /v1/content/:id/reject — reject through the approval gate. To produce a new take, call POST /v1/projects/:id/content again with a fresh hook — generation is fast; we don't keep a separate "regenerate same container" surface.

Listing

GET /v1/projects/:projectId/content?status=completed&format=slideshow-builder&limit=25

Filters: status, format, creativeType, since, until, cursor, limit. Cursor-paginated. Pass status=completed when you're rendering a library view; combine with an approval query to drive your review UX. creativeType=uploaded scopes to your uploaded library. List rows carry the same Preview object as GET /v1/content/:id, so a gallery can render straight from the list response.

Progress during generation

For an in-flight container, both endpoints tell you the same story from different angles:

  • GET /v1/jobs/:jobId - canonical progress. Use this by default.
  • GET /v1/content/:id/progress - same shape, scoped to the container. Useful if you've lost the jobId.

On this page