Blotato
Create, schedule, and publish social media posts to 9 platforms from Claude, Cursor, or any AI agent. Hosted server with OAuth or API key auth.
Dokumentation
For the complete documentation index, see llms.txt. Markdown versions of documentation pages are available by appending
.mdto page URLs; this page is available as Markdown.
API Reference for LLMs
This is the machine-readable API reference for AI agents and LLM integrations.
BLOTATO API REFERENCE
=====================
IMPORTANT: CHECK HOW YOU ARE CONNECTED
- If you have blotato_create_post, blotato_list_accounts, etc. as MCP tools:
You are connected via MCP. Call those tools directly. You do NOT need HTTP
endpoints, base URLs, or auth headers. See tool descriptions for parameters.
MCP tools reference: https://help.blotato.com/api/mcp/tools
- If you are making HTTP requests directly (no MCP tools available):
Use the REST API reference below.
REST API:
Base URL: https://backend.blotato.com/v2
Auth Header: blotato-api-key: YOUR_API_KEY
Content-Type: application/json
API KEY FORMAT: Your key sometimes ends with one or more "=" characters (base64
padding). The "=" is part of the key. Send it exactly. Do not strip, trim, or
URL-encode it. In shells, .env files, and scripts, wrap the key in single quotes
(for example 'abc123=='), because "=" is a special character in those contexts.
A 401 or "invalid API key" almost always means the key sent does not match, and a
dropped trailing "=" is the most common cause. Verify with GET /users/me.
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
================================================================================
MCP TOOL → REST API MAPPING (for reference only)
================================================================================
blotato_get_user → GET /users/me
blotato_list_accounts → GET /users/me/accounts
blotato_get_credits → GET /credits
blotato_buy_credits → POST /credits
blotato_create_post → POST /posts
blotato_get_post_status → GET /posts/:postSubmissionId
blotato_list_posts → GET /posts
blotato_list_top_posts → GET /analytics
blotato_get_post_analytics → GET /posts/:id/analytics
blotato_list_comments → GET /comments
blotato_get_comment → GET /comments/:commentId
blotato_post_comment → POST /comments
blotato_list_conversations → GET /conversations
blotato_get_conversation → GET /conversations/:conversationId
blotato_list_messages → GET /messages
blotato_get_message → GET /messages/:messageId
blotato_send_message → POST /messages
blotato_create_source → POST /source-resolutions-v3 (polls internally)
blotato_get_source_status → GET /source-resolutions-v3/:id
blotato_list_visual_templates → GET /videos/templates
blotato_create_visual → POST /videos/from-templates
blotato_get_visual_status → GET /videos/creations/:id
blotato_list_schedules → GET /schedules
blotato_get_schedule → GET /schedules/:id
blotato_update_schedule → PATCH /schedules/:id
blotato_delete_schedule → DELETE /schedules/:id
blotato_list_pinterest_boards → GET /social/pinterest/boards
blotato_create_presigned_upload_url → POST /media/uploads
================================================================================
REST API ENDPOINTS
================================================================================
USER INFO & CONNECTED ACCOUNTS
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
GET /social/pinterest/boards?accountId=ID - List Pinterest boards (get boardId)
CREDITS (billing)
GET /credits - Get credit balance, account email, and pricing (price per 1,000 credits, min/max purchase quantity)
POST /credits - Create a Stripe Checkout link to buy credits (quantity 1000-10000). Returns checkoutUrl. Creating the link does NOT charge; a human completes payment in the browser. Confirm the account email first (credits are non-transferable). 10 req/hour
PUBLISHING
POST /posts - Create/publish a post (30 req/min)
GET /posts - List posts: scheduled/published/failed, cursor-paginated (60 req/min)
GET /posts/:postSubmissionId - Poll post status (60 req/min)
ANALYTICS (Published Post Metrics)
Collected for twitter/instagram/facebook/threads/bluesky only (others return no metrics yet).
GET /analytics - Top performing published posts (sortBy likes_count/comments_count/views_count/reach_count, default views_count; since/until/platform/limit)
GET /posts/:id/analytics - Latest metrics + snapshot history for one published post (id = published post id, not postSubmissionId)
GET /published-posts - Search published posts (query/platform) with latest analytics snapshot attached
Metrics: every field is optional; counts are strings, rates are numbers, breakdowns are objects.
Full metric list (common + platform-specific): https://help.blotato.com/api/analytics-metrics
COMMENTS (Instagram, Facebook)
GET /comments - List comments on your Instagram and Facebook posts, cursor-paginated (60 req/min). Filters: postId, parentCommentId, accountId, platform, since, until
GET /comments/:commentId - Get a single comment (60 req/min). Poll to check comment status
POST /comments - Post a top-level comment, or a reply when parentCommentId is set, on a published Instagram or Facebook post (30 req/min)
MESSAGING (Instagram, Facebook)
GET /conversations - List direct-message conversations, cursor-paginated (60 req/min)
GET /conversations/:conversationId - Get a single conversation (60 req/min)
GET /messages - List messages, cursor-paginated (60 req/min)
GET /messages/:messageId - Get a single message (60 req/min). Poll to check send status
POST /messages - Send a direct message or a private reply to a comment; async, poll GET /messages/:messageId until sent|failed (30 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)
POST /media/uploads - Get presigned upload URL for local files (120 req/min)
================================================================================
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.
For YouTube, also fetch subaccounts to get playlistIds.
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.
For YouTube accounts, subaccounts return playlists. Use items[].id values as target.playlistIds (array).
For Pinterest, fetch boards to get boardId:
GET /social/pinterest/boards?accountId={accountId}
Response: { "items": [{ "id": "1234567890123456789", "name": "Summer Outfits" }] }
Use items[].id as target.boardId when publishing a pin.
================================================================================
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.
- For local files without a public URL, use POST /media/uploads to get a presigned upload URL:
1. POST /media/uploads with {"filename": "photo.jpg"} -> returns {presignedUrl, publicUrl}
2. PUT the file binary to presignedUrl with correct Content-Type header
3. Use publicUrl in mediaUrls when publishing
Max file size depends on plan (see [Plan Limits](../settings/billing-and-credits.md#plan-limits)). No Google Drive or S3 needed.
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.
carousels (LinkedIn Document / PDF carousel): pass 2-10 image URLs (JPG, PNG) in content.mediaUrls and Blotato auto-builds a LinkedIn Document carousel — LinkedIn's modern PDF-based carousel format, viewers swipe through pages like a PDF. This is the same format you'd use for Instagram carousels — pass the same image URLs to LinkedIn and Blotato handles the conversion. Videos are not supported in carousels. Max 10 images.
facebook:
pageId (REQUIRED) - from GET /users/me/accounts/{accountId}/subaccounts
mediaType - "reel" REQUIRED for videos (regular feed videos no longer supported), "story" for Stories, omit for text/image posts. Stories require one video or image attachment (only the first is used if multiple provided).
link (optional) - URL to attach as link preview
firstComment (optional) - auto-posts as the first comment right after publishing. Up to 8000 chars. Posts and reels only, NOT stories.
Reel video specs: MP4/MOV/AVI, max 1 GB, min 540x960, 9:16, 3-90s, 24-60 fps, H.264/H.265 (VP9/AV1 ok), AAC LC audio 128kbps+ 48kHz stereo
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. Single image posts and reels only - NOT supported on carousels (multi-media posts), which will fail to publish.
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)
firstComment (optional) - auto-posts as the first comment right after publishing. Up to 2200 chars. Posts, carousels, and reels, NOT stories. Useful for links.
trial (optional) - object for trial reels (shown to non-followers first). Only for reels.
graduationStrategy (REQUIRED inside trial) - "MANUAL" or "SS_PERFORMANCE"
MANUAL = you promote to followers manually. SS_PERFORMANCE = Instagram auto-promotes based on performance.
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) - from GET /social/pinterest/boards?accountId={accountId}
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
playlistIds (optional) - array of playlist IDs to add the video to. Get from GET /users/me/accounts/{accountId}/subaccounts
thumbnailUrl (optional) - publicly accessible image URL for custom thumbnail. Requires verified YouTube account with custom thumbnail capabilities.
YouTube "description" comes from the content.text field. Tags are not supported.
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
}
Optional fields:
title - string, human-readable title for the generated video
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 (accountId, content, target). The endpoint
does NOT merge partial drafts. If you pass an incomplete draft, the update
silently no-ops on the draft side. Recommended flow: GET /schedules/:id first,
edit the returned object in place, then PATCH the whole object back.
- DO NOT also call POST /posts (or blotato_create_post) when editing a schedule.
PATCH /schedules/:id updates the existing schedule in place. Calling create
publishes a separate post and you end up with duplicates at publish time.
- 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
================================================================================
MANAGE COMMENTS
================================================================================
Comments are public comments your audience leaves on your published posts, plus
comments you post back through Blotato.
Supported platforms: instagram, facebook
Notes:
- Blotato does not backfill. It captures comments from when the account connects.
- Comments are retained for up to 45 days.
- Lists and posts top-level comments and one level of replies (no reply to a reply).
List comments:
GET /comments?limit=20&cursor=OPTIONAL_CURSOR&accountId=OPTIONAL_ACCOUNT_ID&postId=OPTIONAL_POST_ID&since=ISO_8601&until=ISO_8601
Response: { "items": [ <Comment>, ... ], "cursor": "..." }
Post a comment:
POST /comments
{
"postId": "id-of-post-to-leave-comment-on",
"text": "Comment text",
"parentCommentId": "optional-id-of-comment-to-reply-to"
}
Response: a <Comment> object with status "queued"
PLATFORM-SPECIFIC FIELDS:
instagram:
text limit: 2200 characters
facebook:
text limit: 8000 characters
comments post to your Facebook Page posts
Poll: GET /comments/:commentId
Status values: "queued" | "processing" | "posted" | "failed" | "deleted"
- posted: live on the platform
- failed: response includes errorMessage and errorCode
<Comment>:
{
"id": "cmt_abc123",
"accountId": "98434",
"platform": "instagram",
"postId": "post_xyz789", // null if not published through Blotato
"parentCommentId": null, // set when this comment is a reply
"platformPostId": "17851234567890",
"platformCommentId": "17899876543210", // null until posted
"authorId": "1784000000", // null if unknown
"isAuthor": false, // true = you authored it, false = audience
"text": "Love this!",
"status": "posted",
"errorCode": null,
"errorMessage": null,
"createdAt": "2026-07-01T12:34:56Z"
}
NOTE: Replying to a new audience member counts toward the monthly active-contacts limit
(starter 100 / creator 250 / agency 1000).
================================================================================
MANAGE PRIVATE MESSAGES
================================================================================
Messages are private, direct messages sent through the platform inbox (DMs). Read conversations and messages, and send a direct message or a private
reply to a comment.
Supported platforms: instagram, facebook
Notes:
- Messages are retained for up to 45 days.
- You reply to people who already messaged you. No cold outreach.
- Platforms apply strict messaging windows. Review "MESSAGING WINDOWS" before sending messages.
- Reaching a new person counts toward the monthly active-contacts limit
(starter 100 / creator 250 / agency 1000).
List conversations:
GET /conversations?limit=20&cursor=OPTIONAL_CURSOR&accountId=OPTIONAL_ACCOUNT_ID&platform=OPTIONAL_PLATFORM
Response: { "items": [ <Conversation>, ... ], "cursor": "..." }
Get a conversation:
GET /conversations/:conversationId
Response: a <Conversation> object
<Conversation>:
{
"id": "cnv_abc123",
"accountId": "98434",
"platform": "instagram",
"participants": [
{ "id": "1784000000", "name": "Jamie", "username": "jamie", "profileImageUrl": null }
],
"createdAt": "2026-07-01T12:00:00Z",
"updatedAt": "2026-07-02T09:30:00Z"
}
List messages:
GET /messages?limit=20&cursor=OPTIONAL_CURSOR&conversationId=OPTIONAL_CONVERSATION_ID&platform=OPTIONAL_PLATFORM
Response: { "items": [ <Message>, ... ], "cursor": "..." }
Send a message:
POST /messages
{
"accountId": "required-account-id",
"recipientId": "id-of-contact-to-send-message-to",
"text": "Thanks for reaching out!",
"target": { "targetType": "instagram" }
}
PLATFORM-SPECIFIC FIELDS:
instagram:
text limit: 1000 bytes
Direct message, reply to a contact within the 24-hour window:
recipientId: the incoming message's senderId
target.targetType: "instagram"
Private reply to a comment, once, within 7 days of the comment:
recipientId: the comment's authorId
target.targetType: "instagram"
target.commentId: "id-of-comment-to-reply-to"
facebook:
text limit: 2000 characters
target.pageId (the Facebook Page to send from) is required on every Facebook message
Direct message, reply to a contact within the 24-hour window:
recipientId: the incoming message's senderId
target.targetType: "facebook"
target.pageId: "id-of-page-to-send-from"
Private reply to a comment, once, within 7 days of the comment:
recipientId: the comment's authorId
target.targetType: "facebook"
target.pageId: "id-of-page-to-send-from"
target.commentId: "id-of-comment-to-reply-to"
Poll: GET /messages/:messageId
Status values: "queued" | "processing" | "sent" | "delivered" | "failed"
- sent / delivered: handed to / delivered by the platform
- failed: response includes errorMessage and errorCode
<Message>:
{
"id": "msg_abc123",
"conversationId": "cnv_abc123", // null when not linked to a conversation
"platform": "instagram",
"direction": "incoming", // "incoming" received, "outgoing" you sent
"senderId": "1784000000", // null if unknown
"recipientId": "17841400000000000",
"text": "Hi! Is this available?",
"status": "delivered",
"errorCode": null,
"errorMessage": null,
"createdAt": "2026-07-02T09:30:00Z"
}
MESSAGING WINDOWS:
- Standard DM: reply within 24 hours of the person's last message.
- Private reply to a comment: set target.commentId. Allowed once, within 7 days of the
comment. After it, message the person again only after they open a new 24-hour window.
================================================================================
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)
For YouTube: also GET /users/me/accounts/{accountId}/subaccounts to get playlist IDs
Use subaccount ids as target.playlistIds (optional, array)
For Pinterest: also GET /social/pinterest/boards?accountId={accountId}
Use board id as target.boardId (REQUIRED for Pinterest)
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
================================================================================
PLAN LIMITS
================================================================================
Blotato has three plans (starter, creator, agency). Each plan enforces:
starter creator agency
Max connected social accounts 20 40 100
Max media upload size 400 MB 1 GB 1 GB
Queued scheduled posts 200 1000 3000
Scheduling horizon (months) 9 12 24
Active contacts per month 100 250 1000
Facebook Pages and LinkedIn Company Pages count toward the cap.
* Facebook: Each page counts as one account (the login does not count)
* LinkedIn: Each profile and each connected company Page each count as one account.
For synchronous calls like POST /posts, exceeding a plan limit returns HTTP 422.
For asynchronous calls like POST /media, errors surface asynchronously and the
records associated with the job will transition to status "failed".
================================================================================
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
POST /media/uploads: 120 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.
Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.
Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.
Perform an HTTP GET request on the current page URL with the ask query parameter, and the optional goal query parameter:
GET https://help.blotato.com/api/llm.md?ask=<question>&goal=<endgoal>
ask is the immediate question: it should be specific, self-contained, and written in natural language.
goal is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.
Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.