GET /v1/projects/:projectId/content/source-recommendations
List TikTok video and slideshow ids the partner can pass to video-remix or slideshow-remix.
/v1/projects/:projectId/content/source-recommendations- Auth
- Bearer
- Scope
- content:read
Returns TikTok ids the partner's end-user can pick from. Two backing modes:
| Mode | Triggered by | Source |
|---|---|---|
| Portfolio (default) | No keyword query param | Pre-discovered posts from the project's content-research pipeline. |
| Live search | ?keyword=<string> | Real-time TikTok keyword search. |
Both modes normalize to the same item shape — partners don't branch on source for rendering. The field is informational only ("from your library" vs "live search results").
Path parameters
projectIdstring (uuid)requiredThe project.
Query parameters
keywordstringoptionalOptional. When omitted, returns portfolio results. When supplied, switches to live search against that term. 1–200 chars.kindstringoptionalFilter by source kind. Default `mixed`.One of:video,slideshow,mixedlimitintegeroptionalPage size. Default 20.One of:1–50cursorstringoptionalForward-paginate. Currently always `null` in responses; reserved for future use.
Request
# Portfolio mode (default)
curl "https://api.layers.com/v1/projects/{projectId}/content/source-recommendations?kind=video&limit=20" \
-H "Authorization: Bearer $LAYERS_API_KEY"
# Live search
curl "https://api.layers.com/v1/projects/{projectId}/content/source-recommendations?keyword=fitness%20app" \
-H "Authorization: Bearer $LAYERS_API_KEY"const res = await fetch(
`https://api.layers.com/v1/projects/${projectId}/content/source-recommendations?keyword=fitness+app`,
{ headers: { 'Authorization': `Bearer ${process.env.LAYERS_API_KEY}` } },
);
const { items, source } = await res.json();import os, httpx
r = httpx.get(
f"https://api.layers.com/v1/projects/{project_id}/content/source-recommendations",
params={"keyword": "fitness app"},
headers={"Authorization": f"Bearer {os.environ['LAYERS_API_KEY']}"},
)
items = r.json()["items"]Responses
{
"items": [
{
"tiktokId": "7234567890123456789",
"kind": "video",
"thumbnailUrl": "https://...",
"caption": "...",
"author": { "username": "creator_handle", "displayName": "..." },
"stats": { "views": 1240000, "likes": 89000, "comments": 4100, "shares": 12000 }
},
{
"tiktokId": "7234567890987654321",
"kind": "slideshow",
"thumbnailUrl": "https://...",
"caption": "...",
"author": { "username": "creator_handle", "displayName": "..." },
"stats": { "views": 800000, "likes": 56000, "comments": 1200, "shares": 4400 },
"imageCount": 7
}
],
"source": "portfolio",
"nextCursor": null
}source is "portfolio" when the response was served from the project's pre-discovered library, "live" when served from a real-time keyword search.
Portfolio mode draws from the project's pre-discovered library
(synced posts + previously-generated containers). Fresh projects
typically return a small seeded set immediately after creation while
the first research sweep runs in the background; the library fills
out as posts are synced. Projects that genuinely have no library yet
return an empty items array with source: "portfolio" — fall back
to a keyword search in that case (or surface the empty state in
your UI).
Errors
| Code | When |
|---|---|
VALIDATION | limit outside 1–50; kind not in the enum; keyword empty or > 200 chars. |
NOT_FOUND | Project id not in this org. |