Skip to content
Developer docs · v1

Pylot API
& MCP.

One Bearer key, scoped to one business. Every operation exists twice: as a JSON endpoint under /api/v1 and as an MCP tool at /api/mcp. Same validation, same behaviour.

01

Authentication

Create a key in Dashboard → Settings → API keys. It is shown once; Pylot stores only a hash. Send it as a Bearer token on every request. A key is bound to exactly one business, so no businessId is needed anywhere.

curl https://www.getpylot.com/api/v1/me \
  -H "Authorization: Bearer pyl_live_…"

Keys inherit the plan of the workspace. Free-plan limits (post quota, one draft at a time) and credit balances apply exactly as in the dashboard.

02

MCP server

Streamable HTTP, stateless, JSON responses. Point any MCP client at https://www.getpylot.com/api/mcp with the same Authorization header. Calling it by hand? Send Accept: application/json, text/event-stream (Pylot adds it if missing).

Claude Code
claude mcp add --transport http pylot https://www.getpylot.com/api/mcp \
  --header "Authorization: Bearer pyl_live_…"
Claude Desktop · Cursor · Codex · OpenClaw
{
  "mcpServers": {
    "pylot": {
      "url": "https://www.getpylot.com/api/mcp",
      "headers": { "Authorization": "Bearer pyl_live_…" }
    }
  }
}
ToolDoesREST equivalent
get_accountWho am IGET /api/v1/me
get_creditsCredit balanceGET /api/v1/credits
list_channelsList connected channelsGET /api/v1/channels
list_postsList postsGET /api/v1/posts
create_postCreate a draft postPOST /api/v1/posts
get_postGet a postGET /api/v1/posts/{id}
delete_postDelete a postDELETE /api/v1/posts/{id}
schedule_postSchedule a postPOST /api/v1/posts/{id}/schedule
publish_postPublish nowPOST /api/v1/posts/{id}/publish
upload_mediaUpload mediaPOST /api/v1/media
generate_captionsGenerate captionsPOST /api/v1/generate/captions
generate_imageGenerate an imagePOST /api/v1/generate/images

The server's instructions tell the model that create_post is always a draft and that publish_post needs confirm: true; publish_post and delete_post are annotated destructiveHint so clients can require approval.

03

Concepts

Channels

A connected social account. Meta channels (a Facebook Page with a linked Instagram account) expose targets: ["facebook","instagram"] and expand to both unless you pass subPlatform. Publish results are recorded per platform, so keep one channel per platform for now.

Targets

Where a post goes. Each target is { channelId, subPlatform?, caption?, postFormat?, mediaUrls? }. A target without its own caption uses the post caption.

Post lifecycle

draft → approved (scheduled time set) → scheduled (queued) → posted | failed | partial. create_post makes drafts; schedule_post moves to approved; publish_post queues immediately.

Credits

Image generation costs 1 credit and is refunded if generation fails. Captions are free. GET /credits shows the balance; 402 insufficient_credits when short.

Media

mediaUrls must be stable, public URLs. Upload with POST /media (URL or multipart) or use the URL returned by generate_image. Post-level mediaUrls apply to every target unless a target overrides.

Timezones

scheduledAt with an offset (…-06:00 / Z) is absolute. Without one it is read in `timezone`, else the business timezone. Must be ≥ 1 minute ahead.

04

Endpoints

Base URL https://www.getpylot.com/api/v1. All bodies and responses are JSON unless noted. Machine-readable spec: openapi.json .

Account
GET/api/v1/health

Health check

Unauthenticated liveness probe for uptime monitors: 200 with `{ ok: true, db: "up" }` when the database answers, 503 otherwise.

Example response
{
  "ok": true,
  "db": "up",
  "latencyMs": 12,
  "version": "v1",
  "time": "2026-10-25T18:00:00.000Z"
}
GET/api/v1/memcp · get_account

Who am I

The business, plan, credit balance and limits behind the key. Call this first to confirm the key is scoped to the workspace you expect.

Example response
{
  "business": {
    "id": "biz_…",
    "name": "Fern & Fig Bakery",
    "niche": "Bakery",
    "timezone": "America/Chicago"
  },
  "plan": {
    "name": "Pro",
    "status": "active"
  },
  "credits": {
    "balance": 42,
    "monthlyAllocation": 50,
    "resetAt": "2026-11-01T00:00:00.000Z"
  },
  "limits": {
    "canCreateVideos": true,
    "captionIntelligence": true,
    "maxDrafts": -1
  },
  "apiKey": {
    "id": "key_…",
    "name": "OpenClaw",
    "prefix": "pyl_live_a1b2c3d"
  }
}
GET/api/v1/creditsmcp · get_credits

Credit balance

Current balance, monthly allocation and the credit cost of each metered operation.

Example response
{
  "balance": 42,
  "monthlyAllocation": 50,
  "resetAt": "2026-11-01T00:00:00.000Z",
  "costs": {
    "image": 1
  }
}
Channels
GET/api/v1/channelsmcp · list_channels

List connected channels

Every social account connected to the workspace. Use `id` as `channelId` when creating posts. Meta channels expose `targets` (facebook and/or instagram).

Example response
{
  "channels": [
    {
      "id": "ch_meta_1",
      "platform": "meta",
      "platformType": "page",
      "displayName": "Fern & Fig",
      "avatarUrl": null,
      "isActive": true,
      "targets": [
        "facebook",
        "instagram"
      ],
      "capabilities": {
        "supportsImages": true,
        "supportsVideos": true,
        "supportsCarousels": true,
        "supportsStories": true,
        "maxCaptionLength": 2200
      },
      "connectedAt": "2026-09-01T00:00:00.000Z"
    },
    {
      "id": "ch_x_1",
      "platform": "x",
      "platformType": "personal",
      "displayName": "@fernandfig",
      "avatarUrl": null,
      "isActive": true,
      "capabilities": {
        "supportsImages": true,
        "supportsVideos": true,
        "supportsCarousels": false,
        "supportsStories": false,
        "maxCaptionLength": 280
      },
      "connectedAt": "2026-09-01T00:00:00.000Z"
    }
  ]
}
Posts
GET/api/v1/postsmcp · list_posts

List posts

Newest first. Filter with a comma-separated `status` list.

Parameters
  • status query — Comma-separated: draft, approved, scheduled, posted, failed, partial
  • limit query — 1–100, default 25
Example response
{
  "posts": [
    {
      "id": "f3b2…",
      "status": "draft",
      "mediaUrls": [
        "https://blob.vercel-storage.com/…/hero.png"
      ],
      "scheduledAt": null,
      "timezone": null,
      "postedAt": null,
      "targets": [
        {
          "channelId": "ch_meta_1",
          "platform": "meta",
          "subPlatform": "instagram",
          "caption": "…",
          "postFormat": null,
          "mediaUrls": null,
          "publishedAt": null,
          "publishedId": null,
          "error": null
        }
      ],
      "createdAt": "2026-10-25T18:12:00.000Z",
      "updatedAt": "2026-10-25T18:12:00.000Z"
    }
  ]
}
POST/api/v1/postsmcp · create_post

Create a draft post

Creates a **draft** — nothing is published. Provide `targets` (channel ids) or omit to target every active channel. Pass `scheduledAt` to create and schedule in one call. Meta channels expand to Facebook + Instagram unless you pass `subPlatform`.

Body
caption
string
Default caption for every target
mediaUrls
string[]
Public image/video URLs. Upload first with POST /media (upload_media) to get a stable URL.
targets
object[]
Channels to post to. Omit to target every active channel.
channelIdrequired
string
A channel id from list_channels / GET /channels
subPlatform
"facebook" | "instagram"
Meta channels only: "facebook" or "instagram". Omit to post to both.
caption
string
Overrides the post-level caption for this target
postFormat
"feed" | "reel" | "story"
Meta only: feed (default), reel or story
mediaUrls
string[]
Overrides the post-level media for this target
tiktokSettings
object
privacyLevel
"PUBLIC_TO_EVERYONE" | "MUTUAL_FOLLOW_FRIENDS" | "FOLLOWER_OF_CREATOR" | "SELF_ONLY"
disableComment
boolean
disableDuet
boolean
disableStitch
boolean
commercialContentEnabled
boolean
brandOrganic
boolean
brandedContent
boolean
musicUsageConfirmed
boolean
scheduledAt
string
ISO-8601 timestamp (with offset, or naive + timezone). When set, the post is created and scheduled in one call.
timezone
string
IANA timezone used to interpret scheduledAt when it has no offset. Defaults to the business timezone.
Example request
curl -X POST https://www.getpylot.com/api/v1/posts \
  -H "Authorization: Bearer pyl_live_…" \
  -H "Content-Type: application/json" \
  -d '{"caption":"Sourdough drops at 7am tomorrow. First 20 loaves get a free cold brew ☕","mediaUrls":["https://blob.vercel-storage.com/…/sourdough.png"],"targets":[{"channelId":"ch_meta_1","subPlatform":"instagram"},{"channelId":"ch_x_1","caption":"Sourdough drops 7am. First 20 loaves get a free cold brew."}]}'
Example response
{
  "post": {
    "id": "f3b2…",
    "status": "draft",
    "mediaUrls": [
      "https://blob.vercel-storage.com/…/hero.png"
    ],
    "scheduledAt": null,
    "timezone": null,
    "postedAt": null,
    "targets": [
      {
        "channelId": "ch_meta_1",
        "platform": "meta",
        "subPlatform": "instagram",
        "caption": "…",
        "postFormat": null,
        "mediaUrls": null,
        "publishedAt": null,
        "publishedId": null,
        "error": null
      }
    ],
    "createdAt": "2026-10-25T18:12:00.000Z",
    "updatedAt": "2026-10-25T18:12:00.000Z"
  }
}
GET/api/v1/posts/{id}mcp · get_post

Get a post

A post with its per-target captions, schedule and publish results (`publishedId`, `error`).

Parameters
  • id path — Post id
Example response
{
  "post": {
    "id": "f3b2…",
    "status": "draft",
    "mediaUrls": [
      "https://blob.vercel-storage.com/…/hero.png"
    ],
    "scheduledAt": null,
    "timezone": null,
    "postedAt": null,
    "targets": [
      {
        "channelId": "ch_meta_1",
        "platform": "meta",
        "subPlatform": "instagram",
        "caption": "…",
        "postFormat": null,
        "mediaUrls": null,
        "publishedAt": null,
        "publishedId": null,
        "error": null
      }
    ],
    "createdAt": "2026-10-25T18:12:00.000Z",
    "updatedAt": "2026-10-25T18:12:00.000Z"
  }
}
DELETE/api/v1/posts/{id}mcp · delete_post

Delete a post

Drafts and scheduled posts only; published posts return `invalid_state`.

Parameters
  • id path — Post id
Example response
{
  "id": "f3b2…",
  "deleted": true
}
POST/api/v1/posts/{id}/schedulemcp · schedule_post

Schedule a post

Queue the post for a future time (≥ 1 minute ahead). Timestamps with an offset are absolute; naive ones are read in `timezone` or the business timezone. Pylot publishes it on schedule.

Parameters
  • id path — Post id
Body
scheduledAtrequired
string
ISO-8601 timestamp (e.g. 2026-11-02T09:00:00-05:00). Must be at least 1 minute in the future.
timezone
string
IANA timezone used when scheduledAt has no offset. Defaults to the business timezone.
Example request
curl -X POST https://www.getpylot.com/api/v1/posts/POST_ID/schedule \
  -H "Authorization: Bearer pyl_live_…" \
  -H "Content-Type: application/json" \
  -d '{"scheduledAt":"2026-11-02T09:00:00-06:00"}'
Example response
{
  "post": {
    "id": "f3b2…",
    "status": "approved",
    "mediaUrls": [
      "https://blob.vercel-storage.com/…/hero.png"
    ],
    "scheduledAt": "2026-11-02T15:00:00.000Z",
    "timezone": "America/Chicago",
    "postedAt": null,
    "targets": [
      {
        "channelId": "ch_meta_1",
        "platform": "meta",
        "subPlatform": "instagram",
        "caption": "…",
        "postFormat": null,
        "mediaUrls": null,
        "publishedAt": null,
        "publishedId": null,
        "error": null
      }
    ],
    "createdAt": "2026-10-25T18:12:00.000Z",
    "updatedAt": "2026-10-25T18:12:00.000Z"
  }
}
POST/api/v1/posts/{id}/publishmcp · publish_post

Publish now

**Irreversible.** Publishes to every enabled target immediately via Pylot's publishing pipeline. The body must be exactly `{ "confirm": true }`. Returns 202; poll `GET /posts/{id}` for per-target results.

Parameters
  • id path — Post id
Body
confirmrequired
boolean
Must be exactly true. Publishing is irreversible and goes live on the connected accounts.
Example request
curl -X POST https://www.getpylot.com/api/v1/posts/POST_ID/publish \
  -H "Authorization: Bearer pyl_live_…" \
  -H "Content-Type: application/json" \
  -d '{"confirm":true}'
Example response
{
  "post": {
    "id": "f3b2…",
    "status": "scheduled",
    "mediaUrls": [
      "https://blob.vercel-storage.com/…/hero.png"
    ],
    "scheduledAt": null,
    "timezone": null,
    "postedAt": null,
    "targets": [
      {
        "channelId": "ch_meta_1",
        "platform": "meta",
        "subPlatform": "instagram",
        "caption": "…",
        "postFormat": null,
        "mediaUrls": null,
        "publishedAt": null,
        "publishedId": null,
        "error": null
      }
    ],
    "createdAt": "2026-10-25T18:12:00.000Z",
    "updatedAt": "2026-10-25T18:12:00.000Z"
  },
  "queued": true
}
Media
POST/api/v1/mediamcp · upload_media

Upload media

Copy a file into Pylot storage. Send JSON `{ "url" }` for a public URL (images ≤ 5MB, videos ≤ 500MB; private/internal hosts are rejected), or `multipart/form-data` with a `file` field for small files (request bodies are capped at ~4.5MB). Use the returned `url` in `mediaUrls`.

Body
urlrequired
string
Publicly reachable image or video URL to copy into Pylot storage
filename
string
Example request
curl -X POST https://www.getpylot.com/api/v1/media \
  -H "Authorization: Bearer pyl_live_…" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/photos/sourdough.jpg"}'
Example response
{
  "asset": {
    "id": "a1…",
    "url": "https://blob.vercel-storage.com/…/sourdough.jpg",
    "filename": "upload-….jpg",
    "contentType": "image/jpeg",
    "sizeBytes": 184202,
    "mediaType": "image",
    "width": 1080,
    "height": 1350
  }
}
Generate
POST/api/v1/generate/captionsmcp · generate_captions

Generate captions

Three on-brand variations per platform, written in the workspace's brand voice (and tuned by learned patterns on Pro+). Free.

Cost · Free
Body
promptrequired
string
What the post is about, who it is for, and any must-include details
platforms
"facebook" | "instagram" | "linkedin" | "x"[]
Platforms to write for. Each gets 3 variations tuned to its length and style. Default: ["instagram"]
tone
"professional" | "casual" | "funny" | "inspirational" | "promotional"
Default: "casual"
includeHashtags
boolean
Default: true
Example request
curl -X POST https://www.getpylot.com/api/v1/generate/captions \
  -H "Authorization: Bearer pyl_live_…" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"Launch of our new sourdough loaf, bakery in Austin, casual and warm","platforms":["instagram","x"],"tone":"casual"}'
Example response
{
  "captions": [
    {
      "id": 1,
      "text": "…",
      "hashtags": [
        "austinbakery",
        "sourdough"
      ],
      "platform": "instagram",
      "tone": "casual"
    }
  ],
  "creditsDeducted": 0
}
POST/api/v1/generate/imagesmcp · generate_image

Generate an image

A brand-aware marketing image: brand colours, logo and visual style are applied automatically. Saved to the Assets library and returned as a URL you can pass straight to `mediaUrls`.

Cost · 1 credit (refunded on failure)
Body
promptrequired
string
Describe the image. Brand colours, logo and style are added automatically.
aspectRatio
"1:1" | "3:4" | "4:3" | "4:5" | "9:16" | "16:9"
Default: "1:1"
productId
string
Optional product id to feature (uses its name, description and photos)
includeLogo
boolean
Default: true
includeWebsite
boolean
Default: false
Example request
curl -X POST https://www.getpylot.com/api/v1/generate/images \
  -H "Authorization: Bearer pyl_live_…" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"Fresh sourdough loaf on a wooden board, morning light","aspectRatio":"4:5"}'
Example response
{
  "imageUrl": "https://…/image.webp",
  "width": 1080,
  "height": 1350,
  "assetId": "a2…",
  "creditsDeducted": 1,
  "creditsRemaining": 41
}
05

Errors

Every error is { "error": { "code", "message", "details?" } }. Branch on code. Every response carries an x-request-id header — include it when reporting issues. For uptime checks, poll GET /api/v1/health (no auth).

unauthorized401Missing, malformed, invalid or revoked API key
forbidden403The account behind the key is no longer active
payment_required402Subscription inactive, or payment required before publishing
insufficient_credits402Not enough credits for a metered operation
plan_limit_reached403Free-plan post or draft limit hit
validation_error400Body or query failed validation; `details` has the field errors
not_found404Post or channel does not exist in this workspace
invalid_state409Operation not allowed in the resource's current state
upstream_error502A provider (AI, storage, queue) failed
internal_error500Unexpected error; retry with the same request id
Not in v1 yet

Video generation (async jobs), webhooks for publish results, per-key scopes and rate-limit headers are on the way. Need one of these first? Tell us