API Reference for LLMs
BLOTATO API REFERENCE
=====================
Base URL: https://backend.blotato.com/v2
Auth Header: blotato-api-key: YOUR_API_KEY
Content-Type: application/json
All creation operations are ASYNC. Submit a request, then poll for status.
Docs: https://help.blotato.com/api/start
OpenAPI: https://help.blotato.com/api/api-reference/openapi-reference
Errors: https://help.blotato.com/support/errors
================================================================================
ENDPOINTS
================================================================================
USER INFO & CONNECTED ACCOUNTS (all under /users/me/)
GET /users/me - Verify API key, get user info
GET /users/me/accounts - List connected social accounts (get accountId)
GET /users/me/accounts/:accountId/subaccounts - Get Facebook/LinkedIn pageId
PUBLISHING
POST /posts - Create/publish a post (30 req/min)
GET /posts/:postSubmissionId - Poll post status (60 req/min)
VISUALS
POST /videos/from-templates - Create visual from template (30 req/min)
GET /videos/creations/:id - Poll visual status
GET /videos/templates - List available templates
DELETE /videos/:id - Delete a video
SOURCES (Content Extraction)
POST /source-resolutions-v3 - Extract content from URL/text (30 req/min)
GET /source-resolutions-v3/:id - Poll source status (60 req/min)
SCHEDULES (Content Calendar)
GET /schedules - List future scheduled posts (cursor-paginated)
GET /schedules/:id - Get a single scheduled post
PATCH /schedules/:id - Update scheduled post (content and/or time)
DELETE /schedules/:id - Delete scheduled post and cancel publishing job
SCHEDULE SLOTS (Recurring Time Windows)
GET /schedule/slots - List all scheduling slots
POST /schedule/slots - Create one or more slots
PATCH /schedule/slots/:id - Update slot targets (platforms/accounts)
DELETE /schedules/slots/:id - Delete a slot
POST /schedule/slots/next-available - Find next open slot for a platform/account
MEDIA
POST /media - Upload media from URL (30 req/min, optional)
================================================================================
STEP 0: GET ACCOUNTS (always do this first)
================================================================================
GET /users/me/accounts
GET /users/me/accounts?platform=twitter (filter by platform)
Response:
{
"items": [
{ "id": "98432", "platform": "twitter", "fullname": "Jane", "username": "jane" }
]
}
For Facebook/LinkedIn, also fetch subaccounts to get pageId:
GET /users/me/accounts/98432/subaccounts
Response:
{
"items": [
{ "id": "123456789", "accountId": "98432", "name": "My Business Page" }
]
}
Use items[].id as target.pageId when publishing to Facebook or LinkedIn.
Pinterest boardId is not available via API. Ask the user for it.
================================================================================
PUBLISHING A POST
================================================================================
POST /posts
Minimal payload (Twitter):
{
"post": {
"accountId": "98432",
"content": {
"text": "Hello world",
"mediaUrls": [],
"platform": "twitter"
},
"target": {
"targetType": "twitter"
}
}
}
RULES:
- content.platform and target.targetType must be set to the same value
- mediaUrls is required. Pass [] for text-only posts. Pass public URLs for media.
- accountId comes from GET /users/me/accounts
- No upload step needed. Pass any public URL in mediaUrls.
SCHEDULING (optional, top-level fields alongside "post"):
- scheduledTime: ISO 8601 timestamp with timezone offset (e.g., "2026-03-04T16:30:00+00:00") to publish at a specific time. If provided, useNextFreeSlot is ignored.
- useNextFreeSlot: true to schedule at the user's next available calendar slot. Requires at least one calendar slot configured for the target platform.
- If NEITHER scheduledTime NOR useNextFreeSlot is provided, the post PUBLISHES IMMEDIATELY.
- Both fields MUST be root-level (siblings of "post"). If nested inside "post", "options", or any other object, they are IGNORED and the post publishes immediately.
Publish immediately (no scheduling fields):
{
"post": { "accountId": "98432", "content": {...}, "target": {...} }
}
Schedule at user's next free calendar slot:
{
"post": { "accountId": "98432", "content": {...}, "target": {...} },
"useNextFreeSlot": true
}
Schedule at a specific time:
{
"post": { "accountId": "98432", "content": {...}, "target": {...} },
"scheduledTime": "2025-12-25T15:00:00Z"
}
WRONG (do NOT nest scheduling fields inside "post" or "options"):
{
"post": { "accountId": "98432", "content": {...}, "target": {...}, "useNextFreeSlot": true }
}
{
"post": { "accountId": "98432", "content": {...}, "target": {...} },
"options": { "scheduledTime": "2026-03-04T16:30:00+00:00" }
}
THREADS (Twitter, Bluesky, Threads):
Use content.additionalPosts[] to create a thread in a single API call.
The first tweet goes in content.text. Additional tweets go in additionalPosts[].
{
"post": {
"accountId": "98432",
"content": {
"text": "First tweet in the thread (1/3)",
"mediaUrls": [],
"platform": "twitter",
"additionalPosts": [
{ "text": "Second tweet (2/3)", "mediaUrls": [] },
{ "text": "Third tweet (3/3)", "mediaUrls": [] }
]
},
"target": { "targetType": "twitter" }
}
}
Each additionalPosts[] entry has: text (string), mediaUrls (array of strings).
Blotato handles reply chaining. You do NOT need to capture tweet IDs.
Works for: twitter, bluesky, threads.
PLATFORM-SPECIFIC TARGET FIELDS (set these inside "target" alongside "targetType"):
twitter:
(no extra fields required)
linkedin:
pageId (optional) - LinkedIn Company Page ID from subaccounts endpoint. Omit for personal profile.
facebook:
pageId (REQUIRED) - from GET /users/me/accounts/{accountId}/subaccounts
mediaType (optional) - "video" or "reel"
link (optional) - URL to attach as link preview
instagram:
mediaType (optional) - "reel" or "story". Default: "reel". No effect on image posts.
altText (optional) - alt text for images, up to 1000 characters
collaborators (optional) - array of Instagram handles (without @), max 3
coverImageUrl (optional) - cover image URL for reels, max 8MB
shareToFeed (optional) - boolean, share the reel to feed
audioName (optional) - custom audio name for reels (can only set once)
tiktok (ALL of these are REQUIRED):
privacyLevel - "SELF_ONLY", "PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR"
disabledComments - boolean
disabledDuet - boolean
disabledStitch - boolean
isBrandedContent - boolean
isYourBrand - boolean
isAiGenerated - boolean
(optional) title - for image posts, max 90 chars
(optional) autoAddMusic - boolean, for photo posts only
(optional) isDraft - boolean, save as draft
(optional) imageCoverIndex - number, cover image index for carousels (starts from 0)
(optional) videoCoverTimestamp - number, milliseconds for video cover frame
pinterest:
boardId (REQUIRED) - not available via API, ask the user
title (optional)
altText (optional)
link (optional)
threads:
replyControl (optional) - "everyone", "accounts_you_follow", "mentioned_only"
bluesky:
(no extra fields required)
youtube:
title (REQUIRED) - video title
privacyStatus (REQUIRED) - "private", "public", "unlisted"
shouldNotifySubscribers (REQUIRED) - boolean
isMadeForKids (optional) - boolean, default false
containsSyntheticMedia (optional) - boolean, for AI-generated content
webhook:
url (REQUIRED) - the webhook URL
Response: { "postSubmissionId": "uuid" }
Poll: GET /posts/{postSubmissionId}
Status values: "in-progress" | "published" | "failed"
- published: response includes "publicUrl"
- failed: response includes "errorMessage"
================================================================================
CREATING VISUALS
================================================================================
POST /videos/from-templates
RECOMMENDED: Use "prompt" to describe what you want. Set "inputs" to {}.
AI fills in the template inputs automatically from your prompt.
Do NOT manually construct the "inputs" object unless you have a specific reason.
templateId: Use the bare UUID from the templates list (e.g., "77f65d2b-48cc-4adb-bfbb-5bc86f8c01bd").
Do NOT use the full path format (e.g., "/base/v2/quote-card/.../v1").
CORRECT:
{
"templateId": "77f65d2b-48cc-4adb-bfbb-5bc86f8c01bd",
"inputs": {},
"prompt": "Create 5 motivational quotes about entrepreneurship",
"render": true
}
WRONG (do not manually fill inputs, use prompt instead):
{
"templateId": "77f65d2b-48cc-4adb-bfbb-5bc86f8c01bd",
"inputs": { "text": "Slide 1: ..." },
"render": true
}
Response: { "item": { "id": "vid_123", "status": "queueing" } }
Poll: GET /videos/creations/{id}
Status values: "queueing" | "generating-script" | "script-ready" |
"generating-media" | "media-ready" | "exporting" |
"done" | "creation-from-template-failed"
Terminal states: "done" (success) or "creation-from-template-failed" (failure)
When done: response includes "mediaUrl" and/or "imageUrls"
Use mediaUrl or imageUrls in your publish request's mediaUrls field.
Discover templates: GET /videos/templates?fields=id,name,description,inputs
Template reference: https://help.blotato.com/api/visuals
================================================================================
EXTRACTING CONTENT (SOURCES)
================================================================================
POST /source-resolutions-v3
Body MUST contain a "source" object. Do NOT put fields at the top level.
source.sourceType (REQUIRED, no auto-detection):
- "text" - Transform raw text (uses source.text)
- "article" - Extract from article URL (uses source.url)
- "youtube" - Extract from YouTube URL (uses source.url)
- "twitter" - Extract from Twitter/X URL (uses source.url)
- "tiktok" - Extract from TikTok URL (uses source.url)
- "perplexity-query" - AI web research (uses source.text)
- "audio" - Extract from audio URL (uses source.url)
- "pdf" - Extract from PDF URL (uses source.url)
CORRECT:
{
"source": {
"sourceType": "youtube",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
}
}
WRONG (missing "source" wrapper):
{ "sourceType": "youtube", "url": "..." }
Response: { "id": "src_123" }
Poll: GET /source-resolutions-v3/{id}
Status values: "queued" | "processing" | "completed" | "failed"
When completed: response includes "content" and "title"
================================================================================
MANAGING SCHEDULED POSTS
================================================================================
List scheduled posts:
GET /schedules?limit=20&cursor=OPTIONAL_CURSOR
Response:
{
"items": [
{
"id": "sch_abc123",
"scheduledAt": "2026-04-01T14:00:00.000Z",
"account": {
"id": "98432",
"name": "Jane Smith",
"username": "janesmith",
"profileImageUrl": "https://...",
"subaccountId": null,
"subId": null,
"subaccountName": null
},
"draft": {
"accountId": "98432",
"content": { "text": "...", "mediaUrls": [], "platform": "twitter" },
"target": { "targetType": "twitter" }
}
}
],
"count": "12",
"cursor": "eyJzY2hlZHVsZWRBd..."
}
NOTE: "draft" has the same structure as the "post" object in POST /posts.
NOTE: "scheduledAt" is the response field name. "scheduledTime" is the input field name for updates.
Get single schedule:
GET /schedules/:id
Response: { "schedule": { ...same fields as list item... } }
Update schedule (content, time, or both):
PATCH /schedules/:id
{
"patch": {
"scheduledTime": "2026-04-05T10:00:00Z",
"draft": {
"accountId": "98432",
"content": { "text": "Updated text", "mediaUrls": [], "platform": "twitter" },
"target": { "targetType": "twitter" }
}
}
}
Response: 204 No Content
RULES:
- At least one of scheduledTime or draft must be provided.
- scheduledTime must be valid ISO 8601 and in the future.
- draft must be a complete post object (not a partial update).
- The update endpoint does NOT accept useNextFreeSlot. To reschedule to the next
available slot, first call POST /schedule/slots/next-available to get the time,
then pass it as scheduledTime.
Delete schedule:
DELETE /schedules/:id
Response: 204 No Content (publishing job is also cancelled)
================================================================================
SCHEDULE SLOTS (Content Calendar Time Windows)
================================================================================
Slots define recurring posting times. When you publish with useNextFreeSlot: true,
the system finds the next open slot for the target platform and schedules the post.
HOW SLOTS AND SCHEDULES RELATE:
1. Slots define your posting cadence (e.g., "Monday 9 AM for Twitter").
2. useNextFreeSlot: true (on POST /posts) picks the next open slot.
3. A slot is "occupied" when a scheduled post is queued at that time.
4. Manage what is queued (schedules) and when (slots) separately.
List slots:
GET /schedule/slots
Response:
{
"items": [
{
"id": "slot_1",
"hour": 9,
"minute": 0,
"day": "monday",
"selectedTargets": [
{ "platform": "twitter", "accountId": "98432", "subaccountId": null }
]
}
]
}
Create slots:
POST /schedule/slots
{
"slots": [
{
"hour": 9,
"minute": 0,
"day": "monday",
"selectedTargets": [
{ "platform": "twitter", "accountId": "98432", "subaccountId": null }
]
}
]
}
Response: 201 Created (returns created slots with IDs)
Update slot targets:
PATCH /schedule/slots/:id
{
"patch": {
"selectedTargets": [
{ "platform": "twitter", "accountId": "98432", "subaccountId": null }
]
}
}
Response: 204 No Content
Delete slot:
DELETE /schedules/slots/:id
Response: 204 No Content
NOTE: Fails if the slot has future scheduled content. Delete the schedules first.
Find next available slot:
POST /schedule/slots/next-available
{ "platform": "twitter", "accountId": "98432", "subaccountId": null }
Response: { "slot": { "slotId": "slot_1", "slotTime": "2026-04-02T09:00:00Z" } }
Use this when rescheduling a post to the next free slot:
1. POST /schedule/slots/next-available -> get slotTime
2. PATCH /schedules/:id -> set scheduledTime to slotTime
================================================================================
COMPLETE WORKFLOW (for AI agents)
================================================================================
1. accounts = GET /users/me/accounts
For Facebook or LinkedIn: also GET /users/me/accounts/{accountId}/subaccounts
Use subaccount id as target.pageId (REQUIRED for Facebook, optional for LinkedIn)
2. sourceId = POST /source-resolutions-v3 { source: { sourceType, url/text } }
3. POLL: GET /source-resolutions-v3/{sourceId} until status = "completed"
4. videoId = POST /videos/from-templates { templateId, prompt, render: true }
5. POLL: GET /videos/creations/{videoId} until status = "done"
6. postId = POST /posts { post: { accountId, content, target }, scheduledTime?: "ISO8601" }
scheduledTime and useNextFreeSlot go OUTSIDE "post", at the root level.
Omit both to publish immediately.
7. POLL: GET /posts/{postId} until status = "published"
8. DONE: use publicUrl from step 7
================================================================================
RATE LIMITS
================================================================================
POST /posts: 30 requests / minute
GET /posts/:id: 60 requests / minute
POST /videos/from-templates: 30 requests / minute
POST /source-resolutions-v3: 30 requests / minute
GET /source-resolutions-v3/:id: 60 requests / minute
POST /media: 30 requests / minute
GET /users/me/accounts: No limit
GET /users/me: No limit
429 response means rate limit exceeded. Check "message" for retry timing.
================================================================================
COMMON MISTAKES
================================================================================
1. Scheduling fields nested inside "post":
scheduledTime and useNextFreeSlot are ROOT-LEVEL fields, siblings of "post".
WRONG: { "post": { ..., "scheduledTime": "2025-12-25T15:00:00Z" } }
RIGHT: { "post": { ... }, "scheduledTime": "2025-12-25T15:00:00Z" }
If nested inside "post", the post publishes immediately instead of scheduling.
2. Missing pageId for Facebook:
Facebook REQUIRES target.pageId. You must call GET /users/me/accounts/{accountId}/subaccounts
to get the pageId before publishing. Without it, the request fails.
3. content.platform and target.targetType mismatch:
These two fields must have the same value (e.g., both "twitter").
4. Using template path instead of UUID for templateId:
WRONG: "templateId": "/base/v2/quote-card/.../v1"
RIGHT: "templateId": "77f65d2b-48cc-4adb-bfbb-5bc86f8c01bd"
5. Manually filling template inputs instead of using prompt:
Set "inputs": {} and describe what you want in "prompt". AI fills the inputs.
6. Wrong accounts endpoint path:
WRONG: GET /accounts or GET /v2/accounts
RIGHT: GET /users/me/accounts
There is no /accounts endpoint. Always use the full path /users/me/accounts.Last updated