Skip to content

Content board

The content board carries one card per video, through seven columns:

IDEA → SCRIPT → VALIDATED → TO_SHOOT → TO_EDIT → TO_SCHEDULE → PUBLISHED

The card does not disappear when the video goes out: tied to the posts scheduled from it, it moves to PUBLISHED by itself when the first of them goes live, then carries their numbers. These endpoints read and write the same cards as the board in the app, so a card created from a voice note, a Notion page or a DM shows up there straight away.

These endpoints do not exist in the original project.

GET /contents
GET /contents/performance
GET /contents/{id}
POST /contents
POST /contents/upsert
PUT /contents/{id}
PUT /contents/{id}/move
PUT /contents/{id}/post
DELETE /contents/{id}

A card of another organisation, or one that was deleted, answers 404.

POST /contents

Field Type Required Description
title string Yes Short title of the video
externalId string No Your own id for the card (a script file path, a build id). Unique per organisation: a second card with the same one answers 409
stage string No The column to start in. Default IDEA
hook string No The spoken hook, the first seconds that stop the scroll. Markdown
hookText string No The text shown on screen over the first seconds
script string No Markdown: the board renders it (tables, bold, lists)
notes string No
tags string[] No Free labels, e.g. a series
resourceStatus string No The resource the video gives away (a link, a guide, a masterclass): READY, or TO_CREATE. Omitted: no resource. See The resource of a video
resourceUrl string No Where the resource lives, or will live
resourceBrief string No What the resource holds, or the prompt to make it. Markdown
integrationIds string[] No The channels the video is meant for
plannedAt string No ISO date the video should go out
parentId string No The card this one is repurposed from

The card lands at the bottom of its column. The answer is 201 with the card.

{
"title": "Three mistakes when moving to Malta",
"hook": "Nobody told me about the third one.",
"integrationIds": ["cm4ean69r0003w8w1cdomox9n"]
}

POST /contents/upsert takes the same fields as creation, with externalId required. The card holding that externalId is updated with the fields sent; without one, a card is created. Re-running an import therefore never duplicates, and you do not need to keep a table of your ids against ours.

On an existing card, stage is ignored: someone may have moved the card since, and the import should not move it back. Move it with PUT /contents/{id}/move.

The answer is { "created": true | false, "content": { … } }.

A deleted card lets go of its externalId: the next upsert creates a new card.

PUT /contents/{id} takes the same fields as creation except stage and parentId, plus videoMediaId and videoPath (the final video, from the media library). Only the fields sent change, and null clears one.

PUT /contents/{id}/move

Field Type Required Description
stage string Yes The target column
position number No Rank inside the column (fractional, ascending). Omitted, the card goes to the bottom

A video that promises a link (the resources it mentions, a guide, a masterclass) must not go out before the link leads somewhere. A card whose resourceStatus is TO_CREATE therefore cannot move to TO_SCHEDULE or PUBLISHED: the move, or a creation straight into one of these columns, answers 409. Set resourceStatus to READY once the resource exists.

The card still moves by itself when one of its posts goes live (see below): that is a fact, not a choice.

PUT /contents/{id}/post with { "postGroup": "…" }: the group of a post created with POST /posts. A card holds several posts, for example a TikTok draft sent now and an Instagram post scheduled for tomorrow: each call adds one to the card’s postGroups, and sending the same group twice changes nothing. A card that is still behind TO_SCHEDULE jumps there, and it moves to PUBLISHED when the first channel of any of its posts goes live (at once, if one already has).

A TikTok post sent to the app inbox (SENT_UNCONFIRMED) does not count as live: the video still waits for someone to publish it in TikTok.

GET /contents returns every card, in board order. On top of its fields, each card has childrenCount (the cards repurposed from it) and posts: one entry per channel of each post scheduled from it, with its state, publishDate and releaseURL. GET /contents/{id} returns a single card, in the same shape.

Query parameters, all optional and combined:

Parameter Description
stage Only this column
tag Only the cards carrying this tag
externalId The card holding this id
fields Comma-separated fields to return, e.g. id,title,stage. The id always comes back; an unknown field answers 400. Omitted, every field comes back, scripts included
GET /contents?stage=SCRIPT&fields=id,title,externalId

GET /contents/performance returns an object keyed by card id, for the published cards tied to a post:

Field Description
views Sum across channels. null when no channel is collected, never a 0 nobody measured
engagementRate Interactions / views, in %
skipRate Instagram only, weighted by views
followersGained What the accounts gained in the 48 hours after publication
leads, emails People who went through an automation posed on the post, and the addresses collected
channels The same numbers per channel

The counters come from the collected history, read every 30 minutes: a channel whose collection is off has nulls.