# Clone a top performer (/docs/api/guides/clone-top-performer)



## What you'll build [#what-youll-build]

A focused flow: take a post that's working, ask Layers to produce more content shaped by it, and route the results through your approval queue. This is one endpoint, one mode choice, and one poll.

Prerequisite: you've published a few posts and identified a winner. If you haven't, work through [Publish to learn](/docs/api/guides/publish-to-learn) first.

<Callout type="warn">
  **This flow requires a real `sourcePlatformPostId`.** Clone-from-post only accepts posts Layers already tracks — from SIFT-synced social accounts or from your ad accounts. You cannot fabricate an id; see [Requires top-performer data](#requires-top-performer-data) below for how to confirm the source exists before you try to clone it.
</Callout>

## Why clone instead of generate from scratch [#why-clone-instead-of-generate-from-scratch]

A new generation from a fresh brief is cheap, but it throws away what you already learned. A clone preserves the things that were working — hook, pacing, influencer, structure — and rerolls the parts that didn't matter. Clone when you know the hook is landing. Generate fresh when you want a new angle.

## Fork vs reimagine [#fork-vs-reimagine]

Two modes. They trade off fidelity against variety.

| Mode        | What's preserved                                    | What's rerolled                                                          | When to use                                                               |
| ----------- | --------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
| `fork`      | Hook, caption structure, influencer, format, pacing | Visuals, shot list, minor caption edits                                  | A near-duplicate that feels fresh on the feed without burning the pattern |
| `reimagine` | Hook, rough theme                                   | Everything else — format may change, influencer may swap, pacing differs | Exploring how far the winning hook can travel                             |

Pick `fork` when you want a safe clone that keeps working. Pick `reimagine` when the hook is proven but you think the execution has more room.

<Steps>
  <Step>
    ## Call clone-from-post [#call-clone-from-post]

    <Endpoint method="POST" path="/v1/content/:containerId/clone-from-post" scope="content:write" phase="1" />

    The endpoint is target-first: you mint a fresh UUID for the new container client-side and that UUID goes in the URL path. Replay with the same id and you get the same container back — the path itself is your idempotency key. The `Idempotency-Key` header is still accepted (and recommended for retries across crashed clients), but `:containerId` alone is enough for basic dedupe.

    `sourcePlatformPostId` comes from [`/top-performers`](/docs/api/guides/publish-to-learn#find-top-performers) or your own metrics query. `variations` is how many distinct outputs to produce from this one source, all handled inside a single job (max 10).

    <Tabs items="['curl', 'TypeScript', 'Python']">
      <Tab value="curl">
        ```bash
        NEW_CONTAINER_ID=$(uuidgen)

        curl -X POST "https://api.layers.com/v1/content/$NEW_CONTAINER_ID/clone-from-post" \
          -H "Authorization: Bearer $LAYERS_API_KEY" \
          -H "Idempotency-Key: $(uuidgen)" \
          -H "Content-Type: application/json" \
          -d '{
            "projectId": "'"$PROJECT_ID"'",
            "sourcePlatformPostId": "pp_01H9ZXT7...",
            "mode": "fork",
            "variations": 2
          }'
        ```
      </Tab>

      <Tab value="TypeScript">
        ```ts
        import { randomUUID } from "node:crypto";

        const newContainerId = randomUUID();
        const res = await fetch(
          `https://api.layers.com/v1/content/${newContainerId}/clone-from-post`,
          {
            method: "POST",
            headers: {
              Authorization: `Bearer ${process.env.LAYERS_API_KEY}`,
              "Idempotency-Key": randomUUID(),
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              projectId,
              sourcePlatformPostId: "pp_01H9ZXT7...",
              mode: "fork",
              variations: 2,
            }),
          },
        );
        const { jobId, containerId, locationUrl } = await res.json();
        ```
      </Tab>

      <Tab value="Python">
        ```python
        import os
        import uuid
        import requests

        new_container_id = str(uuid.uuid4())
        res = requests.post(
            f"https://api.layers.com/v1/content/{new_container_id}/clone-from-post",
            headers={
                "Authorization": f"Bearer {os.environ['LAYERS_API_KEY']}",
                "Idempotency-Key": str(uuid.uuid4()),
                "Content-Type": "application/json",
            },
            json={
                "projectId": project_id,
                "sourcePlatformPostId": "pp_01H9ZXT7...",
                "mode": "fork",
                "variations": 2,
            },
        )
        body = res.json()
        job_id, container_id = body["jobId"], body["containerId"]
        ```
      </Tab>
    </Tabs>

    <Response status="202">
      ```json
      {
        "jobId": "job_01H9ZXV...",
        "kind": "content_clone_from_post",
        "status": "running",
        "containerId": "cnt_01H9ZXV...",
        "locationUrl": "/v1/jobs/job_01H9ZXV..."
      }
      ```
    </Response>

    One call produces one job and one container. The `variations` parameter controls how many distinct outputs the job emits onto that single container — no fan-out into multiple containers, no `jobs[]` array in the response.

    **If `NOT_FOUND` comes back with `message: "Source post not found."`**, the post either isn't in this org's scope, has been deleted from the underlying platform, or hasn't synced yet. Platform post sync runs every 30 minutes — a post that went live in the last hour may not be queryable yet. `sourcePlatformPostId` accepts both the internal `platform_posts.id` and the platform-native `external_id`.

    **If `CONFLICT` comes back with `details.layerCount > 1`**, the project has multiple content layers and you need to pass `projectLayerId` in the body to disambiguate.
  </Step>

  <Step>
    ## Poll the job [#poll-the-job]

    The job is standard — see [Jobs](/docs/api/concepts/jobs). Poll `GET /v1/jobs/:jobId` (or the `locationUrl` from the response) until `status` reaches `completed`.

    ```ts
    const { status, result } = await waitForJob(jobId);
    ```

    A completed clone returns the same container shape as `POST /v1/projects/:id/content`. Fetch it via [`GET /v1/content/:containerId`](/docs/api/reference/content/get-container) for media, captions, and approval status.

    ```json
    {
      "id": "cnt_01H9ZXV...",
      "status": "completed",
      "approvalStatus": "pending",
      "format": "video_remix",
      "hook": "Get things done. Faster than your to-do list app did yesterday.",
      "captionVariants": [
        { "platform": "tiktok", "text": "Day 14 of actually finishing my list. Try acme-todo." }
      ],
      "mediaAssets": [{ "id": "asset_01H9...", "kind": "video", "durationMs": 10800 }],
      "sourcePlatformPostId": "pp_01H9ZXT7..."
    }
    ```
  </Step>

  <Step>
    ## Route through approval [#route-through-approval]

    Cloned content respects the same approval policy as any other generation. If the project is past its `firstNPostsBlocked` count, clones land approved; before it, they sit as `pending` until you flip them.

    Don't auto-approve clones just because the source is a winner. The clone inherits structural DNA, not the outcome — a fork that copies the hook badly can still flop, and approval is the only gate that catches that before it's live.

    Once approved, schedule as usual: [`POST /v1/content/:containerId/schedule`](/docs/api/guides/onboard-customer#schedule-the-first-post).
  </Step>
</Steps>

## Requires top-performer data [#requires-top-performer-data]

`sourcePlatformPostId` has to resolve to a post Layers already tracks. On a fresh project with no social accounts linked and no ad creatives, there's nothing to clone — `/top-performers` will return an empty `items` array and this endpoint will 404 on any id you invent. Confirm the source exists before you try:

```bash
# Is this platform post in scope for your key?
curl "https://api.layers.com/v1/projects/:projectId/top-performers?limit=25" \
  -H "X-Api-Key: $LAYERS_API_KEY"
```

The `items[].platformPostId` values you get back are the only ones `clone-from-post` will accept — that's the authoritative list of what Layers knows about for a given project.

* Empty `items` → no social accounts linked yet, or platform sync hasn't finished the first pass. Connect at least one account ([social-accounts OAuth flow](/docs/api/reference/social-accounts/oauth-url)) and wait one sync cycle (≈30 min).
* `/top-performers` returns an id but `clone-from-post` still 404s → the post exists but was soft-deleted by a revoke. Reconnect the social account and resync.
* Row exists in a different org → the API will 404 with `"Source post not found."`. That is not a bug — we refuse cross-tenant reads on purpose.

## When clones stop helping [#when-clones-stop-helping]

If you've cloned the same source post more than 4–5 times and none of the clones outperform the original, the hook has saturated. Switch to a fresh brief with a new hook — ideally one pulled from the customer's brand context or from a second-tier top performer whose signal is growing.

## What's next [#whats-next]

<Cards>
  <Card title="Publish-to-learn loop" href="/docs/api/guides/publish-to-learn" description="How you found the top performer in the first place." />

  <Card title="Content items" href="/docs/api/concepts/content-items" description="The full lifecycle of a content container — from generation through publish." />

  <Card title="Jobs" href="/docs/api/concepts/jobs" description="The polling envelope used here and everywhere else." />
</Cards>
