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_create_post                 โ†’ POST /posts
blotato_get_post_status             โ†’ GET  /posts/:postSubmissionId
blotato_list_posts                  โ†’ GET  /posts
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)

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)

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
  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
  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)
  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

================================================================================
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

Facebook Pages and LinkedIn Company Pages do not count toward the cap.

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.

Last updated