Clone a top performer
Fork or reimagine a winning post into new variants without rewriting the brief from scratch.
What you'll 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 first.
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 below for how to confirm the source exists before you try to clone it.
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
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.
Call clone-from-post
/v1/content/:containerId/clone-from-post- Auth
- Bearer
- Scope
- content:write
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 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).
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
}'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();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"]{
"jobId": "job_01H9ZXV...",
"kind": "content_clone_from_post",
"status": "running",
"containerId": "cnt_01H9ZXV...",
"locationUrl": "/v1/jobs/job_01H9ZXV..."
}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.
Poll the job
The job is standard — see Jobs. Poll GET /v1/jobs/:jobId (or the locationUrl from the response) until status reaches completed.
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 for media, captions, and approval status.
{
"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..."
}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.
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:
# 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) and wait one sync cycle (≈30 min). /top-performersreturns an id butclone-from-poststill 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
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.