API reference
Your posts, wherever you need them.
A versioned REST API over the same data your dashboard reads. Create a key, call it from your server, and render your work anywhere.
Building with an AI assistant? Copy the whole reference as markdown and paste it in. It has everything it needs, including the caching rules.
curl "https://topics.so/api/v1/posts?perPage=5" \
-H "Authorization: Bearer $TOPICS_API_KEY"Overview
Every endpoint lives under one base URL and returns JSON. Requests are scoped to the brand that owns the key, so a key only ever sees its own posts, its own metrics, and nothing belonging to anyone else.
List endpoints answer with a data array and a pagination object. Single-resource endpoints answer with the object itself.
The version sits in the path. Anything that would break an existing integration ships as a new version, so v1 keeps working.
https://topics.so/api{
"data": [
"…"
],
"pagination": {
"page": 1,
"perPage": 20,
"total": 128,
"pageCount": 7
}
}Build with AI
Most integrations here are written by an assistant, so the whole reference is published as one markdown file at /docs/api.md. Copy it, paste it into Claude or ChatGPT, and describe what you want built.
Assistants that fetch their own context will find /llms.txt at the root, following the llmstxt.org convention, and the OpenAPI document at /api/v1/openapi.json.
The markdown is generated from the same definitions as this page, so it is never out of date, and it states the two things assistants get wrong most often: the key belongs on the server, and the responses need caching.
Build a Topics.so API integration.
The complete reference is at:
https://topics.so/docs/api.md
Fetch it, follow it exactly, and pay attention
to the caching and server-side key rules.https://topics.so/llms.txt
https://topics.so/docs/api.md
https://topics.so/api/v1/openapi.jsonAuthentication
Create a key on the API page of your dashboard. The secret is shown once, at the moment it is created, and never again. Send it as a bearer token on every request.
A key is a secret. Call the API from your server and keep the key in an environment variable. Never ship it in browser code, where anyone can read it.
Each key carries scopes that decide what it can reach. A request for something outside a key's scopes returns 403 forbidden. Revoking a key in the dashboard takes effect immediately.
Authorization: Bearer tso_G_USxN6rS4rzt0Enh3B_-uxinWqPhJHa{
"error": {
"code": "unauthorized",
"message": "That API key is not valid."
}
}Rate limits
Limits are per brand and scale with your plan. Requests are counted per second, and again across the calendar month.
| Plan | Per second | Per month | Keys |
|---|---|---|---|
| Free | 1 | 500 | 1 |
| Creator | 5 | 25,000 | 5 |
| Studio | 20 | 250,000 | 20 |
Every response reports where you stand on both, so a client can pace itself instead of waiting to be turned away.
| Header | Meaning |
|---|---|
| X-RateLimit-Limit | Requests allowed in one second. |
| X-RateLimit-Remaining | Requests left in the current second. |
| X-RateLimit-Reset | Unix time, in seconds, when the current window resets. |
| X-Quota-Limit | Requests allowed this calendar month. |
| X-Quota-Remaining | Requests left this calendar month. |
| Retry-After | Seconds to wait before retrying. Sent with both 429 responses. |
Going over the per-second limit returns 429 rateLimitExceeded, and the window reopens the next second. Using up the monthly quota returns 429 quotaExceeded, which clears at the start of the next month.
HTTP/1.1 200 OK
X-RateLimit-Limit: 1
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1785115680
X-Quota-Limit: 500
X-Quota-Remaining: 483Caching
The metrics behind this API are refreshed every 30 minutes. Asking for the same data more often than that cannot return anything new, it only spends quota. Cache the responses and the free plan goes a long way; skip caching and 500 requests a month disappears in an afternoon.
- Cache for at least 30 minutes. An hour or a day is entirely reasonable for published-post data.
- Cache the rendered page, not just the HTTP call, so a traffic spike cannot become an API spike.
- Fetch at build time or on a schedule where you can, rather than on every request.
- Page in bulk.
perPage=100reads a hundred posts in one request;perPage=10spends ten times the quota for the same posts. - Serve the last good response if a request fails, so a 429 degrades to slightly stale data instead of a broken page.
Throttled requests do not consume monthly quota, so a retry after a 429 costs nothing but the wait.
const response = await fetch("https://topics.so/api/v1/posts?perPage=100", {
headers: { Authorization: `Bearer ${process.env.TOPICS_API_KEY}` },
// One request an hour, however much traffic the page gets.
next: { revalidate: 3600 }
});
const { data, pagination } = await response.json();let cache = { at: 0, data: null };
const TTL_MS = 60 * 60 * 1000;
export async function getPosts() {
if (cache.data && Date.now() - cache.at < TTL_MS) return cache.data;
const response = await fetch("https://topics.so/api/v1/posts?perPage=100", {
headers: { Authorization: `Bearer ${process.env.TOPICS_API_KEY}` }
});
if (!response.ok) {
// Serve stale data rather than break the page on a 429.
if (cache.data) return cache.data;
throw new Error(`Topics.so API ${response.status}`);
}
cache = { at: Date.now(), data: await response.json() };
return cache.data;
}Errors
Every failure returns the same envelope. Branch on code: the message is written for people and may change.
| Code | Status | Meaning |
|---|---|---|
unauthorized | 401 | No key was sent, or the key is unknown or revoked. |
forbidden | 403 | The key is valid but lacks a scope the endpoint requires. |
notFound | 404 | Nothing with that id exists in this brand. |
validationError | 400 | A parameter or body field is missing, malformed, or out of range. |
rateLimitExceeded | 429 | Too many requests in one minute. Retry after the window resets. |
quotaExceeded | 429 | The monthly request quota is used up. It resets at the start of next month. |
creditsExhausted | 402 | The brand is out of image credits for the month. Upgrade the plan for more. |
conflict | 409 | The resource isn't in a state that allows this (already publishing, not failed, and so on). |
serviceUnavailable | 503 | An upstream dependency didn't answer. Safe to retry shortly. |
{
"error": {
"code": "rateLimitExceeded",
"message": "Too many requests. Slow down and try again shortly."
}
}MCP server
Agents get the same API as first-class tools. The MCP server lives at https://topics.so/api/mcp (streamable HTTP) and works with Claude Code, Cursor, and anything else that speaks MCP: no docs to paste, no glue code to write.
Authentication is the same bearer key, sent as a header. Every tool call passes through the same scopes, rate limits, and quotas as the REST API, and revoking the key shuts off both at once.
| Tools | Requires |
|---|---|
| get_brand, list_posts, get_post, list_series, get_series_queue, list_assets, list_cast_members | postsRead / createRead |
| compose_prompt, generate_image, set_brand_kit, add_asset, create_asset_upload, confirm_asset_upload, save_cast_member, delete_cast_member, save_series, set_series_active, run_autopilot_review, skip_item, regenerate_item, generate_item, rerender_overlay, publish_item | createWrite |
| publish_item | publishWrite |
| list_organizations, create_organization, update_organization | brandsWrite |
Keep the key out of committed files. Reference it as an environment variable in .mcp.json, exactly as the sample shows.
claude mcp add --transport http topics https://topics.so/api/mcp \
--header "Authorization: Bearer $TOPICS_API_KEY"{
"mcpServers": {
"topics": {
"type": "http",
"url": "https://topics.so/api/mcp",
"headers": {
"Authorization": "Bearer ${TOPICS_API_KEY}"
}
}
}
}TypeScript SDK
@topics-so/sdk is a typed client for Node.js and server-side JavaScript, generated from the OpenAPI spec below so it can never disagree with the API.
It adds one convenience worth having: generateImage composes a prompt from your idea, spends the credit, and waits for the finished image, so the whole flow is a single call. Pass idempotencyKey and a retried call can never spend twice.
Everything else is available through topics.api, one typed method per endpoint. The key rules from Authentication apply: server-side only, never in browser code.
pnpm add @topics-so/sdkimport { TopicsClient } from "@topics-so/sdk";
const topics = new TopicsClient({ apiKey: process.env.TOPICS_API_KEY! });
// One call: composes the prompt, spends one credit, waits for the image.
const image = await topics.generateImage({
idea: "Morning espresso ritual, warm window light",
aspectRatio: "landscape"
});
console.log(image.url);// Everything else, fully typed from the OpenAPI spec:
const { data } = await topics.api.GET("/v1/series");
await topics.api.PATCH("/v1/series/{id}", {
params: { path: { id: "…" } },
body: { guidance: "Lean on the dose calculator." }
});List posts
/api/v1/postsEvery post across your connected accounts, newest first by default. Filter, search, sort, and page through them the same way the dashboard does. Each post carries its latest metrics.
Requires the postsRead scope.
Query parameters
pageintegeroptional- Page number, starting at 1.Defaults to 1.
perPageintegeroptional- Posts per page.Defaults to 20. Maximum 100.
qstringoptional- Free-text search across title, caption, and cover text.
fromstringoptional- Only posts published on or after this date (yyyy-mm-dd).
tostringoptional- Only posts published on or before this date (yyyy-mm-dd).
platformstringoptional- Restrict to one platform.One of
instagram, linkedin, youtube. formatstringoptional- Restrict to one content format.One of
carousel, image, reel, story, video. hashtagstringoptional- Restrict to posts carrying this hashtag, without the leading #.
sortstringoptional- Field to sort on.One of
published, views, impressions, clicks, comments, engagementRate. Defaults to published. dirstringoptional- Sort direction.One of
asc, desc. Defaults to desc.
curl "https://topics.so/api/v1/posts?perPage=5&platform=instagram" \
-H "Authorization: Bearer $TOPICS_API_KEY"{
"data": [
{
"id": "cm8xk21p40001s6014r9e2n7q",
"platform": "instagram",
"format": "carousel",
"title": "Three openings that land",
"url": "https://www.instagram.com/p/DBv2Qm1Nq8x/",
"caption": "Three ways to open a talk that actually land. #publicspeaking #storytelling",
"hashtags": [
"publicspeaking",
"storytelling"
],
"thumbnailUrl": "https://media.topics.so/posts/cm8xk21p40001/cover.jpg",
"mediaUrls": [
"https://media.topics.so/posts/cm8xk21p40001/1.jpg",
"https://media.topics.so/posts/cm8xk21p40001/2.jpg"
],
"publishedAt": "2026-07-11T15:02:11.000Z",
"metrics": {
"views": 18420,
"impressions": 21031,
"reach": 15877,
"likes": 1204,
"comments": 86,
"shares": 143,
"saves": 219,
"clicks": null,
"engagementRate": 0.0786,
"capturedAt": "2026-07-24T06:00:00.000Z"
}
}
],
"pagination": {
"page": 1,
"perPage": 5,
"total": 128,
"pageCount": 26
}
}Retrieve a post
/api/v1/posts/{id}One post by id, with its full metric history. Snapshots are cumulative lifetime readings, oldest first, so a daily series is the difference between consecutive entries.
Requires the postsRead scope.
Path parameters
idstringrequired- The post id, as returned by the list endpoint.
curl "https://topics.so/api/v1/posts/cm8xk21p40001s6014r9e2n7q" \
-H "Authorization: Bearer $TOPICS_API_KEY"{
"id": "cm8xk21p40001s6014r9e2n7q",
"platform": "instagram",
"format": "carousel",
"title": "Three openings that land",
"url": "https://www.instagram.com/p/DBv2Qm1Nq8x/",
"caption": "Three ways to open a talk that actually land. #publicspeaking #storytelling",
"hashtags": [
"publicspeaking",
"storytelling"
],
"thumbnailUrl": "https://media.topics.so/posts/cm8xk21p40001/cover.jpg",
"mediaUrls": [
"https://media.topics.so/posts/cm8xk21p40001/1.jpg",
"https://media.topics.so/posts/cm8xk21p40001/2.jpg"
],
"publishedAt": "2026-07-11T15:02:11.000Z",
"metrics": {
"views": 18420,
"impressions": 21031,
"reach": 15877,
"likes": 1204,
"comments": 86,
"shares": 143,
"saves": 219,
"clicks": null,
"engagementRate": 0.0786,
"capturedAt": "2026-07-24T06:00:00.000Z"
},
"snapshots": [
{
"views": 12045,
"impressions": 14210,
"reach": 15877,
"likes": 1204,
"comments": 86,
"shares": 143,
"saves": 219,
"clicks": null,
"engagementRate": 0.0721,
"capturedAt": "2026-07-17T06:00:00.000Z"
},
{
"views": 18420,
"impressions": 21031,
"reach": 15877,
"likes": 1204,
"comments": 86,
"shares": 143,
"saves": 219,
"clicks": null,
"engagementRate": 0.0786,
"capturedAt": "2026-07-24T06:00:00.000Z"
}
]
}Get the brand kit
/api/v1/brand-kitThe brand's Create settings: the base style prompt prepended to every composed prompt, and the Discord webhook for digests and alerts.
Requires the createRead scope.
curl "https://topics.so/api/v1/brand-kit" \
-H "Authorization: Bearer $TOPICS_API_KEY"{
"baseStylePrompt": "Warm editorial lifestyle photography, natural light, shallow depth of field.",
"discordWebhookUrl": null
}Update the brand kit
/api/v1/brand-kitReplaces the brand kit. Send an empty string to clear a field.
Requires the createWrite scope.
curl "https://topics.so/api/v1/brand-kit" \
-X PUT \
-H "Authorization: Bearer $TOPICS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"baseStylePrompt":"Warm editorial lifestyle photography, natural light, shallow depth of field.","discordWebhookUrl":""}'{
"baseStylePrompt": "Warm editorial lifestyle photography, natural light, shallow depth of field.",
"discordWebhookUrl": ""
}{
"baseStylePrompt": "Warm editorial lifestyle photography, natural light, shallow depth of field.",
"discordWebhookUrl": null
}Get image credits
/api/v1/creditsThe brand's image-credit position for the current month. Generation debits these, not the request quota.
Requires the createRead scope.
curl "https://topics.so/api/v1/credits" \
-H "Authorization: Bearer $TOPICS_API_KEY"{
"allowance": 200,
"used": 46,
"remaining": 154
}List assets
/api/v1/assetsThe brand's Create asset library: logos, inspiration images, and cast reference photos.
Requires the createRead scope.
Query parameters
kindstringoptional- Restrict to one kind.One of
logo, inspiration, castReference.
curl "https://topics.so/api/v1/assets?kind=castReference" \
-H "Authorization: Bearer $TOPICS_API_KEY"{
"data": [
{
"id": "cmb2n81xq0003s601wdkq5r2a",
"kind": "castReference",
"name": "Maya, front-lit selfie",
"contentType": "image/jpeg",
"url": "https://media.topics.so/media/assets/org1/V1t2u3w4x5y6z7a8.jpg",
"width": 1600,
"height": 2000,
"isPrimary": false,
"createdAt": "2026-08-02T10:12:00.000Z"
}
]
}Add an asset
/api/v1/assetsAdds an image to the library. Send `sourceUrl` and it is fetched server-side from that public URL (JPEG, PNG, or WebP, up to 10 MB). Alternatively, finish a presigned upload by sending the `key` and `url` returned by the upload endpoint.
Requires the createWrite scope.
curl "https://topics.so/api/v1/assets" \
-X POST \
-H "Authorization: Bearer $TOPICS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"kind":"castReference","name":"Maya, front-lit selfie","sourceUrl":"https://example.com/maya-front.jpg"}'{
"kind": "castReference",
"name": "Maya, front-lit selfie",
"sourceUrl": "https://example.com/maya-front.jpg"
}{
"id": "cmb2n81xq0003s601wdkq5r2a",
"kind": "castReference",
"name": "Maya, front-lit selfie",
"contentType": "image/jpeg",
"url": "https://media.topics.so/media/assets/org1/V1t2u3w4x5y6z7a8.jpg",
"width": 1600,
"height": 2000,
"isPrimary": false,
"createdAt": "2026-08-02T10:12:00.000Z"
}Start a direct upload
/api/v1/assets/uploadsStep 1 of a direct upload: returns a presigned `uploadUrl`. PUT the raw bytes there with the same Content-Type, then POST `/v1/assets` with the returned `key` and `url`.
Requires the createWrite scope.
curl "https://topics.so/api/v1/assets/uploads" \
-X POST \
-H "Authorization: Bearer $TOPICS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"kind":"inspiration","name":"Balcony light study","contentType":"image/jpeg","size":482133}'{
"kind": "inspiration",
"name": "Balcony light study",
"contentType": "image/jpeg",
"size": 482133
}{
"uploadUrl": "https://s3.amazonaws.com/bucket/media/assets/org1/V1t2u3w4x5y6z7a8.jpg?X-Amz-Signature=…",
"url": "https://media.topics.so/media/assets/org1/V1t2u3w4x5y6z7a8.jpg",
"key": "media/assets/org1/V1t2u3w4x5y6z7a8.jpg",
"kind": "inspiration",
"name": "Balcony light study",
"contentType": "image/jpeg"
}Delete an asset
/api/v1/assets/{id}Soft-deletes one asset. Cast members and creations that referenced it keep working; the photo simply drops out of their reference sets.
Requires the createWrite scope.
Path parameters
idstringrequired- The asset id.
curl "https://topics.so/api/v1/assets/cmb2n81xq0003s601wdkq5r2a" \
-X DELETE \
-H "Authorization: Bearer $TOPICS_API_KEY"204 No ContentList cast members
/api/v1/cast-membersThe recurring people this brand casts in its images, each with their reference photos.
Requires the createRead scope.
curl "https://topics.so/api/v1/cast-members" \
-H "Authorization: Bearer $TOPICS_API_KEY"{
"data": [
{
"id": "cmb2n9f2t0005s601m3hq8v1d",
"name": "Maya",
"lookPrompt": "24, lean build, dark curly shoulder-length hair, warm brown eyes, minimal athleisure wardrobe",
"lifestylePrompt": "Lives in Lisbon; boutique gyms, espresso bars, coastal drives",
"references": [
{
"id": "cmb2n81xq0003s601wdkq5r2a",
"kind": "castReference",
"name": "Maya, front-lit selfie",
"contentType": "image/jpeg",
"url": "https://media.topics.so/media/assets/org1/V1t2u3w4x5y6z7a8.jpg",
"width": 1600,
"height": 2000,
"isPrimary": false,
"createdAt": "2026-08-02T10:12:00.000Z"
}
],
"active": true,
"createdAt": "2026-08-02T10:15:00.000Z"
}
]
}Create a cast member
/api/v1/cast-membersA cast member is a name, a look reproduced verbatim in every prompt, an optional lifestyle, and up to four reference photos (assets of kind `castReference`). Pin them to a creation or a series for a consistent face.
Requires the createWrite scope.
curl "https://topics.so/api/v1/cast-members" \
-X POST \
-H "Authorization: Bearer $TOPICS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Maya","lookPrompt":"24, lean build, dark curly shoulder-length hair, warm brown eyes, minimal athleisure wardrobe","lifestylePrompt":"Lives in Lisbon; boutique gyms, espresso bars, coastal drives","referenceAssetIds":["cmb2n81xq0003s601wdkq5r2a"]}'{
"name": "Maya",
"lookPrompt": "24, lean build, dark curly shoulder-length hair, warm brown eyes, minimal athleisure wardrobe",
"lifestylePrompt": "Lives in Lisbon; boutique gyms, espresso bars, coastal drives",
"referenceAssetIds": [
"cmb2n81xq0003s601wdkq5r2a"
]
}{
"id": "cmb2n9f2t0005s601m3hq8v1d",
"name": "Maya",
"lookPrompt": "24, lean build, dark curly shoulder-length hair, warm brown eyes, minimal athleisure wardrobe",
"lifestylePrompt": "Lives in Lisbon; boutique gyms, espresso bars, coastal drives",
"references": [
{
"id": "cmb2n81xq0003s601wdkq5r2a",
"kind": "castReference",
"name": "Maya, front-lit selfie",
"contentType": "image/jpeg",
"url": "https://media.topics.so/media/assets/org1/V1t2u3w4x5y6z7a8.jpg",
"width": 1600,
"height": 2000,
"isPrimary": false,
"createdAt": "2026-08-02T10:12:00.000Z"
}
],
"active": true,
"createdAt": "2026-08-02T10:15:00.000Z"
}Retrieve a cast member
/api/v1/cast-members/{id}One cast member by id.
Requires the createRead scope.
Path parameters
idstringrequired- The cast member id.
curl "https://topics.so/api/v1/cast-members/cmb2n9f2t0005s601m3hq8v1d" \
-H "Authorization: Bearer $TOPICS_API_KEY"{
"id": "cmb2n9f2t0005s601m3hq8v1d",
"name": "Maya",
"lookPrompt": "24, lean build, dark curly shoulder-length hair, warm brown eyes, minimal athleisure wardrobe",
"lifestylePrompt": "Lives in Lisbon; boutique gyms, espresso bars, coastal drives",
"references": [
{
"id": "cmb2n81xq0003s601wdkq5r2a",
"kind": "castReference",
"name": "Maya, front-lit selfie",
"contentType": "image/jpeg",
"url": "https://media.topics.so/media/assets/org1/V1t2u3w4x5y6z7a8.jpg",
"width": 1600,
"height": 2000,
"isPrimary": false,
"createdAt": "2026-08-02T10:12:00.000Z"
}
],
"active": true,
"createdAt": "2026-08-02T10:15:00.000Z"
}Update a cast member
/api/v1/cast-members/{id}Partial update: fields you omit keep their stored value.
Requires the createWrite scope.
Path parameters
idstringrequired- The cast member id.
curl "https://topics.so/api/v1/cast-members/cmb2n9f2t0005s601m3hq8v1d" \
-X PATCH \
-H "Authorization: Bearer $TOPICS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"lifestylePrompt":"Moved to Porto; surf mornings, studio afternoons"}'{
"lifestylePrompt": "Moved to Porto; surf mornings, studio afternoons"
}{
"id": "cmb2n9f2t0005s601m3hq8v1d",
"name": "Maya",
"lookPrompt": "24, lean build, dark curly shoulder-length hair, warm brown eyes, minimal athleisure wardrobe",
"lifestylePrompt": "Lives in Lisbon; boutique gyms, espresso bars, coastal drives",
"references": [
{
"id": "cmb2n81xq0003s601wdkq5r2a",
"kind": "castReference",
"name": "Maya, front-lit selfie",
"contentType": "image/jpeg",
"url": "https://media.topics.so/media/assets/org1/V1t2u3w4x5y6z7a8.jpg",
"width": 1600,
"height": 2000,
"isPrimary": false,
"createdAt": "2026-08-02T10:12:00.000Z"
}
],
"active": true,
"createdAt": "2026-08-02T10:15:00.000Z"
}Delete a cast member
/api/v1/cast-members/{id}Retires (soft-deletes) a cast member. Past creations keep their snapshots; a series still pointing at them falls back to its own inspiration set.
Requires the createWrite scope.
Path parameters
idstringrequired- The cast member id.
curl "https://topics.so/api/v1/cast-members/cmb2n9f2t0005s601m3hq8v1d" \
-X DELETE \
-H "Authorization: Bearer $TOPICS_API_KEY"204 No ContentList creations
/api/v1/creationsThe brand's generated images, newest first.
Requires the createRead scope.
Query parameters
pageintegeroptional- Page number, starting at 1.Defaults to 1.
perPageintegeroptional- Creations per page.Defaults to 24. Maximum 100.
curl "https://topics.so/api/v1/creations?perPage=10" \
-H "Authorization: Bearer $TOPICS_API_KEY"{
"data": [
{
"id": "cmb2nbqoe0007s601k2f9d3xz",
"status": "complete",
"aspectRatio": "landscape",
"idea": "Morning mobility routine on a sunlit balcony",
"prompt": "Editorial lifestyle photo of Maya (24, lean build, dark curly shoulder-length hair) mid-stretch on a sunlit Lisbon balcony…",
"overlay": {
"template": "captionBar",
"text": "Three openings that land",
"showLogo": true
},
"url": "https://media.topics.so/media/creations/org1/cmb2nbqoe0007-final-a1b2c3.jpg",
"rawUrl": "https://media.topics.so/media/creations/org1/cmb2nbqoe0007-raw-a1b2c3.jpg",
"error": null,
"creditCost": 1,
"castMemberId": "cmb2n9f2t0005s601m3hq8v1d",
"createdAt": "2026-08-14T08:00:02.000Z",
"completedAt": "2026-08-14T08:00:41.000Z"
}
],
"pagination": {
"page": 1,
"perPage": 10,
"total": 46,
"pageCount": 5
}
}Compose a prompt
/api/v1/creations/composeExpands a rough idea into a full image prompt using the brand's base style and, when given, a cast member's look. Free: credits are only spent when the prompt is generated. Editing the returned prompt before generating is expected.
Requires the createWrite scope.
curl "https://topics.so/api/v1/creations/compose" \
-X POST \
-H "Authorization: Bearer $TOPICS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"idea":"Morning mobility routine on a sunlit balcony","castMemberId":"cmb2n9f2t0005s601m3hq8v1d"}'{
"idea": "Morning mobility routine on a sunlit balcony",
"castMemberId": "cmb2n9f2t0005s601m3hq8v1d"
}{
"idea": "Morning mobility routine on a sunlit balcony",
"prompt": "Editorial lifestyle photo of Maya (24, lean build, dark curly shoulder-length hair) mid-stretch on a sunlit Lisbon balcony…"
}Generate an image
/api/v1/creationsStarts a generation and answers 202 with the pending creation; the brand's base style, cast look, and reference photos are applied server-side. Debits one image credit. Poll the single-creation endpoint (ideally with `wait`) until `status` is `complete`. Send an `Idempotency-Key` header to make retries safe: a replay answers 200 with the original creation instead of debiting again.
Requires the createWrite scope.
curl "https://topics.so/api/v1/creations" \
-X POST \
-H "Authorization: Bearer $TOPICS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt":"Editorial lifestyle photo of Maya (24, lean build, dark curly shoulder-length hair) mid-stretch on a sunlit Lisbon balcony…","idea":"Morning mobility routine on a sunlit balcony","aspectRatio":"landscape","castMemberId":"cmb2n9f2t0005s601m3hq8v1d","overlay":{"template":"captionBar","text":"Three openings that land","showLogo":true}}'{
"prompt": "Editorial lifestyle photo of Maya (24, lean build, dark curly shoulder-length hair) mid-stretch on a sunlit Lisbon balcony…",
"idea": "Morning mobility routine on a sunlit balcony",
"aspectRatio": "landscape",
"castMemberId": "cmb2n9f2t0005s601m3hq8v1d",
"overlay": {
"template": "captionBar",
"text": "Three openings that land",
"showLogo": true
}
}{
"id": "cmb2nbqoe0007s601k2f9d3xz",
"status": "pending",
"aspectRatio": "landscape",
"idea": "Morning mobility routine on a sunlit balcony",
"prompt": "Editorial lifestyle photo of Maya (24, lean build, dark curly shoulder-length hair) mid-stretch on a sunlit Lisbon balcony…",
"overlay": {
"template": "captionBar",
"text": "Three openings that land",
"showLogo": true
},
"url": null,
"rawUrl": null,
"error": null,
"creditCost": 1,
"castMemberId": "cmb2n9f2t0005s601m3hq8v1d",
"createdAt": "2026-08-14T08:00:02.000Z",
"completedAt": null
}Retrieve a creation
/api/v1/creations/{id}One creation by id. Pass `wait` to long-poll: the response is held until the creation completes or fails, or the wait runs out, so a generation resolves in one request instead of a polling loop.
Requires the createRead scope.
Path parameters
idstringrequired- The creation id.
Query parameters
waitintegeroptional- Seconds to hold the response for a terminal status.Defaults to 0. Maximum 60.
curl "https://topics.so/api/v1/creations/cmb2nbqoe0007s601k2f9d3xz?wait=60" \
-H "Authorization: Bearer $TOPICS_API_KEY"{
"id": "cmb2nbqoe0007s601k2f9d3xz",
"status": "complete",
"aspectRatio": "landscape",
"idea": "Morning mobility routine on a sunlit balcony",
"prompt": "Editorial lifestyle photo of Maya (24, lean build, dark curly shoulder-length hair) mid-stretch on a sunlit Lisbon balcony…",
"overlay": {
"template": "captionBar",
"text": "Three openings that land",
"showLogo": true
},
"url": "https://media.topics.so/media/creations/org1/cmb2nbqoe0007-final-a1b2c3.jpg",
"rawUrl": "https://media.topics.so/media/creations/org1/cmb2nbqoe0007-raw-a1b2c3.jpg",
"error": null,
"creditCost": 1,
"castMemberId": "cmb2n9f2t0005s601m3hq8v1d",
"createdAt": "2026-08-14T08:00:02.000Z",
"completedAt": "2026-08-14T08:00:41.000Z"
}Retry a creation
/api/v1/creations/{id}/retryRe-runs a failed creation. Free while attempts remain; once they're spent, it debits a fresh credit and resets the attempt budget.
Requires the createWrite scope.
Path parameters
idstringrequired- The creation id.
curl "https://topics.so/api/v1/creations/cmb2nbqoe0007s601k2f9d3xz/retry" \
-X POST \
-H "Authorization: Bearer $TOPICS_API_KEY"{
"id": "cmb2nbqoe0007s601k2f9d3xz",
"status": "pending",
"aspectRatio": "landscape",
"idea": "Morning mobility routine on a sunlit balcony",
"prompt": "Editorial lifestyle photo of Maya (24, lean build, dark curly shoulder-length hair) mid-stretch on a sunlit Lisbon balcony…",
"overlay": {
"template": "captionBar",
"text": "Three openings that land",
"showLogo": true
},
"url": null,
"rawUrl": null,
"error": null,
"creditCost": 1,
"castMemberId": "cmb2n9f2t0005s601m3hq8v1d",
"createdAt": "2026-08-14T08:00:02.000Z",
"completedAt": null
}Re-render the overlay
/api/v1/creations/{id}/overlaySwaps or removes a creation's overlay, re-rendered from the stored raw image. Free: no model call, no credits. Send `"overlay": null` to remove it.
Requires the createWrite scope.
Path parameters
idstringrequired- The creation id.
curl "https://topics.so/api/v1/creations/cmb2nbqoe0007s601k2f9d3xz/overlay" \
-X POST \
-H "Authorization: Bearer $TOPICS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"overlay":{"template":"centeredTitle","text":"Move first, coffee second"}}'{
"overlay": {
"template": "centeredTitle",
"text": "Move first, coffee second"
}
}{
"id": "cmb2nbqoe0007s601k2f9d3xz",
"status": "complete",
"aspectRatio": "landscape",
"idea": "Morning mobility routine on a sunlit balcony",
"prompt": "Editorial lifestyle photo of Maya (24, lean build, dark curly shoulder-length hair) mid-stretch on a sunlit Lisbon balcony…",
"overlay": {
"template": "centeredTitle",
"text": "Move first, coffee second"
},
"url": "https://media.topics.so/media/creations/org1/cmb2nbqoe0007-final-a1b2c3.jpg",
"rawUrl": "https://media.topics.so/media/creations/org1/cmb2nbqoe0007-raw-a1b2c3.jpg",
"error": null,
"creditCost": 1,
"castMemberId": "cmb2n9f2t0005s601m3hq8v1d",
"createdAt": "2026-08-14T08:00:02.000Z",
"completedAt": "2026-08-14T08:00:41.000Z"
}Delete a creation
/api/v1/creations/{id}Soft-deletes one creation.
Requires the createWrite scope.
Path parameters
idstringrequired- The creation id.
curl "https://topics.so/api/v1/creations/cmb2nbqoe0007s601k2f9d3xz" \
-X DELETE \
-H "Authorization: Bearer $TOPICS_API_KEY"204 No ContentList series
/api/v1/seriesEvery series, manual and autopilot, including each autopilot's current playbook.
Requires the createRead scope.
curl "https://topics.so/api/v1/series" \
-H "Authorization: Bearer $TOPICS_API_KEY"{
"data": [
{
"id": "cmb2ne1kp0009s601p8r4t6vu",
"name": "ChatPEP top of funnel",
"mode": "autopilot",
"active": true,
"theme": null,
"goal": "Grow the Instagram into a top-of-funnel channel for peptide-curious lifters",
"guidance": "Users love the dose calculator and named blends; lean on those.",
"caption": null,
"format": null,
"formats": [
"post",
"carousel"
],
"aspectRatio": "portrait",
"cadenceDays": null,
"hourUtc": 8,
"maxPerWeek": 5,
"postsPerWeek": null,
"creditBudgetMonthly": 60,
"castMemberId": "cmb2n9f2t0005s601m3hq8v1d",
"inspirationAssetIds": [],
"playbook": {
"cadencePerWeek": 4,
"formatWeights": [
{
"format": "post",
"weight": 0.5
},
{
"format": "carousel",
"weight": 0.35
},
{
"format": "story",
"weight": 0.15
}
],
"postHoursUtc": [
8,
17
],
"directions": [
{
"direction": "Named blends beside the dose calculator",
"action": "push",
"rationale": "Both calculator posts beat the account median on saves."
}
],
"overlayGuidance": "Short imperative lines; keep the logo on carousels.",
"experiments": [
"One story per week polling for the next blend"
],
"summary": "Doubling down on calculator content; keeping stories as a probe."
},
"overlay": {
"template": "captionBar",
"text": "Three openings that land",
"showLogo": true
},
"socialAccountId": "cmb2nfhze000bs601a2c4e6gh",
"createdAt": "2026-08-02T10:20:00.000Z"
}
]
}Create a series
/api/v1/seriesA `manual` series runs a fixed recipe and requires `theme`, `format`, and `cadenceDays`. An `autopilot` series requires a `goal` (with optional `guidance`, `formats`, `maxPerWeek`, `postsPerWeek`, and `creditBudgetMonthly`) and plans its own content from measured results. `socialAccountId` names the connected Instagram account to publish to; an empty string stops items at ready.
Requires the createWrite scope.
curl "https://topics.so/api/v1/series" \
-X POST \
-H "Authorization: Bearer $TOPICS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"ChatPEP top of funnel","mode":"autopilot","caption":"","goal":"Grow the Instagram into a top-of-funnel channel for peptide-curious lifters","guidance":"Users love the dose calculator and named blends; lean on those.","maxPerWeek":5,"creditBudgetMonthly":60,"aspectRatio":"portrait","hourUtc":8,"socialAccountId":"cmb2nfhze000bs601a2c4e6gh","castMemberId":"cmb2n9f2t0005s601m3hq8v1d"}'{
"name": "ChatPEP top of funnel",
"mode": "autopilot",
"caption": "",
"goal": "Grow the Instagram into a top-of-funnel channel for peptide-curious lifters",
"guidance": "Users love the dose calculator and named blends; lean on those.",
"maxPerWeek": 5,
"creditBudgetMonthly": 60,
"aspectRatio": "portrait",
"hourUtc": 8,
"socialAccountId": "cmb2nfhze000bs601a2c4e6gh",
"castMemberId": "cmb2n9f2t0005s601m3hq8v1d"
}{
"id": "cmb2ne1kp0009s601p8r4t6vu",
"name": "ChatPEP top of funnel",
"mode": "autopilot",
"active": true,
"theme": null,
"goal": "Grow the Instagram into a top-of-funnel channel for peptide-curious lifters",
"guidance": "Users love the dose calculator and named blends; lean on those.",
"caption": null,
"format": null,
"formats": [
"post",
"carousel"
],
"aspectRatio": "portrait",
"cadenceDays": null,
"hourUtc": 8,
"maxPerWeek": 5,
"postsPerWeek": null,
"creditBudgetMonthly": 60,
"castMemberId": "cmb2n9f2t0005s601m3hq8v1d",
"inspirationAssetIds": [],
"playbook": null,
"overlay": {
"template": "captionBar",
"text": "Three openings that land",
"showLogo": true
},
"socialAccountId": "cmb2nfhze000bs601a2c4e6gh",
"createdAt": "2026-08-02T10:20:00.000Z"
}Retrieve a series
/api/v1/series/{id}One series with its recent slots (each carrying its images) and, on autopilot, its strategy history.
Requires the createRead scope.
Path parameters
idstringrequired- The series id.
curl "https://topics.so/api/v1/series/cmb2ne1kp0009s601p8r4t6vu" \
-H "Authorization: Bearer $TOPICS_API_KEY"{
"id": "cmb2ne1kp0009s601p8r4t6vu",
"name": "ChatPEP top of funnel",
"mode": "autopilot",
"active": true,
"theme": null,
"goal": "Grow the Instagram into a top-of-funnel channel for peptide-curious lifters",
"guidance": "Users love the dose calculator and named blends; lean on those.",
"caption": null,
"format": null,
"formats": [
"post",
"carousel"
],
"aspectRatio": "portrait",
"cadenceDays": null,
"hourUtc": 8,
"maxPerWeek": 5,
"postsPerWeek": null,
"creditBudgetMonthly": 60,
"castMemberId": "cmb2n9f2t0005s601m3hq8v1d",
"inspirationAssetIds": [],
"playbook": {
"cadencePerWeek": 4,
"formatWeights": [
{
"format": "post",
"weight": 0.5
},
{
"format": "carousel",
"weight": 0.35
},
{
"format": "story",
"weight": 0.15
}
],
"postHoursUtc": [
8,
17
],
"directions": [
{
"direction": "Named blends beside the dose calculator",
"action": "push",
"rationale": "Both calculator posts beat the account median on saves."
}
],
"overlayGuidance": "Short imperative lines; keep the logo on carousels.",
"experiments": [
"One story per week polling for the next blend"
],
"summary": "Doubling down on calculator content; keeping stories as a probe."
},
"overlay": {
"template": "captionBar",
"text": "Three openings that land",
"showLogo": true
},
"socialAccountId": "cmb2nfhze000bs601a2c4e6gh",
"createdAt": "2026-08-02T10:20:00.000Z",
"items": [
{
"id": "cmb2nhtvc000fs601r5t7v9xy",
"status": "ready",
"scheduledFor": "2026-08-26T08:00:00.000Z",
"format": "post",
"caption": "The 90-second mobility opener that sticks.",
"hashtags": [
"mobility",
"morningroutine"
],
"decision": {
"direction": "Named blends beside the dose calculator",
"overlayTemplate": "captionBar",
"rationale": "Push direction from the current playbook."
},
"error": null,
"publishedAt": null,
"externalPostId": null,
"creations": [
{
"id": "cmb2nbqoe0007s601k2f9d3xz",
"status": "complete",
"aspectRatio": "landscape",
"idea": "Morning mobility routine on a sunlit balcony",
"prompt": "Editorial lifestyle photo of Maya (24, lean build, dark curly shoulder-length hair) mid-stretch on a sunlit Lisbon balcony…",
"overlay": {
"template": "captionBar",
"text": "Three openings that land",
"showLogo": true
},
"url": "https://media.topics.so/media/creations/org1/cmb2nbqoe0007-final-a1b2c3.jpg",
"rawUrl": "https://media.topics.so/media/creations/org1/cmb2nbqoe0007-raw-a1b2c3.jpg",
"error": null,
"creditCost": 1,
"castMemberId": "cmb2n9f2t0005s601m3hq8v1d",
"createdAt": "2026-08-14T08:00:02.000Z",
"completedAt": "2026-08-14T08:00:41.000Z"
}
]
}
],
"reviews": [
{
"id": "cmb2ng8a1000ds601j9l1n3pq",
"rationale": "Carousels beat singles on saves twice running; cadence holds at four while the story probe reads.",
"scoredItems": 9,
"playbook": {
"cadencePerWeek": 4,
"formatWeights": [
{
"format": "post",
"weight": 0.5
},
{
"format": "carousel",
"weight": 0.35
},
{
"format": "story",
"weight": 0.15
}
],
"postHoursUtc": [
8,
17
],
"directions": [
{
"direction": "Named blends beside the dose calculator",
"action": "push",
"rationale": "Both calculator posts beat the account median on saves."
}
],
"overlayGuidance": "Short imperative lines; keep the logo on carousels.",
"experiments": [
"One story per week polling for the next blend"
],
"summary": "Doubling down on calculator content; keeping stories as a probe."
},
"periodStart": "2026-08-17T00:00:00.000Z",
"periodEnd": "2026-08-24T00:00:00.000Z",
"createdAt": "2026-08-24T08:00:00.000Z"
}
]
}Update a series
/api/v1/series/{id}Partial update: fields you omit keep their stored value, and the merged result is validated by the same mode-dependent rules as create. Include `active` to pause or resume. Send `"castMemberId": null` to clear the cast, or `"postsPerWeek": null` to hand cadence back to the strategist.
Requires the createWrite scope.
Path parameters
idstringrequired- The series id.
curl "https://topics.so/api/v1/series/cmb2ne1kp0009s601p8r4t6vu" \
-X PATCH \
-H "Authorization: Bearer $TOPICS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"guidance":"The dose calculator carousel outperformed; lean harder on it.","maxPerWeek":4}'{
"guidance": "The dose calculator carousel outperformed; lean harder on it.",
"maxPerWeek": 4
}{
"id": "cmb2ne1kp0009s601p8r4t6vu",
"name": "ChatPEP top of funnel",
"mode": "autopilot",
"active": true,
"theme": null,
"goal": "Grow the Instagram into a top-of-funnel channel for peptide-curious lifters",
"guidance": "Users love the dose calculator and named blends; lean on those.",
"caption": null,
"format": null,
"formats": [
"post",
"carousel"
],
"aspectRatio": "portrait",
"cadenceDays": null,
"hourUtc": 8,
"maxPerWeek": 5,
"postsPerWeek": null,
"creditBudgetMonthly": 60,
"castMemberId": "cmb2n9f2t0005s601m3hq8v1d",
"inspirationAssetIds": [],
"playbook": {
"cadencePerWeek": 4,
"formatWeights": [
{
"format": "post",
"weight": 0.5
},
{
"format": "carousel",
"weight": 0.35
},
{
"format": "story",
"weight": 0.15
}
],
"postHoursUtc": [
8,
17
],
"directions": [
{
"direction": "Named blends beside the dose calculator",
"action": "push",
"rationale": "Both calculator posts beat the account median on saves."
}
],
"overlayGuidance": "Short imperative lines; keep the logo on carousels.",
"experiments": [
"One story per week polling for the next blend"
],
"summary": "Doubling down on calculator content; keeping stories as a probe."
},
"overlay": {
"template": "captionBar",
"text": "Three openings that land",
"showLogo": true
},
"socialAccountId": "cmb2nfhze000bs601a2c4e6gh",
"createdAt": "2026-08-02T10:20:00.000Z"
}Delete a series
/api/v1/series/{id}Soft-deletes a series and stops its planning. Published history and images remain.
Requires the createWrite scope.
Path parameters
idstringrequired- The series id.
curl "https://topics.so/api/v1/series/cmb2ne1kp0009s601p8r4t6vu" \
-X DELETE \
-H "Authorization: Bearer $TOPICS_API_KEY"204 No ContentRun the strategist now
/api/v1/series/{id}/reviewsRuns the autopilot strategist immediately instead of waiting for its weekly turn, and answers with the review it produced. Not-yet-generated slots are dropped so the new playbook takes effect from the next cron pass. Autopilot series only.
Requires the createWrite scope.
Path parameters
idstringrequired- The series id.
curl "https://topics.so/api/v1/series/cmb2ne1kp0009s601p8r4t6vu/reviews" \
-X POST \
-H "Authorization: Bearer $TOPICS_API_KEY"{
"id": "cmb2ng8a1000ds601j9l1n3pq",
"rationale": "Carousels beat singles on saves twice running; cadence holds at four while the story probe reads.",
"scoredItems": 9,
"playbook": {
"cadencePerWeek": 4,
"formatWeights": [
{
"format": "post",
"weight": 0.5
},
{
"format": "carousel",
"weight": 0.35
},
{
"format": "story",
"weight": 0.15
}
],
"postHoursUtc": [
8,
17
],
"directions": [
{
"direction": "Named blends beside the dose calculator",
"action": "push",
"rationale": "Both calculator posts beat the account median on saves."
}
],
"overlayGuidance": "Short imperative lines; keep the logo on carousels.",
"experiments": [
"One story per week polling for the next blend"
],
"summary": "Doubling down on calculator content; keeping stories as a probe."
},
"periodStart": "2026-08-17T00:00:00.000Z",
"periodEnd": "2026-08-24T00:00:00.000Z",
"createdAt": "2026-08-24T08:00:00.000Z"
}Skip a slot
/api/v1/series/{id}/items/{itemId}/skipOpts one scheduled slot out permanently. Planning never resurrects a skipped slot.
Requires the createWrite scope.
Path parameters
idstringrequired- The series id.
itemIdstringrequired- The slot id.
curl "https://topics.so/api/v1/series/cmb2ne1kp0009s601p8r4t6vu/items/cmb2nhtvc000fs601r5t7v9xy/skip" \
-X POST \
-H "Authorization: Bearer $TOPICS_API_KEY"204 No ContentRegenerate a slot
/api/v1/series/{id}/items/{itemId}/regenerateSends a slot back to planned so the next cron pass generates a fresh image. The old images stay in the brand's history.
Requires the createWrite scope.
Path parameters
idstringrequired- The series id.
itemIdstringrequired- The slot id.
curl "https://topics.so/api/v1/series/cmb2ne1kp0009s601p8r4t6vu/items/cmb2nhtvc000fs601r5t7v9xy/regenerate" \
-X POST \
-H "Authorization: Bearer $TOPICS_API_KEY"204 No ContentRewrite a slot's caption
/api/v1/series/{id}/items/{itemId}/captionWrites a generated slot's caption and hashtags again from the series' current goal and guidance, keeping its images. For ready or held slots. Costs no image credit.
Requires the createWrite scope.
Path parameters
idstringrequired- The series id.
itemIdstringrequired- The slot id.
curl "https://topics.so/api/v1/series/cmb2ne1kp0009s601p8r4t6vu/items/cmb2nhtvc000fs601r5t7v9xy/caption" \
-X POST \
-H "Authorization: Bearer $TOPICS_API_KEY"{
"id": "cmb2nhtvc000fs601r5t7v9xy",
"caption": "The 90-second mobility opener that sticks.",
"hashtags": [
"mobility",
"morningroutine"
]
}Publish a slot now
/api/v1/series/{id}/items/{itemId}/publishPublishes a ready or held slot to the series' Instagram account immediately instead of waiting for its scheduled time. The one endpoint that posts publicly, behind its own scope.
Requires the publishWrite scope.
Path parameters
idstringrequired- The series id.
itemIdstringrequired- The slot id.
curl "https://topics.so/api/v1/series/cmb2ne1kp0009s601p8r4t6vu/items/cmb2nhtvc000fs601r5t7v9xy/publish" \
-X POST \
-H "Authorization: Bearer $TOPICS_API_KEY"204 No ContentOpenAPI spec
The full specification is served as OpenAPI 3.1, generated from the same definitions that produced this page. Point your client generator, editor, or API console at it.