Skip to content

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.

View as markdown
Quickstart
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.

Base URL
https://topics.so/api
Response envelope
{
  "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.

View as markdown
Paste this into your assistant
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.
Or point it at these
https://topics.so/llms.txt
https://topics.so/docs/api.md
https://topics.so/api/v1/openapi.json

Authentication

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.

ScopeCovers
postsReadPosts and their metrics, topics, followers and comments.
studioReadCreations, assets, cast, sounds, the brand kit and the credit balance.
studioWriteGenerate, animate, upload and edit any of those. Spends credits.
seriesReadSeries, their queues and autopilot reviews.
seriesWriteCreate and edit series, generate slots and run autopilot reviews. Spends credits.
publishWritePost a slot, mark a hand-off posted, and pause or resume a series.
brandWriteThe brand's name, slug, logo, studio defaults and notifications.
membersReadSee members and pending invitations.
membersWriteInvite people, change roles, remove members and cancel invitations.
apiKeysWriteCreate, 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.

What each role can do
PermissionOwnerAdminEditorViewer
Posts: ReadPosts and their metrics, topics, followers and comments.YesYesYesYes
Studio: ReadCreations, assets, cast, sounds, the brand kit and the credit balance.YesYesYesYes
Studio: WriteGenerate, animate, upload and edit any of those. Spends credits.YesYesYesNo
Series: ReadSeries, their queues and autopilot reviews.YesYesYesYes
Series: WriteCreate and edit series, generate slots and run autopilot reviews. Spends credits.YesYesYesNo
Publishing: WritePost a slot, mark a hand-off posted, and pause or resume a series.YesYesNoNo
Brand: WriteThe brand's name, slug, logo, studio defaults and notifications.YesYesNoNo
Accounts: WriteConnect and disconnect social accounts.YesYesNoNo
Members: ReadSee members and pending invitations.YesYesYesYes
Members: WriteInvite people, change roles, remove members and cancel invitations.YesYesNoNo
API keys: WriteCreate, edit and revoke API keys.YesYesNoNo
OwnershipDelete the brand, make someone an owner, and hand ownership over.YesNoNoNo
Request header
Authorization: Bearer tso_G_USxN6rS4rzt0Enh3B_-uxinWqPhJHa
Missing or invalid key
{
  "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.

PlanPer secondPer monthKeys
Free15001
Creator525,0005
Studio20250,00020

Every response reports where you stand on both, so a client can pace itself instead of waiting to be turned away.

HeaderMeaning
X-RateLimit-LimitRequests allowed in one second.
X-RateLimit-RemainingRequests left in the current second.
X-RateLimit-ResetUnix time, in seconds, when the current window resets.
X-Quota-LimitRequests allowed this calendar month.
X-Quota-RemainingRequests left this calendar month.
Retry-AfterSeconds 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.

Response headers
HTTP/1.1 200 OK
X-RateLimit-Limit: 1
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1785115680
X-Quota-Limit: 500
X-Quota-Remaining: 483

Caching

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=100 reads a hundred posts in one request; perPage=10 spends 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.

Next.js, revalidating hourly
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();
Anywhere else
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.

CodeStatusMeaning
unauthorized401No key was sent, or the key is unknown or revoked.
forbidden403The key is valid but lacks a scope the endpoint requires.
notFound404Nothing with that id exists in this brand.
validationError400A parameter or body field is missing, malformed, or out of range.
rateLimitExceeded429Too many requests in one minute. Retry after the window resets.
quotaExceeded429The monthly request quota is used up. It resets at the start of next month.
creditsExhausted402The brand is out of credits: the month's allowance is spent and no purchased credits remain.
planRequired403The request uses something the brand's plan does not include, such as a video model reserved for a higher plan.
conflict409The 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.
serviceUnavailable503An upstream dependency didn't answer. Safe to retry shortly.
Error response
{
  "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.

PlanSeats
Free1
Creator3
StudioUnlimited

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.

ToolsRequires
list_posts, get_postpostsRead
get_brand, get_creation, list_assets, list_sounds, search_instagram_audio, list_cast_membersstudioRead
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_memberstudioWrite
list_series, get_series_queueseriesRead
save_series, update_series, run_autopilot_review, add_series_item, update_item, skip_item, delete_item, regenerate_item, generate_item, rewrite_captionseriesWrite
publish_item, mark_item_posted, set_series_activepublishWrite
list_organizations, create_organization, update_organizationbrandWrite
list_membersmembersRead
invite_member, update_member_role, cancel_invitationmembersWrite

Keep the key out of committed files. Reference it as an environment variable in .mcp.json, exactly as the sample shows.

Claude Code
claude mcp add --transport http topics https://topics.so/api/mcp \
  --header "Authorization: Bearer $TOPICS_API_KEY"
Any MCP client (.mcp.json)
{
  "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.

Install
pnpm add @topics-so/sdk
Generate an on-brand image
import { 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
// 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);
}
Handle errors
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

GET/api/v1/posts

Every 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"
Response
{
  "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

GET/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"
Response
{
  "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

GET/api/v1/brand-kit

The 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"
Response
{
  "imageModel": null,
  "videoModel": null,
  "baseStylePrompt": "Warm editorial lifestyle photography, natural light, shallow depth of field.",
  "discordWebhookUrl": null
}

Update the brand kit

PUT/api/v1/brand-kit

Replaces 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":""}'
Request body
{
  "baseStylePrompt": "Warm editorial lifestyle photography, natural light, shallow depth of field.",
  "discordWebhookUrl": ""
}
Response
{
  "imageModel": null,
  "videoModel": null,
  "baseStylePrompt": "Warm editorial lifestyle photography, natural light, shallow depth of field.",
  "discordWebhookUrl": null
}

Get credits

GET/api/v1/credits

The 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"
Response
{
  "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

GET/api/v1/assets

The 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"
Response
{
  "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

POST/api/v1/assets

Adds 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"}'
Request body
{
  "kind": "castReference",
  "name": "Maya, front-lit selfie",
  "sourceUrl": "https://example.com/maya-front.jpg"
}
Response
{
  "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

POST/api/v1/assets/uploads

Step 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}'
Request body
{
  "kind": "inspiration",
  "name": "Balcony light study",
  "contentType": "image/jpeg",
  "size": 482133
}
Response
{
  "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

DELETE/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"
Response
204 No Content

List sounds

GET/api/v1/sounds

The 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"
Response
{
  "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

GET/api/v1/sounds/search

Instagram'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"
Response
{
  "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

POST/api/v1/sounds

Adds 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"}'
Request body
{
  "audioId": "1234567890123456",
  "description": "Warm, mid-tempo; the slow-morning reels"
}
Response
{
  "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

PATCH/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}'
Request body
{
  "active": false
}
Response
{
  "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

DELETE/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"
Response
204 No Content

List cast members

GET/api/v1/cast-members

The 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"
Response
{
  "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

POST/api/v1/cast-members

A 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"]}'
Request body
{
  "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"
  ]
}
Response
{
  "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

GET/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"
Response
{
  "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

PATCH/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"}'
Request body
{
  "lifestylePrompt": "Moved to Porto; surf mornings, studio afternoons"
}
Response
{
  "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

DELETE/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"
Response
204 No Content

List creations

GET/api/v1/creations

The 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"
Response
{
  "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

POST/api/v1/creations/compose

Expands 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"}'
Request body
{
  "idea": "Morning mobility routine on a sunlit balcony",
  "castMemberId": "cmb2n9f2t0005s601m3hq8v1d"
}
Response
{
  "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

POST/api/v1/creations

Starts 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}}'
Request body
{
  "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
  }
}
Response
{
  "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

GET/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"
Response
{
  "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

POST/api/v1/creations/{id}/retry

Re-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"
Response
{
  "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

POST/api/v1/creations/{id}/animate

Animates 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"}'
Request body
{
  "motionPrompt": "A slow push in; her hair moves in a light breeze.",
  "durationSeconds": 5,
  "soundId": "cmb2nsnd10001s601q4w8e2rt"
}
Response
{
  "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

POST/api/v1/creations/{id}/overlay

Swaps 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"}}'
Request body
{
  "overlay": {
    "template": "centeredTitle",
    "text": "Move first, coffee second"
  }
}
Response
{
  "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

DELETE/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"
Response
204 No Content

List series

GET/api/v1/series

Every 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"
Response
{
  "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

POST/api/v1/series

A `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"}'
Request body
{
  "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"
}
Response
{
  "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

GET/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"
Response
{
  "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

PATCH/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}'
Request body
{
  "guidance": "The dose calculator carousel outperformed; lean harder on it.",
  "maxPerWeek": 4
}
Response
{
  "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

DELETE/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"
Response
204 No Content

Run the strategist now

POST/api/v1/series/{id}/reviews

Runs 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"
Response
{
  "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

POST/api/v1/series/{id}/items

Adds 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"}'
Request body
{
  "scheduledFor": "2026-08-27T08:00:00Z",
  "format": "post",
  "sourceUrl": "https://example.com/maya-espresso.jpg"
}
Response
{
  "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

POST/api/v1/series/{id}/items/{itemId}/skip

Opts 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"
Response
204 No Content

Regenerate a slot

POST/api/v1/series/{id}/items/{itemId}/regenerate

Sends 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"
Response
204 No Content

Edit a slot

PATCH/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"]}'
Request body
{
  "caption": "The 90-second mobility opener that sticks. Link in bio.",
  "hashtags": [
    "mobility",
    "morningroutine"
  ]
}
Response
{
  "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

DELETE/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"
Response
204 No Content

Rewrite a slot's caption

POST/api/v1/series/{id}/items/{itemId}/caption

Writes 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"
Response
{
  "id": "cmb2nhtvc000fs601r5t7v9xy",
  "caption": "The 90-second mobility opener that sticks.",
  "hashtags": [
    "mobility",
    "morningroutine"
  ]
}

Publish a slot now

POST/api/v1/series/{id}/items/{itemId}/publish

Delivers 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"
Response
204 No Content

Mark a hand-off as posted

POST/api/v1/series/{id}/items/{itemId}/posted

Closes 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"}'
Request body
{
  "postId": "cm8xk21p40001s6014r9e2n7q"
}
Response
{
  "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

GET/api/v1/members

Everyone 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"
Response
{
  "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

PATCH/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"}'
Request body
{
  "role": "admin"
}
Response
{
  "id": "kq2V8xR1mN4pT6wY9zB3c",
  "name": "Jane Okafor",
  "email": "jane@agency.com",
  "role": "admin",
  "joinedAt": "2026-09-02T14:20:00.000Z",
  "planHolder": false
}

List invitations

GET/api/v1/invitations

Invitations 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"
Response
{
  "data": [
    {
      "id": "h7Lw2QeR5tY8uI1oP4aSd",
      "email": "sam@agency.com",
      "role": "viewer",
      "invitedBy": "Geno Fabelo",
      "expiresAt": "2026-10-22T09:00:00.000Z"
    }
  ]
}

Invite someone

POST/api/v1/invitations

Emails 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"}'
Request body
{
  "email": "sam@agency.com",
  "role": "viewer"
}
Response
{
  "id": "h7Lw2QeR5tY8uI1oP4aSd",
  "email": "sam@agency.com",
  "role": "viewer",
  "invitedBy": "Geno Fabelo",
  "expiresAt": "2026-10-22T09:00:00.000Z"
}

Cancel an invitation

DELETE/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"
Response
204 No Content

List brands

GET/api/v1/brands

Every 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"
Response
{
  "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

POST/api/v1/brands

Creates 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}'
Request body
{
  "name": "Acme Recovery",
  "createApiKey": true
}
Response
{
  "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

PATCH/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."}'
Request body
{
  "name": "Acme Mobility Co."
}
Response
{
  "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.