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.
| Scope | Covers |
|---|---|
postsRead | Posts and their metrics, topics, followers and comments. |
studioRead | Creations, assets, cast, sounds, the brand kit and the credit balance. |
studioWrite | Generate, animate, upload and edit any of those. Spends credits. |
seriesRead | Series, their queues and autopilot reviews. |
seriesWrite | Create and edit series, generate slots and run autopilot reviews. Spends credits. |
publishWrite | Post a slot, mark a hand-off posted, and pause or resume a series. |
brandWrite | The brand's name, slug, logo, studio defaults and notifications. |
membersRead | See members and pending invitations. |
membersWrite | Invite people, change roles, remove members and cancel invitations. |
apiKeysWrite | Create, edit and revoke API keys. |
A key is never more than the person who made it. Its scopes are capped by its creator's role in the brand, checked on every request, so a role change applies at once. membersWrite and apiKeysWrite are off by default.
| Permission | Owner | Admin | Editor | Viewer |
|---|---|---|---|---|
| Posts: ReadPosts and their metrics, topics, followers and comments. | Yes | Yes | Yes | Yes |
| Studio: ReadCreations, assets, cast, sounds, the brand kit and the credit balance. | Yes | Yes | Yes | Yes |
| Studio: WriteGenerate, animate, upload and edit any of those. Spends credits. | Yes | Yes | Yes | No |
| Series: ReadSeries, their queues and autopilot reviews. | Yes | Yes | Yes | Yes |
| Series: WriteCreate and edit series, generate slots and run autopilot reviews. Spends credits. | Yes | Yes | Yes | No |
| Publishing: WritePost a slot, mark a hand-off posted, and pause or resume a series. | Yes | Yes | No | No |
| Brand: WriteThe brand's name, slug, logo, studio defaults and notifications. | Yes | Yes | No | No |
| Accounts: WriteConnect and disconnect social accounts. | Yes | Yes | No | No |
| Members: ReadSee members and pending invitations. | Yes | Yes | Yes | Yes |
| Members: WriteInvite people, change roles, remove members and cancel invitations. | Yes | Yes | No | No |
| API keys: WriteCreate, edit and revoke API keys. | Yes | Yes | No | No |
| OwnershipDelete the brand, make someone an owner, and hand ownership over. | Yes | No | No | No |
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 credits: the month's allowance is spent and no purchased credits remain. |
planRequired | 403 | The request uses something the brand's plan does not include, such as a video model reserved for a higher plan. |
conflict | 409 | The resource isn't in a state that allows this (already publishing, not failed, and so on), or an `Idempotency-Key` was reused with a different body or while its first request is still running. |
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."
}
}Members
A brand can have several people on it, each with one role: owner, admin, editor, or viewer. In the API an editor's role is member. The roles table under Authentication shows what each can do.
The member endpoints act as the person who made the key, so the dashboard's rules apply. Only an owner makes an owner or changes one. The last owner stays an owner. While the plan holder's plan is paid, they stay an owner too, and either change answers 409 conflict.
An invitation link lasts 7 days and works for whoever signs in with it. Members and pending invitations count against the plan's seats, viewers included. Past the cap, inviting answers 403 planRequired. Members cannot be removed over the API.
| Plan | Seats |
|---|---|
| Free | 1 |
| Creator | 3 |
| Studio | Unlimited |
MCP server
Claude and any other MCP client 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 |
|---|---|
| list_posts, get_post | postsRead |
| get_brand, get_creation, list_assets, list_sounds, search_instagram_audio, list_cast_members | studioRead |
| compose_prompt, generate_image, animate_creation, rerender_overlay, set_brand_kit, add_asset, create_asset_upload, confirm_asset_upload, set_primary_logo, save_sound, delete_sound, save_cast_member, delete_cast_member | studioWrite |
| list_series, get_series_queue | seriesRead |
| save_series, update_series, run_autopilot_review, add_series_item, update_item, skip_item, delete_item, regenerate_item, generate_item, rewrite_caption | seriesWrite |
| publish_item, mark_item_posted, set_series_active | publishWrite |
| list_organizations, create_organization, update_organization | brandWrite |
| list_members | membersRead |
| invite_member, update_member_role, cancel_invitation | membersWrite |
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 the official client for Node.js, Bun, Deno, and edge functions. It is built from the same spec as this page, so every endpoint below is a typed method on it.
generateImage and generateVideo turn an idea into a finished file in one call. Give them an idempotencyKey built from your own record, and running the call twice never pays twice.
Failed requests retry on their own, with backoff, under one idempotency key. Each error has its own class, and each carries the requestId to quote when you write to us. The key rules from Authentication apply: server-side only, never in browser code.
pnpm add @topics-so/sdkimport { Topics } from "@topics-so/sdk";
const topics = new Topics({ apiKey: process.env.TOPICS_API_KEY });
// Composes the prompt, spends the credits, and waits for the image.
const image = await topics.creations.generateImage({
idea: "Morning espresso ritual, warm window light",
aspectRatio: "landscape",
idempotencyKey: `post-${post.id}-hero`
});
console.log(image.url);// Every endpoint is a typed method.
const series = await topics.series.get("ser_…");
await topics.series.items.skip(series.id, "itm_…");
// Lists page themselves.
for await (const post of topics.posts.list({ platform: "instagram" })) {
console.log(post.metrics.engagementRate);
}import { InsufficientCreditsError, TopicsError } from "@topics-so/sdk";
try {
await topics.creations.generateImage({ idea, aspectRatio: "square" });
} catch (error) {
if (error instanceof InsufficientCreditsError) {
// Out of credits. Top up or wait for the monthly reset.
} else if (error instanceof TopicsError) {
console.error(error.message, error.requestId);
}
}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, on-screen text, and transcript.
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, tiktok, youtube. formatstringoptional- Restrict to one content format.One of
carousel, image, reel, story, text, video. hashtagstringoptional- Restrict to posts carrying this hashtag, without the leading #.
sortstringoptional- Field to sort on.One of
published, views, impressions, reach, likes, comments, shares, saves, clicks, 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 and its media. Snapshots are cumulative lifetime readings, oldest first, so a daily series is the difference between consecutive entries. Each image or video carries its hook, description, transcript, and on-screen text.
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"
}
],
"media": [
{
"mediaType": "image",
"position": 0,
"url": "https://media.topics.so/posts/cm8xk21p40001/1.jpg",
"thumbnailUrl": "https://media.topics.so/posts/cm8xk21p40001/1.jpg",
"hook": "Three openings that land",
"description": "A speaker on a small stage mid-sentence, one hand raised, with the title set large across the top.",
"transcript": null,
"onScreenText": "Three openings that land\n@topics.so"
}
]
}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 studioRead scope.
curl "https://topics.so/api/v1/brand-kit" \
-H "Authorization: Bearer $TOPICS_API_KEY"{
"imageModel": null,
"videoModel": null,
"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 studioWrite 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": ""
}{
"imageModel": null,
"videoModel": null,
"baseStylePrompt": "Warm editorial lifestyle photography, natural light, shallow depth of field.",
"discordWebhookUrl": null
}Get credits
/api/v1/creditsThe brand's credit position. One pool pays for analysis and generation: the monthly `allowance` is spent first and resets on the first of the UTC month, then `purchased` credits, which never expire. `byKind` is this month's net spend. Credits are separate from the request quota.
Requires the studioRead scope.
curl "https://topics.so/api/v1/credits" \
-H "Authorization: Bearer $TOPICS_API_KEY"{
"allowance": 3000,
"used": 460,
"remaining": 2540,
"purchased": 1000,
"available": 3540,
"byKind": [
{
"kind": "generateImage",
"amount": 240
},
{
"kind": "generateVideo",
"amount": 110
},
{
"kind": "analyzeVideo",
"amount": 78
},
{
"kind": "analyzeImage",
"amount": 32
}
]
}List assets
/api/v1/assetsThe brand's Create asset library: logos, inspiration images, cast reference photos, photos, videos, and audio files.
Requires the studioRead scope.
Query parameters
kindstringoptional- Restrict to one kind.One of
logo, inspiration, castReference, photo, video, audio.
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,
"durationSeconds": null,
"isPrimary": false,
"createdAt": "2026-08-02T10:12:00.000Z"
}
]
}Add an asset
/api/v1/assetsAdds a file to the library. Send `sourceUrl` and it is fetched server-side from that public URL: an image (JPEG, PNG, or WebP, up to 10 MB), or with kind `video` an MP4 or MOV (up to 200 MB, 3 seconds to 15 minutes; a story takes up to 60 seconds) to post as a reel or a story, or with kind `audio` an MP3, M4A, or WAV (up to 20 MB) for the sound library. Alternatively, finish a presigned upload by sending the `key` and `url` returned by the upload endpoint; a video or audio file is probed for its length on confirm.
Requires the studioWrite 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,
"durationSeconds": null,
"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`. Images up to 10 MB; kind `video` takes MP4 or MOV up to 200 MB; kind `audio` takes MP3, M4A, or WAV up to 20 MB.
Requires the studioWrite 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 studioWrite 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 sounds
/api/v1/soundsThe brand's sound library: Instagram catalogue tracks pinned by id and uploaded audio, each with the description the autopilot picks a sound by. Series pin these by id; the animate and add-item endpoints take a `soundId`.
Requires the studioRead scope.
curl "https://topics.so/api/v1/sounds" \
-H "Authorization: Bearer $TOPICS_API_KEY"{
"data": [
{
"id": "cmb2nsnd10001s601q4w8e2rt",
"source": "instagram",
"title": "Golden Hour",
"artist": "JVKE",
"description": "Warm, mid-tempo; the slow-morning reels",
"durationSeconds": 209,
"audioId": "1234567890123456",
"audioType": "music",
"coverArtworkUrl": "https://scontent.cdninstagram.com/cover.jpg",
"url": null,
"active": true,
"createdAt": "2026-08-02T10:18:00.000Z"
}
]
}Search Instagram's sounds
/api/v1/sounds/searchInstagram's audio catalogue for the brand's account; an empty `q` lists what is trending. Only tracks licensed for API publishing appear, so a sound saved in the app can be missing. Needs the Instagram account connected through Facebook Login; the other variant has no Audio API. Pin a result with `POST /v1/sounds` and its `audioId`.
Requires the studioRead scope.
Query parameters
qstringoptional- Title or artist. Empty lists trending audio.
typestringoptional- Music (default) or original sounds.One of
music, originalSound.
curl "https://topics.so/api/v1/sounds/search?q=golden%20hour" \
-H "Authorization: Bearer $TOPICS_API_KEY"{
"data": [
{
"audioId": "1234567890123456",
"title": "Golden Hour",
"artist": "JVKE",
"durationSeconds": 209,
"audioType": "music",
"coverArtworkUrl": "https://scontent.cdninstagram.com/cover.jpg",
"previewUrl": "https://scontent.cdninstagram.com/preview.m4a"
}
]
}Add a sound
/api/v1/soundsAdds a sound to the library: an Instagram track by `audioId` (from the search endpoint), or an uploaded audio asset by `assetId` (kind `audio`). Give it a `description` of mood, tempo, and where it fits; that line is what the autopilot picks by. Upload only music the brand has the rights to: Instagram mutes reels carrying tracks it can't clear.
Requires the studioWrite scope.
curl "https://topics.so/api/v1/sounds" \
-X POST \
-H "Authorization: Bearer $TOPICS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"audioId":"1234567890123456","description":"Warm, mid-tempo; the slow-morning reels"}'{
"audioId": "1234567890123456",
"description": "Warm, mid-tempo; the slow-morning reels"
}{
"id": "cmb2nsnd10001s601q4w8e2rt",
"source": "instagram",
"title": "Golden Hour",
"artist": "JVKE",
"description": "Warm, mid-tempo; the slow-morning reels",
"durationSeconds": 209,
"audioId": "1234567890123456",
"audioType": "music",
"coverArtworkUrl": "https://scontent.cdninstagram.com/cover.jpg",
"url": null,
"active": true,
"createdAt": "2026-08-02T10:18:00.000Z"
}Edit a sound
/api/v1/sounds/{id}Changes a sound's title, description, or whether it is active. Omitted fields keep their stored value. The source is fixed at creation.
Requires the studioWrite scope.
Path parameters
idstringrequired- The sound id.
curl "https://topics.so/api/v1/sounds/cmb2nsnd10001s601q4w8e2rt" \
-X PATCH \
-H "Authorization: Bearer $TOPICS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"active":false}'{
"active": false
}{
"id": "cmb2nsnd10001s601q4w8e2rt",
"source": "instagram",
"title": "Golden Hour",
"artist": "JVKE",
"description": "Warm, mid-tempo; the slow-morning reels",
"durationSeconds": 209,
"audioId": "1234567890123456",
"audioType": "music",
"coverArtworkUrl": "https://scontent.cdninstagram.com/cover.jpg",
"url": null,
"active": false,
"createdAt": "2026-08-02T10:18:00.000Z"
}Remove a sound
/api/v1/sounds/{id}Removes a sound from the library. Series that pinned it stop offering it; reels already made with it keep it.
Requires the studioWrite scope.
Path parameters
idstringrequired- The sound id.
curl "https://topics.so/api/v1/sounds/cmb2nsnd10001s601q4w8e2rt" \
-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 studioRead 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,
"durationSeconds": null,
"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 studioWrite 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,
"durationSeconds": null,
"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 studioRead 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,
"durationSeconds": null,
"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 studioWrite 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,
"durationSeconds": null,
"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 studioWrite 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 creations, newest first: generated and uploaded images, and reel clips (`mediaType` video, whose `url` is the cover and `videoUrl` the file).
Requires the studioRead 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",
"mediaType": "image",
"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",
"videoUrl": null,
"durationSeconds": null,
"motionPrompt": null,
"soundId": null,
"sourceCreationId": null,
"error": null,
"creditCost": 10,
"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 studioWrite 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 or a video
/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. An image debits its model's price in credits. With `mediaType` set to `video` the image is generated and then animated into a clip of the same `aspectRatio` with the brand's video model, both paid up front, and the creation returned is the clip. Prices are in the brand's `models` list. Poll the single-creation endpoint (ideally with `wait`) until `status` is `complete`. Send an `Idempotency-Key` header to make retries safe: a replay returns the original creation instead of debiting again.
Requires the studioWrite 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",
"mediaType": "image",
"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,
"videoUrl": null,
"durationSeconds": null,
"motionPrompt": null,
"soundId": null,
"sourceCreationId": null,
"error": null,
"creditCost": 10,
"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 studioRead 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",
"mediaType": "image",
"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",
"videoUrl": null,
"durationSeconds": null,
"motionPrompt": null,
"soundId": null,
"sourceCreationId": null,
"error": null,
"creditCost": 10,
"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 studioWrite 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",
"mediaType": "image",
"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,
"videoUrl": null,
"durationSeconds": null,
"motionPrompt": null,
"soundId": null,
"sourceCreationId": null,
"error": null,
"creditCost": 10,
"castMemberId": "cmb2n9f2t0005s601m3hq8v1d",
"createdAt": "2026-08-14T08:00:02.000Z",
"completedAt": null
}Animate into a reel
/api/v1/creations/{id}/animateAnimates a finished image into a 9:16 reel clip with the brand's video model (image-to-video, so the cast member and style carry over) and answers 202 with the pending clip. The clip takes the still's overlay and, with `soundId`, a library sound: an Instagram track is attached when the reel posts, an upload is mixed in. Debits credits by model and length; prices are in the brand's `models` list. Generation takes minutes: poll `GET /v1/creations/{id}` with `wait`, then add the clip to a reel slot with `creationId`.
Requires the studioWrite scope.
Path parameters
idstringrequired- The still's creation id.
curl "https://topics.so/api/v1/creations/cmb2nbqoe0007s601k2f9d3xz/animate" \
-X POST \
-H "Authorization: Bearer $TOPICS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"motionPrompt":"A slow push in; her hair moves in a light breeze.","durationSeconds":5,"soundId":"cmb2nsnd10001s601q4w8e2rt"}'{
"motionPrompt": "A slow push in; her hair moves in a light breeze.",
"durationSeconds": 5,
"soundId": "cmb2nsnd10001s601q4w8e2rt"
}{
"id": "cmb2ncl1p0008s601v7b2n9ks",
"mediaType": "video",
"status": "pending",
"aspectRatio": "story",
"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": "https://media.topics.so/media/creations/org1/cmb2nbqoe0007-raw-a1b2c3.jpg",
"videoUrl": null,
"durationSeconds": 5,
"motionPrompt": "A slow push in; her hair moves in a light breeze.",
"soundId": "cmb2nsnd10001s601q4w8e2rt",
"sourceCreationId": "cmb2nbqoe0007s601k2f9d3xz",
"error": null,
"creditCost": 10,
"castMemberId": "cmb2n9f2t0005s601m3hq8v1d",
"createdAt": "2026-08-14T08:05:00.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 studioWrite 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",
"mediaType": "image",
"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",
"videoUrl": null,
"durationSeconds": null,
"motionPrompt": null,
"soundId": null,
"sourceCreationId": null,
"error": null,
"creditCost": 10,
"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 studioWrite 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 seriesRead 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"
],
"generateHashtags": true,
"hashtags": [
"chatpep"
],
"aspectRatio": "portrait",
"cadenceDays": null,
"hourUtc": 8,
"hourPinned": false,
"maxPerWeek": 5,
"postsPerWeek": null,
"weekdaysUtc": [
1,
3,
5
],
"creditBudgetMonthly": 600,
"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",
"delivery": "auto",
"crosspostAccountIds": [],
"tiktok": null,
"audioMode": "trending",
"soundIds": [
"cmb2nsnd10001s601q4w8e2rt"
],
"videoModel": null,
"createdAt": "2026-08-02T10:20:00.000Z"
}
]
}Create a series
/api/v1/seriesA `manual` series runs a fixed recipe and requires `theme`, `format` (post, carousel, story, or reel), and `cadenceDays`. An `autopilot` series requires a `goal` (with optional `guidance`, `formats`, `maxPerWeek`, `postsPerWeek`, `weekdaysUtc`, `hourPinned`, and `creditBudgetMonthly`) and plans its own content from measured results. A reel is a short 9:16 video animated from a generated still; `audioMode` and `soundIds` decide its sound and `videoModel` overrides the brand's default. `socialAccountId` names the connected Instagram or TikTok account to publish to; an empty string stops items at ready. `delivery` decides who makes the post: `auto` posts it, `handoff` gives it to the operator (a draft in TikTok's inbox, or a Discord message with the file, caption, and sound for Instagram) and the slot waits as `handedOff` until the post appears on the account. TikTok's API attaches no sound and has no stories, so a TikTok series defaults to `handoff`, mixes in uploaded sounds only, and on autopilot plans reels only; `auto` on TikTok needs the `tiktok` settings and TikTok's approval of the app. `crosspostAccountIds` also sends every slot to other connected accounts. Without `publishWrite` the new series starts paused, since an active series posts on its own.
Requires the seriesWrite 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":600,"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": 600,
"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"
],
"generateHashtags": true,
"hashtags": [
"chatpep"
],
"aspectRatio": "portrait",
"cadenceDays": null,
"hourUtc": 8,
"hourPinned": false,
"maxPerWeek": 5,
"postsPerWeek": null,
"weekdaysUtc": [
1,
3,
5
],
"creditBudgetMonthly": 600,
"castMemberId": "cmb2n9f2t0005s601m3hq8v1d",
"inspirationAssetIds": [],
"playbook": null,
"overlay": {
"template": "captionBar",
"text": "Three openings that land",
"showLogo": true
},
"socialAccountId": "cmb2nfhze000bs601a2c4e6gh",
"delivery": "auto",
"crosspostAccountIds": [],
"tiktok": null,
"audioMode": "trending",
"soundIds": [
"cmb2nsnd10001s601q4w8e2rt"
],
"videoModel": null,
"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 seriesRead 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"
],
"generateHashtags": true,
"hashtags": [
"chatpep"
],
"aspectRatio": "portrait",
"cadenceDays": null,
"hourUtc": 8,
"hourPinned": false,
"maxPerWeek": 5,
"postsPerWeek": null,
"weekdaysUtc": [
1,
3,
5
],
"creditBudgetMonthly": 600,
"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",
"delivery": "auto",
"crosspostAccountIds": [],
"tiktok": null,
"audioMode": "trending",
"soundIds": [
"cmb2nsnd10001s601q4w8e2rt"
],
"videoModel": null,
"createdAt": "2026-08-02T10:20:00.000Z",
"items": [
{
"id": "cmb2nhtvc000fs601r5t7v9xy",
"status": "ready",
"scheduledFor": "2026-08-26T08:00:00.000Z",
"format": "post",
"origin": "scheduler",
"idea": null,
"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,
"handedOffAt": null,
"externalPostId": null,
"creations": [
{
"id": "cmb2nbqoe0007s601k2f9d3xz",
"mediaType": "image",
"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",
"videoUrl": null,
"durationSeconds": null,
"motionPrompt": null,
"soundId": null,
"sourceCreationId": null,
"error": null,
"creditCost": 10,
"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; that also needs `publishWrite`. Send `"castMemberId": null` to clear the cast, or `"postsPerWeek": null` to hand cadence back to the strategist.
Requires the seriesWrite 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"
],
"generateHashtags": true,
"hashtags": [
"chatpep"
],
"aspectRatio": "portrait",
"cadenceDays": null,
"hourUtc": 8,
"hourPinned": false,
"maxPerWeek": 5,
"postsPerWeek": null,
"weekdaysUtc": [
1,
3,
5
],
"creditBudgetMonthly": 600,
"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",
"delivery": "auto",
"crosspostAccountIds": [],
"tiktok": null,
"audioMode": "trending",
"soundIds": [
"cmb2nsnd10001s601q4w8e2rt"
],
"videoModel": null,
"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 seriesWrite 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 seriesWrite 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"
}Add a post to a series
/api/v1/series/{id}/itemsAdds one slot on top of what the series plans, on autopilot as much as on a manual series; the scheduler never drops it. Give it at most one media source: `creationId` (a finished image, or a clip from the animate endpoint for a reel), `assetId` (a photo, or a kind `video` asset, which makes the slot a reel, or a story when `format` is `story`), `sourceUrl` (a public image or video URL, imported like `POST /v1/assets`), or `assetIds` (2 to 10 photo assets, in order, posted as a carousel of the operator's own slides). A supplied photo is cropped to the slot's shape, analyzed so the autopilot knows what it shows, captioned on an autopilot series, and the slot answers `ready`; a supplied video is rendered to the 9:16 spec with the series' overlay and a sound (`soundId`, or the series' first allowed sound) after the response, so the slot answers `generating` and turns `ready` on `GET /v1/series/{id}` once rendered (or `failed` with the reason). A story video runs up to 60 seconds and publishes without a caption or an Instagram sound. Without media the slot is planned around `idea` (or invents one); `generateNow` generates its images inline, one credit each; a reel slot generates a still, reviews it, then animates it. Omit `scheduledFor` for the series' next free posting hour. A not-yet-generated planned slot at the same time is taken over.
Requires the seriesWrite scope.
Path parameters
idstringrequired- The series id.
curl "https://topics.so/api/v1/series/cmb2ne1kp0009s601p8r4t6vu/items" \
-X POST \
-H "Authorization: Bearer $TOPICS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"scheduledFor":"2026-08-27T08:00:00Z","format":"post","sourceUrl":"https://example.com/maya-espresso.jpg"}'{
"scheduledFor": "2026-08-27T08:00:00Z",
"format": "post",
"sourceUrl": "https://example.com/maya-espresso.jpg"
}{
"id": "cmb2nk4wd000hs601x1z3b5dn",
"status": "ready",
"scheduledFor": "2026-08-27T08:00:00.000Z",
"format": "post",
"origin": "operator",
"idea": "Maya reading the dose calculator results on her phone at a Lisbon espresso bar",
"caption": "The calculator does the math so the morning doesn't.",
"hashtags": [
"peptides",
"dosecalculator"
],
"decision": null,
"error": null,
"publishedAt": null,
"handedOffAt": null,
"externalPostId": null,
"creations": [
{
"id": "cmb2nk9q2000js601f7h9j1lm",
"mediaType": "image",
"status": "complete",
"aspectRatio": "landscape",
"idea": "Maya reading the dose calculator results on her phone at a Lisbon espresso bar",
"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",
"videoUrl": null,
"durationSeconds": null,
"motionPrompt": null,
"soundId": null,
"sourceCreationId": null,
"error": null,
"creditCost": 0,
"castMemberId": "cmb2n9f2t0005s601m3hq8v1d",
"createdAt": "2026-08-14T08:00:02.000Z",
"completedAt": "2026-08-14T08:00:41.000Z"
}
]
}Skip a slot
/api/v1/series/{id}/items/{itemId}/skipOpts one scheduled slot out permanently. Planning never resurrects a skipped slot.
Requires the seriesWrite 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 seriesWrite 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 ContentEdit a slot
/api/v1/series/{id}/items/{itemId}Changes a slot's caption, hashtags, time, format, or its video's sound; fields you omit keep their stored value. Caption and hashtags take on a ready or held slot, since generation writes them. The format changes only while the slot has no images, which are cropped to it, except between `reel` and `story`, which share the 9:16 frame (a reel needs a clip). A moved slot becomes the operator's, so the scheduler never drops it; a not-yet-generated planned slot at the new time is taken over.
Requires the seriesWrite scope.
Path parameters
idstringrequired- The series id.
itemIdstringrequired- The slot id.
curl "https://topics.so/api/v1/series/cmb2ne1kp0009s601p8r4t6vu/items/cmb2nhtvc000fs601r5t7v9xy" \
-X PATCH \
-H "Authorization: Bearer $TOPICS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"caption":"The 90-second mobility opener that sticks. Link in bio.","hashtags":["mobility","morningroutine"]}'{
"caption": "The 90-second mobility opener that sticks. Link in bio.",
"hashtags": [
"mobility",
"morningroutine"
]
}{
"id": "cmb2nhtvc000fs601r5t7v9xy",
"status": "ready",
"scheduledFor": "2026-08-26T08:00:00.000Z",
"format": "post",
"origin": "scheduler",
"idea": null,
"caption": "The 90-second mobility opener that sticks. Link in bio.",
"hashtags": [
"mobility",
"morningroutine"
],
"decision": {
"direction": "Named blends beside the dose calculator",
"overlayTemplate": "captionBar",
"rationale": "Push direction from the current playbook."
},
"error": null,
"publishedAt": null,
"handedOffAt": null,
"externalPostId": null,
"creations": [
{
"id": "cmb2nbqoe0007s601k2f9d3xz",
"mediaType": "image",
"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",
"videoUrl": null,
"durationSeconds": null,
"motionPrompt": null,
"soundId": null,
"sourceCreationId": null,
"error": null,
"creditCost": 10,
"castMemberId": "cmb2n9f2t0005s601m3hq8v1d",
"createdAt": "2026-08-14T08:00:02.000Z",
"completedAt": "2026-08-14T08:00:41.000Z"
}
]
}Delete a slot
/api/v1/series/{id}/items/{itemId}Takes a slot out of the queue for good. Its images stay in the brand's history. A published slot can't be deleted; it is the record of what went out.
Requires the seriesWrite scope.
Path parameters
idstringrequired- The series id.
itemIdstringrequired- The slot id.
curl "https://topics.so/api/v1/series/cmb2ne1kp0009s601p8r4t6vu/items/cmb2nhtvc000fs601r5t7v9xy" \
-X DELETE \
-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 seriesWrite 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}/publishDelivers a ready or held slot to the series' account and its cross-posts immediately instead of waiting for its scheduled time, as a feed post, carousel, story, or reel per its format. A series on `handoff` hands the slot over instead (a TikTok draft, or the Instagram hand-off message) and the slot turns `handedOff`. A reel's Instagram sound is attached by id when the account is connected through Facebook Login; otherwise it posts with the clip's own audio. One of the endpoints behind `publishWrite`, with marking a hand-off posted and pausing or resuming a series.
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 ContentMark a hand-off as posted
/api/v1/series/{id}/items/{itemId}/postedCloses a `handedOff` slot once the operator has posted it. `postId` (from `GET /v1/posts`) joins the slot to that post, so it is scored like one that published itself; the post's account decides whether it settles the series' own delivery or one of its cross-posts. Without `postId` the slot is only marked done. Ingestion usually makes this join on its own from the caption; this is for the posts it couldn't place.
Requires the publishWrite scope.
Path parameters
idstringrequired- The series id.
itemIdstringrequired- The slot id.
curl "https://topics.so/api/v1/series/cmb2ne1kp0009s601p8r4t6vu/items/cmb2nhtvc000fs601r5t7v9xy/posted" \
-X POST \
-H "Authorization: Bearer $TOPICS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"postId":"cm8xk21p40001s6014r9e2n7q"}'{
"postId": "cm8xk21p40001s6014r9e2n7q"
}{
"id": "cmb2nhtvc000fs601r5t7v9xy",
"status": "published",
"scheduledFor": "2026-08-26T08:00:00.000Z",
"format": "post",
"origin": "scheduler",
"idea": null,
"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": "2026-08-26T17:40:00.000Z",
"handedOffAt": "2026-08-26T08:00:04.000Z",
"externalPostId": null,
"creations": [
{
"id": "cmb2nbqoe0007s601k2f9d3xz",
"mediaType": "image",
"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",
"videoUrl": null,
"durationSeconds": null,
"motionPrompt": null,
"soundId": null,
"sourceCreationId": null,
"error": null,
"creditCost": 10,
"castMemberId": "cmb2n9f2t0005s601m3hq8v1d",
"createdAt": "2026-08-14T08:00:02.000Z",
"completedAt": "2026-08-14T08:00:41.000Z"
}
]
}List members
/api/v1/membersEveryone in the brand, longest-standing first, with their role. A `member` role is the dashboard's Editor. `planHolder` marks the earliest owner, whose plan sets the brand's limits.
Requires the membersRead scope.
curl "https://topics.so/api/v1/members" \
-H "Authorization: Bearer $TOPICS_API_KEY"{
"data": [
{
"id": "pR4tY7uI0oP3aS6dF9gH2",
"name": "Geno Fabelo",
"email": "geno@agency.com",
"role": "owner",
"joinedAt": "2026-07-01T10:00:00.000Z",
"planHolder": true
},
{
"id": "kq2V8xR1mN4pT6wY9zB3c",
"name": "Jane Okafor",
"email": "jane@agency.com",
"role": "member",
"joinedAt": "2026-09-02T14:20:00.000Z",
"planHolder": false
}
]
}Change a member's role
/api/v1/members/{id}Gives a member one new role, acting as the key's creator. Only an owner can make someone an owner or change an owner's role. A change that would leave the brand with no owner, or move a paid plan holder off the owner role, is refused with `409 conflict`. The change applies on the member's next request, their API keys included. Members cannot be removed over the API.
Requires the membersWrite scope.
Path parameters
idstringrequired- The member id, from the members list.
curl "https://topics.so/api/v1/members/kq2V8xR1mN4pT6wY9zB3c" \
-X PATCH \
-H "Authorization: Bearer $TOPICS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"role":"admin"}'{
"role": "admin"
}{
"id": "kq2V8xR1mN4pT6wY9zB3c",
"name": "Jane Okafor",
"email": "jane@agency.com",
"role": "admin",
"joinedAt": "2026-09-02T14:20:00.000Z",
"planHolder": false
}List invitations
/api/v1/invitationsInvitations nobody has accepted yet, newest first. Each holds a seat until it is accepted, cancelled, or expires.
Requires the membersRead scope.
curl "https://topics.so/api/v1/invitations" \
-H "Authorization: Bearer $TOPICS_API_KEY"{
"data": [
{
"id": "h7Lw2QeR5tY8uI1oP4aSd",
"email": "sam@agency.com",
"role": "viewer",
"invitedBy": "Geno Fabelo",
"expiresAt": "2026-10-22T09:00:00.000Z"
}
]
}Invite someone
/api/v1/invitationsEmails a link to join the brand as a role, acting as the key's creator. The link lasts 7 days and works for whoever signs in with it. Inviting an address that already has a pending invitation replaces it with a fresh link. Members and pending invitations together count against the plan's seats (Free 1, Creator 3, Studio unlimited); past that the request answers `403 planRequired`. Only an owner can invite an owner.
Requires the membersWrite scope.
curl "https://topics.so/api/v1/invitations" \
-X POST \
-H "Authorization: Bearer $TOPICS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"sam@agency.com","role":"viewer"}'{
"email": "sam@agency.com",
"role": "viewer"
}{
"id": "h7Lw2QeR5tY8uI1oP4aSd",
"email": "sam@agency.com",
"role": "viewer",
"invitedBy": "Geno Fabelo",
"expiresAt": "2026-10-22T09:00:00.000Z"
}Cancel an invitation
/api/v1/invitations/{id}Cancels a pending invitation. Its link stops working and its seat comes back.
Requires the membersWrite scope.
Path parameters
idstringrequired- The invitation id.
curl "https://topics.so/api/v1/invitations/h7Lw2QeR5tY8uI1oP4aSd" \
-X DELETE \
-H "Authorization: Bearer $TOPICS_API_KEY"204 No ContentList brands
/api/v1/brandsEvery brand the key's creator belongs to, oldest first, with their role in each. `current` marks the key's own brand. A key still acts on one brand only: to work in another, use a key made for it.
Requires the brandWrite scope.
curl "https://topics.so/api/v1/brands" \
-H "Authorization: Bearer $TOPICS_API_KEY"{
"data": [
{
"id": "Zr3nK8vB1xQ6wE9tY2uI5",
"name": "Acme Mobility",
"slug": "acme-mobility",
"logo": null,
"role": "owner",
"createdAt": "2026-07-01T10:00:00.000Z",
"current": true
}
]
}Create a brand
/api/v1/brandsCreates a brand owned by the key's creator. A taken slug gets a numeric suffix. Set `createApiKey` to also mint a key for the new brand with this key's scopes: its `secret` is in this response and never again.
Requires the brandWrite scope.
curl "https://topics.so/api/v1/brands" \
-X POST \
-H "Authorization: Bearer $TOPICS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Acme Recovery","createApiKey":true}'{
"name": "Acme Recovery",
"createApiKey": true
}{
"id": "Bt6yU9iO2pA5sD8fG1hJ4",
"name": "Acme Recovery",
"slug": "acme-recovery",
"logo": null,
"role": "owner",
"createdAt": "2026-07-01T10:00:00.000Z",
"current": false,
"apiKey": {
"secret": "tso_9f2c…",
"prefix": "tso_9f2c",
"scopes": [
"brandWrite",
"studioWrite"
]
}
}Update a brand
/api/v1/brands/{id}Renames a brand or changes its slug or logo, as the key's creator, who must be an owner or admin there. Any of their brands works, not only the key's. Omitted fields keep their stored value.
Requires the brandWrite scope.
Path parameters
idstringrequired- The brand id, from the brands list.
curl "https://topics.so/api/v1/brands/Zr3nK8vB1xQ6wE9tY2uI5" \
-X PATCH \
-H "Authorization: Bearer $TOPICS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Acme Mobility Co."}'{
"name": "Acme Mobility Co."
}{
"id": "Zr3nK8vB1xQ6wE9tY2uI5",
"name": "Acme Mobility Co.",
"slug": "acme-mobility",
"logo": null,
"role": "owner",
"createdAt": "2026-07-01T10:00:00.000Z",
"current": true
}OpenAPI 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.