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).
An empty items array always means "nothing matched", never "we
could not look". If the upstream source provider is unreachable this
endpoint returns PLATFORM_ERROR, so you can treat an empty success
as a real answer and retry the error separately.
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. |
PLATFORM_ERROR | The upstream source provider failed. details.platform is tiktok; details.platformMessage carries the provider's own reason. Retryable with backoff. |