PostWing

You are reading the legacy v1 API docs.

v1 keeps working for existing integrations. New integrations should use API v2, and the migration guide maps every v1 field to its v2 name.

Agent API · v1 (legacy)

PostWing API v1 docs

Schedule, edit, and manage social posts from agents and apps. Same capabilities as the dashboard: accounts, workspaces, media, platform options, and a required preview → confirm write flow.

What the API offers

Everything an AI agent or backend needs to run PostWing without the dashboard UI.

Schedule posts

One call puts a post on the calendar, same as the dashboard. Prefer to review first? Use the two-step preview → confirm flow.

Edit and cancel

PATCH a scheduled post with only the fields that change, or DELETE it. Batch edits cover up to 20 posts at once.

Connect accounts

Start OAuth for Instagram, TikTok, YouTube, LinkedIn, X, Facebook, Pinterest, or Threads, then sync into the workspace.

Workspaces

List and create workspaces (Scale), then pass workspace_id on any call, or switch the active workspace.

Upload media

Get a direct upload URL, PUT the file to storage, then pass the returned media_url when you schedule.

MCP for agents

Same capabilities over Model Context Protocol for Claude, ChatGPT, Cursor, and other MCP clients.

Base URLs

  • REST: https://postwing.io/api/v1
  • OpenAPI: https://postwing.io/api/v1/openapi.json
  • MCP: https://postwing.io/api/mcp

Quickstart

Four steps from zero to a scheduled post.

  1. 1Create an API key in Dashboard → AI & API (or connect via OAuth).
  2. 2GET /accounts to see connected accounts and your timezone.
  3. 3Upload media with POST /media/upload-url, then PUT the file to upload_url (optional for text posts).
  4. 4POST /posts with caption, scheduled_at, account_ids or targets, and media_items. Done.

Authentication

Send a Bearer token on every protected request. Prefer OAuth for third-party agents; API keys for scripts and servers.

MethodHow
API keyCreate a key in Dashboard → AI & API. Use Authorization: Bearer pw_live_…
OAuth (PKCE)Resource https://postwing.io/api/v1. Authorize at /oauth/authorize, tokens at /oauth/token. Dynamic client registration is supported for trusted hosts including muse.ai and cursor.com.
Header
Authorization: Bearer pw_live_…
Content-Type: application/json

Scopes

Request only what your client needs.

ScopeAccess
accounts:readList connected accounts and Pinterest boards
accounts:writeConnect, sync, and disconnect social accounts
posts:readList posts, get a post, schedule summary
posts:writeUpload media, preview, confirm, edit, and cancel
workspaces:readList workspaces
workspaces:writeCreate workspaces and switch the active one

Scheduling & retries

Schedule in one call, or preview first when a person approves each post.

  1. One call: POST /posts schedules a post and PATCH /posts/{id} edits one. To schedule several, call POST /posts once per post.
  2. Two steps (human approval): POST /posts/preview, show the preview, then POST /posts/confirm with the confirmation_token. The same applies to batch edits via /posts/edit/preview and /posts/edit/confirm.
  • If a person should approve each post, use the two-step preview → confirm endpoints and wait for their approval before confirming.
  • Confirmation tokens expire in 30 minutes, are bound to that user, and confirming the same token twice returns the first result.
  • Cancel or disconnect only when the user asked for it.
  • Only operate on accounts and posts returned by the API for the authenticated user.
  • Use the user’s PostWing timezone for relative times like “tomorrow at 9am”.

Errors & rate limits

Errors use a simple JSON shape: { "error": "message" }.

StatusMeaning
401Missing or invalid Bearer token
403Missing scope, inactive subscription, plan limit, or Scale-only feature
400Validation or business rule error (message in error)
404Account, post, workspace, or board not found for this user
409The same confirmation_token is already being confirmed; check your posts before retrying
429Rate limit exceeded (requests ~240/min, writes ~40/min, media upload ~80/min per credential)
500Unexpected server error
  • All requests: about 240/min per credential
  • Writes: about 40 requests/min per credential
  • Media upload: about 80 requests/min per user

Media upload

Upload files before you schedule. Get an upload URL, PUT the file directly to storage, then pass media_url into preview.

  1. POST /media/upload-url with filename, content_type, and size
  2. PUT the raw file bytes to upload_url (same Content-Type)
  3. Pass media_url into POST /posts/preview as media_items[].url

You can also pass a durable public HTTPS URL straight into preview. POST /media (URL import) remains available but is optional.

media_items fields

FieldTypeRequiredDescription
urlstring (uri)YesPublic HTTPS URL of the image or video.
type"image" | "video"YesMedia kind. Inferred from the URL extension if omitted.
thumbnail_urlstring (uri)NoVideo only. Custom cover image URL (JPG/PNG/WEBP). Takes precedence over thumbnail_timestamp_ms. Same capability as Custom thumbnail in the dashboard.
thumbnail_timestamp_msinteger ≥ 0NoVideo only. Milliseconds into the video to use as the cover frame when thumbnail_url is omitted. Default: 1000 (first second). Ignored when thumbnail_url is set.

Video cover rules

  • If thumbnail_url is set, that image is the cover (Instagram Reel cover and other platforms that accept a custom thumbnail).
  • Otherwise PostWing uses thumbnail_timestamp_ms (default 1000) to pick a frame from the video.
  • Cover images: JPG, PNG, WEBP, max 10MB. Must be a public HTTPS URL.
  • thumbnail_url always wins when present.

Formats & size

KindFormatsLimits
ImagesJPG, PNG, WEBP10MB each · Up to 35 images per post (platform limits still apply when publishing)
VideosMP4, MOV, WEBM450MB (URL import via Agent API may be lower; see upload_media errors) · One video per post
Video coversJPG, PNG, WEBP10MB

Per-platform image caps

Extra images beyond a platform’s cap are omitted for that platform when publishing.

PlatformMax images
Instagram10
Facebook20
LinkedIn20
X4
Threads4
TikTok30
Pinterest1
YouTubevideo only

Captions

One global caption, optional per-account overrides. Same model as the dashboard.

FieldBehavior
captionGlobal caption for every target. Required on schedule preview. Pass "" or " " for no caption (both mean empty).
targets[].captionWhen set (including "" or " "), overrides the global caption for that account only. Omit the field to inherit the global caption.
targets[].platform_options.captionLegacy/alternate place for a per-account caption. Prefer targets[].caption.

Platform options

Pass these on targets[].platform_options when scheduling or editing. Same fields as the dashboard compose form.

X (Twitter)

x

FieldTypeDescription
quote_tweet_urlstringTweet URL or status ID to quote. Creates a native X quote post (not a plain link in the caption). Example: https://x.com/user/status/123
reply_settingsstring | nullWho can reply: null (everyone), following, mentionedUsers, subscribers, verified

Instagram

instagram

FieldTypeDescription
placementstringSet automatically by PostWing: reels for video, timeline for image/text. You usually do not need to send this.
share_to_feedbooleanVideo/Reels only. If true, also share the Reel to the main feed (default true). Same as “Share Reels to feed” in the dashboard.
trial_reel_typestring | nullVideo only. null (off), manual, or performance. Trial reels require 1000+ followers; max 5 per day.

Facebook

facebook

FieldTypeDescription
placementstringtimeline or reels. Video defaults to reels; images/text use timeline.

YouTube

youtube

FieldTypeDescription
titlestringRequired for YouTube. Shorts/long-form title.
made_for_kidsbooleanWhether the video is made for kids (default false)

TikTok

tiktok

FieldTypeDescription
titlestringImage posts only (not used for video)
privacy_statusstringpublic or private (default public)
allow_commentbooleanAllow comments (default true)
allow_duetbooleanAllow duets (default true)
allow_stitchbooleanAllow stitch (default true)
is_ai_generatedbooleanMark as AI-generated content (default false)
is_draftbooleanSave as draft in the TikTok app (default false)

Pinterest

pinterest

FieldTypeDescription
board_idsstring[]Required. Board IDs from GET /accounts/pinterest-boards or list_pinterest_boards
titlestringPin title
linkstringDestination URL for the pin

LinkedIn

linkedin

FieldTypeDescription
(none)n/aCaption only; no extra platform options

Threads

threads

FieldTypeDescription
(none)n/aCaption only; no extra platform options

Discovery

Public metadata. No auth required.

GET/api/v1public

API discovery

Returns base URL, OpenAPI link, docs links, auth hints, and available scopes.

Hit this first when wiring a new client. It tells you where OpenAPI lives, which scopes exist, and that writes use preview → confirm.

Request
curl -s "https://postwing.io/api/v1"
Response (200)
{
  "name": "PostWing Agent API",
  "version": "1.0.0",
  "base_url": "https://postwing.io/api/v1",
  "openapi_url": "https://postwing.io/api/v1/openapi.json",
  "documentation": "https://postwing.io/docs",
  "security": {
    "scopes": [
      "accounts:read",
      "accounts:write",
      "posts:read",
      "posts:write",
      "workspaces:read",
      "workspaces:write"
    ],
    "write_flow": "preview → confirmation_token → confirm (token expires in 5 minutes)",
    "isolation": "All data is scoped to the authenticated user and active workspace"
  }
}
GET/api/v1/openapi.jsonpublic

OpenAPI document

Machine-readable OpenAPI 3.1 schema for the full Agent API.

Use this URL in Muse custom connectors, code generators, or any OpenAPI-aware tool. Prefer OAuth when the host asks how to authenticate.

Request
curl -s "https://postwing.io/api/v1/openapi.json"
Response (200)
Full OpenAPI 3.1 object (paths, schemas, security schemes). Too large to inline here.

Accounts

Connect, list, sync, and disconnect social accounts in the active workspace.

GET/api/v1/accountsaccounts:read

List connected accounts

Returns every social account in the active workspace plus the user timezone.

Start here before scheduling. Use account_id values in targets[], and use timezone when interpreting relative times like “tomorrow at 9am”.

Query parameters

NameTypeRequiredDescription
workspace_idstring (uuid)NoList accounts in this workspace instead of the active one
  • status is connected or reauth_required.
  • Without workspace_id, only accounts in the active workspace are returned.
Request
curl -s "https://postwing.io/api/v1/accounts" \
  -H "Authorization: Bearer pw_live_…"
Response (200)
{
  "timezone": "America/New_York",
  "accounts": [
    {
      "account_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "platform": "instagram",
      "handle": "@brand",
      "display_name": "Brand",
      "status": "connected"
    },
    {
      "account_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "platform": "linkedin",
      "handle": "company-page",
      "display_name": "Company",
      "status": "connected"
    }
  ]
}
POST/api/v1/accountsaccounts:write

Start account connect

Starts OAuth for a platform and returns a browser URL the user must open.

This does not finish the connection by itself. After the user authorizes in the browser, call POST /accounts/sync to import the account into PostWing.

Request body

NameTypeRequiredDescription
platformstringYesOne of: facebook, instagram, linkedin, pinterest, threads, tiktok, x, youtube
  • Account limits follow the user’s plan.
  • Show the URL to the user; do not invent a completed connection.
Request
curl -s -X POST "https://postwing.io/api/v1/accounts" \
  -H "Authorization: Bearer pw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "platform": "linkedin"
}'
Response (200)
{
  "url": "https://…oauth…",
  "platform": "linkedin",
  "next_step": "Open this URL in the browser so the user can authorize the social account. After they finish, call POST /accounts/sync."
}
POST/api/v1/accounts/syncaccounts:write

Sync accounts after OAuth

Imports or refreshes accounts after the user finishes the connect browser flow.

Call this right after the user returns from the OAuth URL. No body is required. Returns the accounts now available in the active workspace.

Request
curl -s -X POST "https://postwing.io/api/v1/accounts/sync" \
  -H "Authorization: Bearer pw_live_…"
Response (200)
{
  "synced_count": 2,
  "accounts": [
    {
      "account_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "platform": "linkedin",
      "handle": "company-page",
      "display_name": "Company",
      "status": "connected"
    }
  ]
}
DELETE/api/v1/accountsaccounts:write

Disconnect an account

Removes a social account from the workspace after explicit confirmation.

Destructive. Only call after the user agrees. confirm must be true or the API rejects the request.

Request body

NameTypeRequiredDescription
account_idstring (uuid)YesPostWing social account UUID from GET /accounts
confirmbooleanYesMust be true
Request
curl -s -X DELETE "https://postwing.io/api/v1/accounts" \
  -H "Authorization: Bearer pw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "account_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "confirm": true
}'
Response (200)
{
  "disconnected": true,
  "account_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "platform": "instagram",
  "handle": "@brand"
}
GET/api/v1/accounts/pinterest-boardsaccounts:read

List Pinterest boards

Lists boards for a connected Pinterest account (needed before scheduling Pins).

Pinterest posts require board_ids in targets[].platform_options. Fetch boards with this endpoint first, then pass the IDs into preview.

Query parameters

NameTypeRequiredDescription
account_idstring (uuid)YesPinterest account UUID from GET /accounts
Request
curl -s "https://postwing.io/api/v1/accounts/pinterest-boards?account_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
  -H "Authorization: Bearer pw_live_…"
Response (200)
{
  "account_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "boards": [
    {
      "id": "1234567890",
      "name": "Marketing"
    }
  ]
}

Workspaces

Everything you schedule or connect is scoped to the active workspace.

GET/api/v1/workspacesworkspaces:read

List workspaces

Returns workspaces the user belongs to and which one is active.

Use this to show workspace context, or before switching with POST /workspaces/active.

Request
curl -s "https://postwing.io/api/v1/workspaces" \
  -H "Authorization: Bearer pw_live_…"
Response (200)
{
  "active_workspace_id": "ws-uuid-1",
  "workspaces": [
    {
      "workspace_id": "ws-uuid-1",
      "name": "Default",
      "created_at": "2025-01-15T12:00:00.000Z",
      "role": "owner",
      "is_active": true
    }
  ]
}
POST/api/v1/workspacesworkspaces:write

Create a workspace

Creates a workspace (Scale plan).

Useful for agencies and multi-brand setups. Returns 403 if the plan does not include workspaces.

Request body

NameTypeRequiredDescription
namestringYesNon-empty workspace name
Request
curl -s -X POST "https://postwing.io/api/v1/workspaces" \
  -H "Authorization: Bearer pw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Client A"
}'
Response (200)
{
  "workspace": {
    "workspace_id": "ws-uuid-2",
    "name": "Client A",
    "created_at": "2026-10-04T14:00:00.000Z"
  }
}
POST/api/v1/workspaces/activeworkspaces:write

Switch active workspace

Sets which workspace subsequent account and post calls use.

After switching, GET /accounts and GET /posts reflect the new workspace until you switch again.

Request body

NameTypeRequiredDescription
workspace_idstring (uuid)YesWorkspace UUID from GET /workspaces (workspaceId alias accepted)
Request
curl -s -X POST "https://postwing.io/api/v1/workspaces/active" \
  -H "Authorization: Bearer pw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "workspace_id": "ws-uuid-2"
}'
Response (200)
{
  "active_workspace": {
    "workspace_id": "ws-uuid-2",
    "name": "Client A",
    "created_at": "2026-10-04T14:00:00.000Z"
  },
  "next_step": "Subsequent account and post calls use this workspace until switched again."
}

Media

Upload files first, then pass the returned media_url into schedule preview.

POST/api/v1/media/upload-urlposts:write

Get a direct upload URL

Returns upload_url and media_url. PUT your file to upload_url, then use media_url in preview.

Preferred path for images and videos from your app or script. Same flow as the dashboard: prepare an upload URL, send the file bytes with PUT, then schedule with the returned media_url.

Request body

NameTypeRequiredDescription
filenamestringYesOriginal filename (e.g. launch.jpg)
content_typestringYesMIME type. contentType alias accepted. e.g. image/jpeg, video/mp4
sizenumberYesFile size in bytes
  • After this call, PUT the raw file to upload_url (not JSON). Include Content-Type matching content_type.
  • Then pass { url: media_url, type } in media_items on POST /posts/preview.
  • Images: JPG, PNG, WEBP up to 10MB. Videos: MP4, MOV, WEBM up to 450MB.
  • Rate limited (~20 media requests/min per user).
Request
curl -s -X POST "https://postwing.io/api/v1/media/upload-url" \
  -H "Authorization: Bearer pw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "filename": "launch.jpg",
  "content_type": "image/jpeg",
  "size": 245760
}'
Response (200)
{
  "upload_url": "https://upload…/…",
  "media_url": "https://cdn…/launch.jpg",
  "type": "image",
  "next_step": "PUT the file bytes to upload_url with the same Content-Type. Then pass media_url into POST /posts/preview as media_items[].url."
}
POST/api/v1/mediaposts:write

Import media from URLs (optional)

Optional. Downloads public HTTPS media and returns media_items. Prefer /media/upload-url for local files.

Kept for existing clients. Prefer POST /media/upload-url for files on disk, or pass a durable public URL directly into POST /posts/preview as media_items / media_urls. URL import is capped at 100MB for videos.

Request body

NameTypeRequiredDescription
urlsstring[] (HTTPS URIs)Yes1–10 public URLs. Only one video URL per video post.
  • Optional. Most clients should use /media/upload-url instead.
  • Private or signed URLs that expire often fail.
Request
curl -s -X POST "https://postwing.io/api/v1/media" \
  -H "Authorization: Bearer pw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "urls": [
    "https://cdn.example.com/launch.jpg"
  ]
}'
Response (200)
{
  "media_items": [
    {
      "url": "https://cdn.postwing…/launch.jpg",
      "type": "image"
    }
  ],
  "upload_page": "https://postwing.io/dashboard/ai/upload",
  "next_step": "Pass media_items into POST /posts/preview along with caption, schedule time, and targets."
}

Posts

List, inspect, schedule, edit, and cancel posts. Scheduling and editing are one call each; the older preview → confirm endpoints still work.

GET/api/v1/postsposts:read

List scheduled posts

Paginated list of posts in the active workspace.

By default only upcoming posts (scheduled_at ≥ now) are returned. This is one page, not the full calendar. Follow has_more with a higher offset, or use GET /schedule/summary for aggregate counts.

Query parameters

NameTypeRequiredDescription
limitintegerNoPage size. Default 20 (full) or 100 (summary). Max 50 full / 200 summary.
offsetintegerNoPagination offset (default 0)
fields"full" | "summary"Nosummary returns compact rows (no captions). full includes richer post rows.
statusstringNoscheduled | publishing | published | partially_failed | failed | paused
fromISO datetimeNoscheduled_at ≥ from
untilISO datetimeNoscheduled_at ≤ until
include_pastbooleanNoIf true (and from omitted), include past posts
workspace_idstring (uuid)NoList posts in this workspace instead of the active one
Request
curl -s "https://postwing.io/api/v1/posts?limit=20&offset=0&fields=full" \
  -H "Authorization: Bearer pw_live_…"
Response (200)
{
  "total": 42,
  "returned": 20,
  "offset": 0,
  "has_more": true,
  "pagination_note": "Showing posts 1-20 of 42. This is NOT the full list. Call again with offset=20, or use fields=summary.",
  "posts": [
    {
      "id": "post-uuid",
      "caption_common": "Hello world",
      "scheduled_at": "2026-10-05T15:00:00.000Z",
      "status": "scheduled",
      "created_at": "2026-10-04T10:00:00.000Z",
      "post_targets": [
        {
          "platform": "linkedin",
          "account_handle": "company",
          "status": "scheduled"
        }
      ]
    }
  ]
}
GET/api/v1/posts/{id}posts:read

Get one post

Full detail for a single post: media, per-account captions, and platform_options.

Use this when you need exact captions or platform settings before editing. Includes view_url to open the post in the dashboard.

Path parameters

NameTypeRequiredDescription
idstring (uuid)YesPost ID from list or confirm responses
Request
curl -s "https://postwing.io/api/v1/posts/post-uuid" \
  -H "Authorization: Bearer pw_live_…"
Response (200)
{
  "timezone": "America/New_York",
  "post": {
    "id": "post-uuid",
    "caption_common": "Launch day",
    "scheduled_at": "2026-10-05T15:00:00.000Z",
    "scheduled_at_local": "10/5/2026, 11:00:00 AM",
    "status": "scheduled",
    "post_type": "image",
    "media": [
      {
        "url": "https://cdn.example.com/photo.jpg",
        "type": "image"
      }
    ],
    "targets": [
      {
        "account_id": "acct-uuid",
        "platform": "instagram",
        "handle": "@brand",
        "status": "scheduled",
        "caption": null,
        "platform_options": {
          "placement": "timeline"
        }
      }
    ],
    "view_url": "https://postwing.io/dashboard/post/post-uuid"
  }
}
POST/api/v1/postsposts:write

Schedule a post

Validates and schedules a post in a single call.

The post goes on the PostWing calendar exactly as if it were scheduled from the dashboard. To schedule several posts, call this once per post. If a person should approve each post first, use the two-step preview → confirm endpoints below.

Request body

NameTypeRequiredDescription
captionstringYesGlobal caption. Use "" or " " for no caption (both mean empty).
scheduled_atISO 8601 datetimeYesMust be in the future
targetsobject[]NoPreferred. Each item: { account_id, caption?, platform_options? }. Use instead of account_ids/platforms when you need per-account settings.
account_idsstring[] (uuid)NoPostWing account UUIDs from GET /accounts
platformsstring[]Noe.g. ["instagram","linkedin"] when one connected account per platform is enough
media_itemsMediaItem[]NoPreferred media. { url, type, thumbnail_url?, thumbnail_timestamp_ms? }
media_urlsstring[]NoShorthand public HTTPS URLs (imported/validated like media_items)
post_type"text" | "image" | "video"NoUsually inferred from media
workspace_idstring (uuid)NoWorkspace to schedule in. Defaults to the workspace the accounts belong to, else the active workspace.
  • Provide at least one of targets, account_ids, or platforms.
  • Same body as POST /posts/preview; same response as POST /posts/confirm.
  • Validation errors return 400 and nothing is scheduled.
Request
curl -s -X POST "https://postwing.io/api/v1/posts" \
  -H "Authorization: Bearer pw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "caption": "Product update",
  "scheduled_at": "2026-10-05T18:00:00.000Z",
  "account_ids": [
    "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  ],
  "media_items": [
    {
      "url": "https://cdn.example.com/photo.jpg",
      "type": "image"
    }
  ]
}'
Response (200)
{
  "scheduled": true,
  "post": {
    "id": "new-post-uuid",
    "caption_common": "Product update",
    "scheduled_at": "2026-10-05T18:00:00.000Z",
    "status": "scheduled",
    "targets": [
      {
        "platform": "instagram",
        "handle": "@brand"
      }
    ],
    "dashboard_url": "/dashboard/post/new-post-uuid"
  },
  "view_url": "https://postwing.io/dashboard/post/new-post-uuid"
}
PATCH/api/v1/posts/{id}posts:write

Edit a post

Change a scheduled post in one call. Send only the fields that change.

Everything you omit keeps its current value. Only posts with status scheduled can be edited.

Path parameters

NameTypeRequiredDescription
idstring (uuid)YesPost ID

Request body

NameTypeRequiredDescription
captionstringNoNew global caption. "" or " " clears it.
scheduled_atISO datetimeNoNew absolute time
shift_minutesintegerNoMove relative to the current time, e.g. 60 or -1440
targets / account_ids / platformsarrayNoReplace the accounts this post goes to
target_optionsobject[]NoUpdate captions/platform_options for accounts already on the post
media_items / media_urlsarrayNoReplace media
remove_mediabooleanNoClear all media when true
  • To change several posts at once, use the edit preview → confirm endpoints below.
Request
curl -s -X PATCH "https://postwing.io/api/v1/posts/post-uuid" \
  -H "Authorization: Bearer pw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "caption": "Updated copy",
  "shift_minutes": 30
}'
Response (200)
{
  "updated": true,
  "post": {
    "id": "post-uuid",
    "caption_common": "Updated copy",
    "scheduled_at": "2026-10-05T15:30:00.000Z",
    "status": "scheduled",
    "view_url": "https://postwing.io/dashboard/post/post-uuid"
  }
}
POST/api/v1/posts/previewposts:write

Preview a schedule (two-step)legacy · still supported

Validates a post and returns a preview plus a confirmation_token (30 minute TTL).

Two-step alternative to POST /posts for flows where a person approves each post before it is scheduled. Nothing is scheduled until you call confirm with the token. Still fully supported.

Request body

NameTypeRequiredDescription
captionstringYesGlobal caption. Use "" or " " for no caption (both mean empty).
scheduled_atISO 8601 datetimeYesMust be in the future
targetsobject[]NoPreferred. Each item: { account_id, caption?, platform_options? }. Use instead of account_ids/platforms when you need per-account settings.
account_idsstring[] (uuid)NoPostWing account UUIDs from GET /accounts
platformsstring[]Noe.g. ["instagram","linkedin"] when one connected account per platform is enough
media_itemsMediaItem[]NoPreferred media. { url, type, thumbnail_url?, thumbnail_timestamp_ms? }
media_urlsstring[]NoShorthand public HTTPS URLs (imported/validated like media_items)
post_type"text" | "image" | "video"NoUsually inferred from media
workspace_idstring (uuid)NoDefaults to the workspace the accounts belong to, else the active workspace
  • You must provide at least one of targets, account_ids, or platforms.
  • Never invent a confirmation_token. Always take it from this response.
  • Tokens expire in 30 minutes and are bound to the authenticated user.
Request
curl -s -X POST "https://postwing.io/api/v1/posts/preview" \
  -H "Authorization: Bearer pw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "caption": "Product update",
  "scheduled_at": "2026-10-05T18:00:00.000Z",
  "platforms": [
    "instagram",
    "linkedin"
  ],
  "media_items": [
    {
      "url": "https://cdn.example.com/photo.jpg",
      "type": "image"
    }
  ]
}'
Response (200)
{
  "preview": {
    "caption": "Product update",
    "scheduled_at": "2026-10-05T18:00:00.000Z",
    "scheduled_at_local": "10/5/2026, 2:00:00 PM",
    "timezone": "America/New_York",
    "post_type": "image",
    "targets": [
      {
        "account_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "platform": "instagram",
        "handle": "@brand",
        "caption": null,
        "platform_options": {}
      }
    ],
    "media_count": 1,
    "media": [
      {
        "url": "https://cdn.example.com/photo.jpg",
        "type": "image"
      }
    ]
  },
  "confirmation_token": "eyJ…signature",
  "expires_in_seconds": 1800,
  "next_step": "Show the preview to the user. After explicit approval, POST /posts/confirm with confirmation_token."
}
POST/api/v1/posts/confirmposts:write

Confirm a schedule (two-step)legacy · still supported

Creates the scheduled post using the token from preview.

Call only after the operator approves the preview. Confirming the same token again returns the original post instead of scheduling a duplicate.

Request body

NameTypeRequiredDescription
confirmation_tokenstringYesToken from POST /posts/preview
Request
curl -s -X POST "https://postwing.io/api/v1/posts/confirm" \
  -H "Authorization: Bearer pw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "confirmation_token": "eyJ…signature"
}'
Response (200)
{
  "scheduled": true,
  "post": {
    "id": "new-post-uuid",
    "caption_common": "Product update",
    "scheduled_at": "2026-10-05T18:00:00.000Z",
    "status": "scheduled",
    "targets": [
      {
        "platform": "instagram",
        "handle": "@brand"
      }
    ],
    "dashboard_url": "/dashboard/post/new-post-uuid"
  },
  "view_url": "https://postwing.io/dashboard/post/new-post-uuid"
}
POST/api/v1/posts/edit/previewposts:write

Preview edits

Dry-run changes on 1–20 scheduled posts; returns a diff and confirmation_token (30 minute TTL).

Only send fields you want to change. Everything else stays as-is. For batch time moves, use shift_minutes (absolute scheduled_at is not allowed across multiple posts).

Request body

NameTypeRequiredDescription
post_idsstring[] (uuid)Yes1–20 post IDs
captionstringNoNew global caption. "" or " " clears caption. Omit to keep current.
scheduled_atISO datetimeNoAbsolute new time (single post only)
shift_minutesintegerNoRelative shift for one or many posts
targetsobject[]NoReplace the full target list (captions / platform_options)
target_optionsobject[]NoPatch captions/platform_options for accounts already on the post without replacing targets
account_ids / platformsstring[]NoReplace targets by account IDs or platform names
media_items / media_urlsarrayNoReplace media
remove_mediabooleanNoClear all media when true
Request
curl -s -X POST "https://postwing.io/api/v1/posts/edit/preview" \
  -H "Authorization: Bearer pw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "post_ids": [
    "post-uuid-1"
  ],
  "caption": "Updated copy",
  "shift_minutes": 30
}'
Response (200)
{
  "preview": {
    "post_count": 1,
    "timezone": "America/New_York",
    "requested_changes": {
      "caption": "Updated copy",
      "shift_minutes": 30
    },
    "posts": [
      {
        "post_id": "post-uuid-1",
        "changes": [
          "caption",
          "scheduled_at"
        ],
        "current": {
          "caption": "Old copy",
          "scheduled_at": "2026-10-05T15:00:00.000Z"
        },
        "updated": {
          "caption": "Updated copy",
          "scheduled_at": "2026-10-05T15:30:00.000Z"
        }
      }
    ]
  },
  "confirmation_token": "eyJ…signature",
  "expires_in_seconds": 1800,
  "next_step": "Show current vs updated values. After approval, POST /posts/edit/confirm with confirmation_token."
}
POST/api/v1/posts/edit/confirmposts:write

Confirm edits

Applies the edit token from edit preview.

If a post changed since preview (fingerprint mismatch), that post is rejected. Batch responses may include a failed array for partial success. Confirming the same token again returns the original result.

Request body

NameTypeRequiredDescription
confirmation_tokenstringYesToken from POST /posts/edit/preview
Request
curl -s -X POST "https://postwing.io/api/v1/posts/edit/confirm" \
  -H "Authorization: Bearer pw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "confirmation_token": "eyJ…signature"
}'
Response (200)
{
  "updated": true,
  "updated_count": 1,
  "posts": [
    {
      "id": "post-uuid-1",
      "caption_common": "Updated copy",
      "scheduled_at": "2026-10-05T15:30:00.000Z",
      "status": "scheduled",
      "view_url": "https://postwing.io/dashboard/post/post-uuid-1"
    }
  ]
}
DELETE/api/v1/posts/{id}posts:write

Cancel a post

Cancels a scheduled or paused post.

Only scheduled or paused posts can be cancelled. No body is needed. The older { "confirm": true } body is still accepted; an explicit confirm: false is refused.

Path parameters

NameTypeRequiredDescription
idstring (uuid)YesPost ID to cancel
Request
curl -s -X DELETE "https://postwing.io/api/v1/posts/post-uuid" \
  -H "Authorization: Bearer pw_live_…"
Response (200)
{
  "cancelled": true,
  "post_id": "post-uuid"
}

Schedule insights

Aggregate view of what is already on the calendar.

GET/api/v1/schedule/summaryposts:read

Schedule summary

Counts and patterns across upcoming posts in the active workspace.

Use this instead of paging through every post when you only need totals by platform, account, status, or day. Scans up to 1000 upcoming posts.

Query parameters

NameTypeRequiredDescription
workspace_idstring (uuid)NoSummarize this workspace instead of the active one
  • total_posts counts posts. by_platform / by_account count publications (a post to 3 accounts counts 3 times there).
Request
curl -s "https://postwing.io/api/v1/schedule/summary" \
  -H "Authorization: Bearer pw_live_…"
Response (200)
{
  "timezone": "America/New_York",
  "total_posts": 15,
  "truncated": false,
  "range": {
    "first": "2026-10-05T09:00:00.000Z",
    "last": "2026-10-20T17:00:00.000Z"
  },
  "by_platform": {
    "instagram": 10,
    "linkedin": 8
  },
  "by_status": {
    "scheduled": 15
  },
  "by_account": [
    {
      "platform": "instagram",
      "handle": "@brand",
      "post_count": 6,
      "active_days": 3
    }
  ],
  "by_day": {
    "2026-10-05": {
      "total": 2,
      "platforms": {
        "instagram": 1,
        "linkedin": 1
      }
    }
  }
}

Workflow examples

End-to-end patterns you can copy.

Text post to Instagram + LinkedIn

One call

curl -s -X POST "https://postwing.io/api/v1/posts" \
  -H "Authorization: Bearer pw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "caption": "Hello from the Agent API",
  "scheduled_at": "2026-10-05T09:00:00Z",
  "platforms": [
    "instagram",
    "linkedin"
  ]
}'

Agencies: schedule into a specific client workspace

1. Find the workspace and its accounts

curl -s "https://postwing.io/api/v1/workspaces" \
  -H "Authorization: Bearer pw_live_…"

curl -s "https://postwing.io/api/v1/accounts?workspace_id=ws-uuid-2" \
  -H "Authorization: Bearer pw_live_…"

2. Schedule with workspace_id (no need to switch the active workspace)

curl -s -X POST "https://postwing.io/api/v1/posts" \
  -H "Authorization: Bearer pw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "workspace_id": "ws-uuid-2",
  "caption": "Client launch",
  "scheduled_at": "2026-10-07T15:00:00Z",
  "platforms": [
    "instagram"
  ]
}'

Upload a video, then schedule a Reel with cover

1. Get upload URL

curl -s -X POST "https://postwing.io/api/v1/media/upload-url" \
  -H "Authorization: Bearer pw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "filename": "reel.mp4",
  "content_type": "video/mp4",
  "size": 12582912
}'

2. PUT the file bytes to upload_url

curl -s -X PUT "<upload_url from step 1>" \
  -H "Content-Type: video/mp4" \
  --data-binary @reel.mp4

3. Schedule with the returned media_url

curl -s -X POST "https://postwing.io/api/v1/posts" \
  -H "Authorization: Bearer pw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "caption": "",
  "scheduled_at": "2026-10-05T18:00:00Z",
  "targets": [
    {
      "account_id": "00000000-0000-0000-0000-000000000002",
      "platform_options": {
        "share_to_feed": true
      }
    }
  ],
  "media_items": [
    {
      "url": "https://cdn…/reel.mp4",
      "type": "video",
      "thumbnail_url": "https://cdn…/cover.jpg"
    }
  ]
}'

X native quote post

Schedule with quote_tweet_url

curl -s -X POST "https://postwing.io/api/v1/posts" \
  -H "Authorization: Bearer pw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "caption": "Adding my take",
  "scheduled_at": "2026-10-05T15:00:00Z",
  "targets": [
    {
      "account_id": "00000000-0000-0000-0000-000000000001",
      "platform_options": {
        "quote_tweet_url": "https://x.com/someone/status/1234567890"
      }
    }
  ]
}'

Pinterest pin (boards required)

1. List boards

curl -s "https://postwing.io/api/v1/accounts/pinterest-boards?account_id=pinterest-account-uuid" \
  -H "Authorization: Bearer pw_live_…"

2. Schedule the pin

curl -s -X POST "https://postwing.io/api/v1/posts" \
  -H "Authorization: Bearer pw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "caption": "New lookbook",
  "scheduled_at": "2026-10-06T12:00:00Z",
  "targets": [
    {
      "account_id": "pinterest-account-uuid",
      "platform_options": {
        "board_ids": [
          "1234567890"
        ],
        "title": "Lookbook",
        "link": "https://example.com/shop"
      }
    }
  ],
  "media_items": [
    {
      "url": "https://cdn.example.com/pin.jpg",
      "type": "image"
    }
  ]
}'

MCP tools

Claude, ChatGPT, and Cursor connect over MCP at https://postwing.io/api/mcp with the same scopes, media fields, and platform options as REST. In chat, the assistant previews and you approve before anything is scheduled.

ToolScopeSummary
get_me—Timezone and active workspace
list_accountsaccounts:readConnected accounts (filter by platform or workspace)
connect_accountaccounts:writeStart connecting an account; returns a browser URL
sync_accountsaccounts:writeImport accounts after the user finishes connecting
disconnect_accountaccounts:writeDisconnect an account (only when the user asks)
list_pinterest_boardsaccounts:readBoards for options.board_id
list_workspacesworkspaces:readWorkspaces and which one is active
create_workspaceworkspaces:writeCreate a workspace (Scale plan)
set_active_workspaceworkspaces:writeSet the default workspace
request_media_uploadposts:writeOpen the in-chat upload widget
import_mediaposts:writeCopy media from public HTTPS URLs
schedule_postposts:writeSchedule a post, after the user approves it
get_postposts:readOne post with each account’s status, link, and error
list_postsposts:readPosts, upcoming first (paginated)
get_posts_summaryposts:readCounts by platform, account, status, and day
update_postposts:writeChange one scheduled post
cancel_postposts:writeCancel a post before it publishes

Access requirements

  • Active PostWing subscription (7-day free trial available)
  • Connected social accounts in the user’s workspace (or connect via the API)
  • API key or OAuth token with the scopes you need
  • Platforms: Instagram, TikTok, YouTube, LinkedIn, X, Facebook, Pinterest, Threads
  • Confirmation tokens: 30 minute TTL, single-user bound
  • Batch edit: up to 20 posts per edit preview

Prefer chat?

Connect Claude or Muse to PostWing and schedule with a preview you approve before anything goes live.