API Reference for LLMs

This is the machine-readable API reference for AI agents and LLM integrations.

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