# 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

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.
```


---

# Agent Instructions: 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:

```
GET https://help.blotato.com/api/llm.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
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.
