# POST /v1/projects/:projectId/content (/docs/api/reference/content/generate-content)



<Endpoint method="POST" path="/v1/projects/:projectId/content" scope="content:write" phase="1">
  Starts a content-generate [job](/docs/api/concepts/jobs). Returns `202` with one or
  more `containerIds` and a single `jobId` that covers the whole request.
</Endpoint>

This is the main content entry point. Pass a brief and we resolve the right content layer on the project, pick a format (video remix, slideshow remix, UGC remix, or auto), and run the appropriate generation workflow. One call produces `variantCount` containers — each one a full generated post with hook, caption, and media.

Pin `format` when you want a specific shape. Leave it on `"auto"` (the default) and we pick based on the project's brand context, the influencer, and what's been performing.

## Resolving the content layer [#resolving-the-content-layer]

If the project has exactly one content layer, we use it. If it has multiple (e.g. Social Content and Managed UGC), pass `projectLayerId` to disambiguate — otherwise the request returns `409 CONFLICT` with the candidate layers.

## Idempotency [#idempotency]

Pass `id` to make the request idempotent — we treat it as the first container's id and UPSERT. Replaying with the same id returns the prior response; replaying with a different brief returns `409 CONFLICT`. Pair it with `Idempotency-Key` for a 24-hour replay window.

## Path parameters [#path-parameters]

<Parameters
  rows="[
  { name: 'projectId', type: 'string (uuid)', required: true, description: 'The project to generate content for.' },
]"
/>

## Body [#body]

<Parameters
  title="Body"
  rows="[
  { name: 'id', type: 'string (uuid)', description: 'Your id for the first container. Enables idempotent creation.' },
  { name: 'projectLayerId', type: 'string (uuid)', description: 'Which content layer to run through. Omit if the project only has one.' },
  { name: 'format', type: 'string', default: '&#x22;auto&#x22;', description: 'Content shape.', enum: ['video_remix', 'slideshow_remix', 'ugc_remix', 'auto'] },
  { name: 'variantCount', type: 'integer', default: '1', description: 'Number of container variants to generate. Max 5.' },
  { name: 'brief', type: 'object', description: 'Creative brief. See rows below.' },
  { name: 'brief.topic', type: 'string', description: 'Subject or premise.' },
  { name: 'brief.angle', type: 'string', description: 'Point of view.' },
  { name: 'brief.cta', type: 'string', description: 'Closing call-to-action.' },
  { name: 'brief.valuePropositions', type: 'string[]', description: 'Up to 20 benefit bullets.' },
  { name: 'brief.hook', type: 'string', description: 'Opening line or premise.' },
  { name: 'brief.targetPlatforms', type: 'string[]', description: 'Where this content will ship.', enum: ['instagram', 'tiktok', 'youtube'] },
  { name: 'brief.influencerId', type: 'string (uuid)', description: 'Narrate with this influencer.' },
  { name: 'brief.themeTags', type: 'string[]', description: 'Themes for the generator to lean on.' },
  { name: 'brief.referenceMediaIds', type: 'string[]', description: 'Uploaded media the generator can remix from.' },
  { name: 'brief.language', type: 'string', description: 'BCP-47 tag.' },
  { name: 'references', type: 'object', description: 'Structured references for the generator.' },
  { name: 'references.sourcePostIds', type: 'string[]', description: 'Platform posts to seed from (max 20).' },
  { name: 'references.assetIds', type: 'string[]', description: 'Media library asset ids (max 20).' },
  { name: 'targetPlatforms', type: 'string[]', description: 'Override brief-level targets.', enum: ['tiktok', 'instagram', 'youtube'] },
  { name: 'budget', type: 'object', description: 'Credit cap for this request. `{ maxCredits: integer }`.' },
  { name: 'metadata', type: 'object', description: 'Arbitrary partner-supplied metadata, stored on the container.' },
]"
/>

## Request [#request]

<Tabs items="['curl', 'TypeScript', 'Python']">
  <Tab value="curl">
    ```sh title="terminal"
    curl -X POST https://api.layers.com/v1/projects/{projectId}/content \
      -H "X-Api-Key: $LAYERS_API_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: 8d2f1a3e-0b4c-4a11-9f7e-33c0a2c1bd55" \
      -d '{
        "format": "video_remix",
        "variantCount": 2,
        "brief": {
          "hook": "Your first 30 days of running, one clip at a time.",
          "targetPlatforms": ["instagram", "tiktok"],
          "influencerId": "inf_01HXZ9...",
          "themeTags": ["beginner-friendly", "consistency"],
          "language": "en"
        }
      }'
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts title="generate-content.ts"
    const res = await fetch(
      `https://api.layers.com/v1/projects/${projectId}/content`,
      {
        method: 'POST',
        headers: {
          'X-Api-Key': process.env.LAYERS_API_KEY!,
          'Content-Type': 'application/json',
          'Idempotency-Key': crypto.randomUUID(),
        },
        body: JSON.stringify({
          variantCount: 3,
          brief: {
            targetPlatforms: ['instagram'],
            influencerId,
            themeTags: ['habit-tracking'],
          },
        }),
      },
    );
    const { jobId, containerIds } = await res.json();
    ```
  </Tab>

  <Tab value="Python">
    ```py title="generate_content.py"
    import os, uuid, httpx

    r = httpx.post(
        f"https://api.layers.com/v1/projects/{project_id}/content",
        headers={
            "X-Api-Key": os.environ["LAYERS_API_KEY"],
            "Content-Type": "application/json",
            "Idempotency-Key": str(uuid.uuid4()),
        },
        json={
            "format": "auto",
            "variantCount": 2,
            "brief": {
                "targetPlatforms": ["tiktok"],
                "influencerId": influencer_id,
                "themeTags": ["fitness"],
            },
        },
    )
    job = r.json()
    ```
  </Tab>
</Tabs>

## Responses [#responses]

<Response status="202" description="Job accepted. Containers exist in queued/generating status — poll the job.">
  ```json
  {
    "jobId": "job_01HXZ9G7KMV2QX8Y1S5RJW3B7T",
    "kind": "content_generate",
    "status": "running",
    "containerIds": ["cnt_01HXZ9...", "cnt_01HXZA..."],
    "format": "video_remix",
    "locationUrl": "/v1/jobs/job_01HXZ9G7KMV2QX8Y1S5RJW3B7T"
  }
  ```
</Response>

<Response status="402" description="Project credits below the generation cost.">
  ```json
  {
    "error": {
      "code": "BILLING_EXHAUSTED",
      "message": "Insufficient content-generation credits.",
      "requestId": "req_...",
      "details": { "required": 40, "available": 12 }
    }
  }
  ```
</Response>

<Response status="409" description="Project has multiple content layers and projectLayerId wasn't passed.">
  ```json
  {
    "error": {
      "code": "CONFLICT",
      "message": "Multiple content layers on this project. Pass projectLayerId.",
      "requestId": "req_...",
      "details": { "candidates": ["lyr_01HX...", "lyr_02HX..."] }
    }
  }
  ```
</Response>

## Stages [#stages]

<Parameters
  title="Job stages (order guaranteed)"
  rows="[
  { name: 'planning', type: 'stage', description: 'Compose the shot list, caption drafts, influencer narrative.' },
  { name: 'generating_visuals', type: 'stage', description: 'Render or remix media. This is the longest stage — 30s–3min depending on format.' },
  { name: 'assembling', type: 'stage', description: 'Stitch media, overlay captions, mix audio.' },
  { name: 'finalizing', type: 'stage', description: 'Persist container; surface thumbnails; write assets to object storage.' },
]"
/>

## Notes [#notes]

<Callout type="info">
  Approval gating is enforced at schedule-time, not here. If the project has
  `requiresApproval: true`, generation still runs — the container lands in
  `approvalStatus: "pending"` and [`schedule`](/docs/api/reference/publishing/schedule-content)
  refuses it until you call [`approve`](/docs/api/reference/approval/approve-content).
</Callout>

* **`format: "auto"` is the sane default.** Only pin a format if you're A/B-testing shape, or the customer asked for a specific one. Our auto-pick uses what's been working for this project.
* **`variantCount` is per-request.** One `jobId` covers the lot; there's no fan-out across multiple jobs.

## Errors [#errors]

| Code                 | When                                                                                     |
| -------------------- | ---------------------------------------------------------------------------------------- |
| `VALIDATION_FAILED`  | Missing brief, bad enum, `variantCount > 5`.                                             |
| `CONFLICT`           | Multiple content layers and no `projectLayerId`; or `id` collision with different brief. |
| `BILLING_EXHAUSTED`  | Credits insufficient.                                                                    |
| `MODERATION_BLOCKED` | Brief failed safety.                                                                     |
| `NOT_FOUND`          | `projectLayerId` or `influencerId` not in this project.                                  |

## See also [#see-also]

* [The jobs envelope](/docs/api/concepts/jobs)
* [Regenerate a container in place](/docs/api/reference/content/regenerate-content)
* [Clone from a top-performing post](/docs/api/reference/content/clone-from-post)
* [Approve generated content](/docs/api/reference/approval/approve-content)
