API Docs
Connect an account once, then publish with a single request, over REST or the hosted MCP server. Adding a platform never changes your code. Supported platforms: TikTok, Instagram, LinkedIn, YouTube, X, Threads, and Facebook.
Authentication
Create an API key in your dashboard (shown once). Send it as a bearer token on every request. The same key works for REST and MCP.
Authorization: Bearer <YOUR_API_KEY>1. Connect an account
First connect an account from the dashboard (click + Connect and choose a platform, which runs the OAuth flow). You do not create account IDs yourself; each connected account has an account value you pass to the API.
How to find your account value: call GET /v1/accounts (below). The account field of each result is exactly the string you put in accounts when publishing. It has the shape <platform>:<account_id>.
| Platform | Account value format | Example |
|---|---|---|
| TikTok | tiktok:<open_id> | tiktok:_0f3k9… |
| instagram:<ig_id> | instagram:17841… | |
| LinkedIn (personal) | linkedin:person:<id> | linkedin:person:Fyv… |
| LinkedIn (company page) | linkedin:org:<id> | linkedin:org:99… |
Note: LinkedIn account values contain a second colon (personal profile vs. company page). Always copy the whole account string from /v1/accounts rather than building it yourself.
List your connected accounts and their health:
GET https://creasionapp.com/v1/accounts
Authorization: Bearer <YOUR_API_KEY>
# → 200 {
# "accounts": [
# { "account": "tiktok:_0f3k9x", "platform": "tiktok",
# "display_name": "Ceria Store", "health": "ok",
# "connected_at": "2026-08-30T..." },
# { "account": "instagram:17841400", "platform": "instagram",
# "display_name": "arduinozehan", "health": "ok" },
# { "account": "linkedin:person:Fyv","platform": "linkedin",
# "display_name": "Gusman W.", "health": "ok" }
# ]
# }
#
# Copy an "account" value verbatim into the "accounts" array when publishing.2. Register media (optional)
Two modes: pull from a URL, or upload a file via a presigned PUT.
POST https://creasionapp.com/v1/media
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/json
{ "url": "https://cdn.you.com/teaser.mp4" }
# → 201 { "media": "media_...", "status": "ready" }3. Publish a post
Put one or more account values (from step 1) in accounts to fan out the same post to multiple channels at once. Returns 202 with a queued status, and publishing runs in the background; poll step 4 for per-channel results.
POST https://creasionapp.com/v1/posts
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/json
{
"accounts": [
"tiktok:_0f3k9x",
"instagram:17841400",
"linkedin:person:Fyv"
],
"content": "New drop is live \u23f0 #fyp",
"media": ["media_..."]
}
# → 202 { "id": "post_...", "status": "queued" }
#
# Note: TikTok requires a video; LinkedIn & Instagram accept image or video.LinkedIn document posts. Send a PDF (or PPT/PPTX/DOC/DOCX) to publish the swipeable format usually called a carousel. Add an optional document_title; LinkedIn falls back to the filename without one. LinkedIn accepts one media item per post. Sending more returns a validation_error rather than quietly dropping the rest, so combine slides into a single PDF.
POST https://creasionapp.com/v1/posts
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/json
{
"accounts": ["linkedin:person:Fyv"],
"content": "Our Q3 teardown, 12 slides.",
"media": ["media_..."],
"linkedin": { "document_title": "Q3 Teardown" }
}
# Limits: 100MB and 300 pages per document.Threads text-only posts. Threads is the one channel where a post needs no media at all: a text post is the normal case, not a fallback. Text is capped at 500 characters counted as UTF-8 bytes, so an emoji costs four. Over-length content is rejected before the request reaches Meta, so it never burns a slot in your 250-posts/24h quota.
POST https://creasionapp.com/v1/posts
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/json
{
"accounts": ["threads:1784140579"],
"content": "Shipping the new endpoint today.",
"threads": { "topic_tag": "api" }
}
# Reply to an existing post instead:
# "threads": { "reply_to_id": "17889615691921648" }
# Topic tags cannot contain periods or ampersands.X is billed per post. X charges per API call rather than a monthly fee, so publishing to it spends prepaid credit instead of your post quota. A post costs $0.02, or $0.25 if it contains a link, because X charges 13 times more for those. The response includes the cost whenever an X account is targeted, so you can record it without a second call.
POST https://creasionapp.com/v1/posts
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/json
{
"accounts": ["x:322500631:yourhandle"],
"content": "Shipping something new today."
}
# 202 Accepted
{
"id": "post_abc123",
"status": "queued",
"x_cost": {
"per_account_cents": 2,
"total_cents": 2,
"accounts": 1,
"has_link": false,
"platform_cost_cents": 1.5,
"platform_fee_cents": 0.5
}
}
# A post containing a URL is charged at the link rate:
# "has_link": true, "per_account_cents": 25
#
# What counts as a link: X shortens every URL to t.co, and it does that
# for bare domains too, so "example.com" typed without a scheme is
# billed as a link. Email addresses, @mentions and #hashtags are not.
#
# 280 weighted characters: a link always counts as 23 regardless of its
# real length, and emoji count as 2.
#
# Out of credit returns 402:
# { "error": { "code": "insufficient_credit", "retryable": false } }Facebook Pages. Publishing targets a Page, never a personal profile, because the Graph API does not offer profile publishing at all, so account references always look like facebook:page:<page_id>. One connection can produce several accounts, one per Page you administer. Attach a link preview explicitly; a URL inside your message is left as plain text.
POST https://creasionapp.com/v1/posts
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/json
{
"accounts": ["facebook:page:1234567890"],
"content": "New guide is live.",
"facebook": { "link": "https://creasionapp.com/docs" }
}
# A photo goes to /photos automatically when you attach media.
# Video uploads through Meta's Resumable Upload API and publishes to the Page.
# Connections made before video support was added must reconnect once.YouTube titles and hashtags. The title defaults to the first line of your content. Set youtube.title to control it directly. Either way we fill the remaining space in YouTube’s 100-character title with hashtags from your content, because YouTube renders those as clickable links, so a hashtag left only in the description is wasted. A tag that would not fit whole is skipped rather than cut in half.
POST https://creasionapp.com/v1/posts
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/json
{
"accounts": ["youtube:UCabc123"],
"content": "Episode 12 is live\n\n#podcast #interview #startup",
"media": ["media_..."],
"youtube": { "title": "Episode 12 — building in public" }
}
# Resulting title: "Episode 12 — building in public #podcast #interview #startup"
#
# YouTube ignores EVERY hashtag on a video that carries more than 60, so we
# trim past that ourselves rather than letting all of them be discarded.4. Check status
The post moves queued → publishing → published (or partial / failed). Each channel reports its own status and, once live, the post URL.
GET https://creasionapp.com/v1/posts/{id}
Authorization: Bearer <YOUR_API_KEY>
# → 200 {
# "status": "published",
# "results": [
# { "account": "tiktok:_0f3k9x", "status": "published",
# "url": "https://www.tiktok.com/@you/video/..." },
# { "account": "instagram:17841400", "status": "published",
# "url": "https://www.instagram.com/p/..." },
# { "account": "linkedin:person:Fyv", "status": "published",
# "url": "https://www.linkedin.com/feed/update/..." }
# ]
# }5. List your posts
Cursor paginated, newest first. Pass the next_cursor from a response back as cursor to walk backwards through history. A null next_cursor means you have reached the end.
Cursors are used rather than page numbers because publishing never stops: with offset paging, a post created while you are reading page 2 pushes one row onto page 3 that you already saw, and hides another entirely. A cursor marks an exact position, so new posts arriving at the top cannot disturb it.
GET https://creasionapp.com/v1/posts?limit=20
Authorization: Bearer <YOUR_API_KEY>
# 200
{
"posts": [
{ "id": "post_01J8ZQ", "content": "New drop is live",
"status": "published", "channels": 3,
"created_at": "2026-09-06T10:00:01.000Z" }
],
"next_cursor": "MjAyNi0wOS0wNlQxMDowMDowMS4wMDBafHBvc3RfMDFKOFpR"
}
# Next page:
# GET /v1/posts?limit=20&cursor=MjAyNi0wOS0wNlQxMDowMDowMS4wMDBafHBvc3RfMDFKOFpR
#
# limit defaults to 20 and cannot exceed 100.
# The cursor is opaque: pass it back unchanged rather than parsing it.
# A cursor we did not issue returns 400 validation_error rather than
# silently restarting from the first page.Webhooks
Register a URL to receive signed events instead of polling. Two events exist: post.completed when a post reaches a terminal state, and account.needs_reconnect when a stored token stops working.
POST https://creasionapp.com/v1/webhooks
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/json
{ "url": "https://you.com/hook", "events": ["post.completed"] }
# 201 Created, and this is the ONLY time the secret is returned:
{ "id": "wh_...", "secret": "whsec_...", "url": "https://you.com/hook" }
# Omit "events" to receive all of them.
# GET /v1/webhooks lists them, without secrets
# DELETE /v1/webhooks/<id> removes one
# POST /v1/webhooks/<id>/test sends a sample delivery
# The test reports the real outcome rather than just accepting the request:
# { "delivered": true, "status": 200 }
# { "delivered": false, "status": 500,
# "reason": "Your endpoint returned 500. It must reply 2xx." }
#
# Its payload carries "test": true and a fake post_id, so a receiver
# cannot mistake it for a real publish and act on it twice.All of this is also available in the dashboard, under Settings → Webhooks.
Publishing to several platforms routinely half-works, so the outcome lives in status rather than in the event name: published when every account succeeded, failed when none did, and partial when some did. Branch on status, and read targets for the per-account detail.
{
"event": "post.completed",
"post_id": "post_01J8ZQ",
"status": "partial",
"targets": [
{ "account": "tiktok:701234", "status": "published",
"url": "https://tiktok.com/@you/video/7312" },
{ "account": "linkedin:person:AbC", "status": "failed",
"code": "token_expired", "retryable": false }
],
"created_at": "2026-09-10T04:21:00.000Z"
}Every delivery carries X-Creasion-Signature in the form t=<unix>,v1=<hex>. The HMAC-SHA256 covers "<t>.<raw body>", so the timestamp cannot be altered without breaking the signature. Reject anything older than your tolerance window to stop replays.
import crypto from "node:crypto";
// Use the RAW body. Parsing and re-stringifying changes the bytes and
// the signature will never match.
function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("="))
);
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSec) {
return false;
}
const expected = crypto
.createHmac("sha256", secret)
.update(parts.t + "." + rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(parts.v1, "hex")
);
}Reply 2xx quickly and do your work afterwards. A delivery is retried up to three times with backoff and times out after 10 seconds, so a slow handler turns into a duplicate delivery rather than a lost one.
Hosted MCP server
Point any MCP client at the endpoint below with your API key. Tools map 1:1 to REST: list_accounts, publish_post, schedule_post, get_post_status.
https://creasionapp.com/v1/mcp (Streamable HTTP, Bearer <YOUR_API_KEY>)Errors
Every error uses a uniform shape so you can branch on code and honour retryable.
{ "error": { "code": "rate_limited", "message": "...",
"retryable": true, "retry_after_seconds": 42 } }Ready to build?
Get an API key