REST API
Programmatically create posts, manage accounts, and schedule content via the posterly API.
The posterly API lets you schedule posts, manage accounts, and upload media programmatically.
Public signup for agents
Agents and AI tools can start a paid API signup without an existing posterly session:
curl -X POST https://www.poster.ly/api/v1/signup \
-H "Content-Type: application/json" \
-d '{
"email": "owner@example.com",
"name": "Example Owner",
"tier": "starter",
"api_addon": true,
"source": "chatgpt"
}'
The response returns a posterly checkout_redirect_url, raw Stripe checkout_url, signup poll_url, dashboard/login URLs, and an optional OAuth authorize URL when the request includes a registered client_id, redirect_uri, and PKCE challenge. Agents should send checkout_redirect_url to the user because raw Stripe URLs can contain fragments that are easy to truncate in chat. Poll the signup session while the user pays and sets their posterly password so the AI conversation can keep reporting progress.
The public, crawlable signup handoff page is /agents/signup. Use it when an agent needs a human-readable explanation of the same paid signup and account-connection flow.
If an AI client supports MCP but not generic HTTP requests, install posterly-mcp-server@latest without POSTERLY_API_KEY and use the public MCP tools start_signup and get_signup_session for the same pre-auth signup handshake.
API signup is not a free-token path: no API key is returned from signup. After payment, the verified user lands on the API page and posterly auto-creates the first API key if none exists, showing it once. OAuth clients receive their access token after the paid user completes consent, then should ask which social account to connect and create a connect session.
Authentication
All API requests require an API key. Generate one in Dashboard → API & MCP.
Pass your key in the Authorization header:
Authorization: Bearer YOUR_API_KEY
A key acts as the person who created it, with that person's workspace role. Creating or rescheduling a post, disconnecting an account, and starting a credential connection need the Publisher role or higher in the account's workspace; editing a post's content needs Editor; creating a workspace-scoped key needs Admin. A request the role does not allow returns 403 with code: "insufficient_workspace_permissions" and the required_role. Brand-locked seats are held to their brands as well: rows outside them answer 404, and a connect started by a brand-locked seat must pass workspace_client_id for one of its brands (403 brand_required otherwise), which the newly connected account is placed in. Reconnecting an account that already lives in another brand, or workspace-wide with no brand, answers 404 account_not_found before any credential is stored. See Teams & Roles.
Base URL
https://www.poster.ly/api/v1
Quickstart
The fastest end-to-end flow is: confirm who you are, list your connected accounts, upload media, validate the complete payload with dry_run: true, show the preview, obtain explicit confirmation, then schedule the live post. The examples below run against production with a key from Dashboard → API & MCP.
Node.js
No SDK needed; the built-in fetch in Node 18+ covers everything:
const BASE_URL = "https://www.poster.ly/api/v1";
const headers = {
Authorization: "Bearer pst_live_YOUR_API_KEY",
"Content-Type": "application/json",
};
async function main() {
// 1. Confirm the authenticated user and scopes
const whoami = await fetch(`${BASE_URL}/whoami`, { headers }).then((r) => r.json());
console.log("Authenticated as", whoami.user.email, "scopes:", whoami.api_key.scopes);
// 2. List connected social accounts and pick one
const { accounts } = await fetch(`${BASE_URL}/accounts`, { headers }).then((r) => r.json());
const account = accounts[0];
console.log("Posting to", account.platform, account.username);
// 3. Copy a public image into posterly storage
const media = await fetch(`${BASE_URL}/media/upload-from-url`, {
method: "POST",
headers,
body: JSON.stringify({
url: "https://example.com/launch-photo.jpg",
filename: "launch-photo.jpg",
}),
}).then((r) => r.json());
// 4. Schedule the post
const res = await fetch(`${BASE_URL}/posts`, {
method: "POST",
headers,
body: JSON.stringify({
account_id: account.id,
caption: "Launch notes are live.",
media_urls: [media.url],
scheduled_at: "2026-08-01T14:00:00Z",
}),
});
const { post } = await res.json();
console.log("Post", post.id, "is", post.status, "for", post.scheduled_at);
}
main().catch(console.error);
Python
The same flow with requests:
import requests
BASE_URL = "https://www.poster.ly/api/v1"
headers = {"Authorization": "Bearer pst_live_YOUR_API_KEY"}
# 1. Confirm the authenticated user and scopes
whoami = requests.get(f"{BASE_URL}/whoami", headers=headers).json()
print("Authenticated as", whoami["user"]["email"], "scopes:", whoami["api_key"]["scopes"])
# 2. List connected social accounts and pick one
accounts = requests.get(f"{BASE_URL}/accounts", headers=headers).json()["accounts"]
account = accounts[0]
print("Posting to", account["platform"], account["username"])
# 3. Copy a public image into posterly storage
media = requests.post(
f"{BASE_URL}/media/upload-from-url",
headers=headers,
json={"url": "https://example.com/launch-photo.jpg", "filename": "launch-photo.jpg"},
).json()
# 4. Schedule the post
post = requests.post(
f"{BASE_URL}/posts",
headers=headers,
json={
"account_id": account["id"],
"caption": "Launch notes are live.",
"media_urls": [media["url"]],
"scheduled_at": "2026-08-01T14:00:00Z",
},
).json()["post"]
print("Post", post["id"], "is", post["status"], "for", post["scheduled_at"])
Zapier
posterly does not have a native Zapier app yet, but both directions work today with Zapier's built-in tools:
Actions (Zap creates a posterly post): use the Webhooks by Zapier action with these settings:
- Event:
Custom Request - Method:
POST - URL:
https://www.poster.ly/api/v1/posts - Headers:
Authorization: Bearer pst_live_YOUR_API_KEYandContent-Type: application/json - Data:
{
"account_id": "123",
"caption": "New blog post is live: {{title}}",
"media_urls": ["{{image_url}}"],
"scheduled_at": "2026-08-01T14:00:00Z"
}
Get the account_id values once by calling GET /api/v1/accounts (curl or the Node/Python snippets above).
Triggers (posterly events start a Zap): use the Webhooks by Zapier trigger (Catch Hook), copy the hook URL Zapier gives you, then register it as a posterly outbound webhook:
curl -X POST https://www.poster.ly/api/v1/webhooks \
-H "Authorization: Bearer pst_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.zapier.com/hooks/catch/xxx/yyy/",
"events": ["post.published", "post.failed"]
}'
posterly will now call your Zap whenever a post publishes or fails. Supported events also include post.created, post.updated, post.deleted, post.publishing, account.disconnected, and analytics.synced. Use POST /api/v1/webhooks/{id}/test to send a test delivery while building the Zap.
n8n
Create posts with the HTTP Request node:
- Method:
POST - URL:
https://www.poster.ly/api/v1/posts - Authentication:
Generic Credential Type→Header Auth, nameAuthorization, valueBearer pst_live_YOUR_API_KEY - Body Content Type:
JSON - Body:
{
"account_id": "123",
"caption": "{{ $json.caption }}",
"media_urls": ["{{ $json.image_url }}"],
"scheduled_at": "{{ $json.publish_time }}"
}
React to posterly events with the Webhook node: create a workflow starting with a Webhook node (method POST), copy its production URL, and register it via POST /api/v1/webhooks exactly as in the Zapier example. Each delivery is signed with an X-posterly-Signature header (t=<unix>,v1=<hmac_sha256>) you can verify in a Code node using the webhook secret returned on create.
Make
Create posts with the HTTP → Make a request module:
- URL:
https://www.poster.ly/api/v1/posts - Method:
POST - Headers:
Authorization: Bearer pst_live_YOUR_API_KEY - Body type:
Raw, Content type:JSON (application/json) - Request content:
{
"account_id": "123",
"caption": "Launch notes are live.",
"media_urls": ["https://example.com/launch-photo.jpg"],
"scheduled_at": "2026-08-01T14:00:00Z"
}
React to posterly events with the Webhooks → Custom webhook module: add the module, copy the webhook address Make generates, and register it via POST /api/v1/webhooks with the events you care about (for example post.published and post.failed), the same call shown in the Zapier section.
Scopes
Every API key is granted a set of scopes. The default set on new keys covers everything the public surface offers today:
accounts:read, list connected social accountsaccounts:write, disconnect connected social accountsposts:read/posts:write, read and schedule postsmedia:write, upload images and videoanalytics:read, read per-account and per-post analyticsbilling:read, read your AI credit balance and posterly subscription statusbilling:write, cancel, pause, resume, or downgrade your posterly subscription
Keys issued before analytics:read existed have been backfilled automatically, no regeneration needed. The two billing scopes are separate opt-ins when you create a key. Read billing adds billing:read and can change nothing, so it is safe to grant to a tool you want to ask about credits. Manage billing adds billing:write for cancel, pause, resume and downgrade; only share those keys with tools you trust.
Workspace scoping
API keys can be scoped to a single workspace. Connection handoff and webhook endpoints use that workspace automatically when workspace_id is omitted. If those requests include a different workspace_id, posterly returns 403 API key is scoped to a different workspace.
Unscoped keys can still pass workspace_id on workspace-aware endpoints. posterly verifies the API-key user belongs to that workspace, and holds the role the endpoint needs, before returning data or creating resources. A connect session or credential connection started by an unscoped key without workspace_id targets the key owner's personal workspace; pass workspace_id to connect into a team workspace.
Endpoints
Identity and platform discovery
GET /api/v1/whoami
GET /api/v1/platforms
GET /api/v1/accounts/{id}/schema
POST /api/v1/platforms/trigger
Use whoami to confirm the authenticated user, scopes, default workspace, and available workspaces. Use platform discovery before creating posts with settings; it returns supported post types, media limits, content limits, first-class settings fields, analytics support, and helper tools.
Available helper tools currently include pinterest.boards, youtube.playlists, tiktok.creator_info, linkedin.recent_mentions, and x.quota. Pass include_planned=true to GET /api/v1/platforms to see the integration backlog and possibilities from the provider registry.
The human-readable registry page is at /api-platforms.
List accounts
GET /api/v1/accounts
Returns all connected social accounts in your workspace.
Disconnect an account
DELETE /api/v1/accounts/{id}
Disconnects a connected social account owned by the caller. This requires accounts:write and should be treated as irreversible: posterly deletes the account connection, preserves reconnect metadata on posts, archives Instagram analytics where available, and emits account.disconnected webhooks.
Agents should call GET /api/v1/accounts first, show the exact id, platform, username, and workspace_id, and only call delete after explicit user confirmation.
Account connection handoff
GET /api/v1/connect
GET /api/v1/connect/{platform}
POST /api/v1/connect/{platform}/credentials
POST /api/v1/connect/{platform}/sessions
GET /api/v1/connect/sessions/{session_id}
Use these endpoints when an agent or external workflow needs to help a user connect another channel. They return the dashboard connection_url, connection method (dashboard_oauth, manual_credentials, or planned), provider scopes, missing environment configuration, and matching connected accounts.
Direct public OAuth URLs are intentionally not returned yet. The current provider callbacks depend on short-lived browser state, provider-mode cookies, and PKCE verifiers, so agents should open connection_url in a logged-in browser session.
For a smoother agent experience, create a connect session instead of only fetching the static handoff link:
- Call
POST /api/v1/connect/{platform}/sessions. - Show or open the returned
connect_session.connect_urlfor the user. - Poll
connect_session.poll_url. - Narrate the returned
status_messageuntil the status becomesconnected,failed,cancelled, orexpired.
The dashboard updates the session as the user moves through the flow: created -> opened -> awaiting_provider or awaiting_credentials -> connected. This lets agents say things like "waiting for Instagram approval" or "connection completed" instead of leaving the user guessing.
Examples:
curl -H "Authorization: Bearer pst_live_xxx" \
https://www.poster.ly/api/v1/connect/instagram
{
"connect": {
"platform": "instagram",
"label": "Instagram",
"method": "dashboard_oauth",
"connection_url": "https://www.poster.ly/dashboard/connect?platform=instagram",
"oauth_url": null,
"requires_browser_session": true,
"configured": true,
"connected_count": 1
}
}
Create a live connection session:
curl -X POST \
-H "Authorization: Bearer pst_live_xxx" \
-H "Content-Type: application/json" \
-d '{"workspace_id":"2d0a6d4c-9eef-4ee2-9026-2ca0dc29c8d6","auto_start":true}' \
https://www.poster.ly/api/v1/connect/instagram/sessions
{
"connect_session": {
"id": "8ec5ae8c-fd6b-4e0b-83d7-0f88c3f5e9e1",
"platform": "instagram",
"method": "dashboard_oauth",
"status": "created",
"status_message": "Connection link created. Waiting for the user to open posterly.",
"connect_url": "https://www.poster.ly/dashboard/connect?platform=instagram&connect_session=8ec5ae8c-fd6b-4e0b-83d7-0f88c3f5e9e1&auto_start=1",
"poll_url": "https://www.poster.ly/api/v1/connect/sessions/8ec5ae8c-fd6b-4e0b-83d7-0f88c3f5e9e1",
"connected_count": 0
}
}
Direct credential connection (no browser)
POST /api/v1/connect/{platform}/credentials
Credential-based platforms (telegram, bluesky, discord, wordpress, devto, hashnode, lemmy) can be connected headlessly with accounts:write. Fetch GET /api/v1/connect/{platform} first: its credential_fields array lists exactly which keys the platform needs, and supports_direct_credentials: true plus credentials_endpoint confirm the platform accepts this flow. These seven platforms now also report requires_browser_session: false in the connect options payload. Mastodon and all OAuth platforms are excluded; use connect sessions for those.
Send the fields inside a credentials object. Unknown keys are rejected, required keys must be non-empty strings. Optional workspace_id targets a workspace you are a member of (your personal workspace when omitted on an unscoped key), optional workspace_client_id places a newly connected account in that brand, and optional connect_session_id marks an existing connect session connected or failed based on the outcome. The call has upsert semantics: reconnecting the same account updates it in place and returns 200 either way. A brand-locked seat can only reconnect accounts already in one of its brands; any other existing account answers 404 account_not_found and is left untouched. Prefer scoped secrets (app passwords, bot tokens, webhook URLs, API tokens) over primary passwords.
curl -X POST \
-H "Authorization: Bearer pst_live_xxx" \
-H "Content-Type: application/json" \
-d '{"credentials":{"handle":"posterly.bsky.social","app_password":"xxxx-xxxx-xxxx-xxxx"}}' \
https://www.poster.ly/api/v1/connect/bluesky/credentials
{
"connected": true,
"account": {
"id": 4321,
"platform": "bluesky",
"username": "posterly.bsky.social",
"did": "did:plc:example"
},
"workspace_id": "2d0a6d4c-9eef-4ee2-9026-2ca0dc29c8d6"
}
Rate limit: 20 credential connect attempts per hour per user. Errors return { "error", "code" } with codes like invalid_credentials, plan_limit, provider_unreachable, and credentials_not_supported; credential values are never echoed back.
OAuth developer apps
GET /api/v1/oauth/clients
POST /api/v1/oauth/clients
PATCH /api/v1/oauth/clients/{clientId}
DELETE /api/v1/oauth/clients/{clientId}
Use these endpoints to create public OAuth + PKCE clients for third-party tools that need to act on behalf of a posterly user. Developer clients are owned by the API-key user, use exact redirect URI matching, and never expose client secrets or wildcard redirects.
Direct provider connection URLs are still handled by the dashboard connection handoff endpoints above. OAuth developer apps are for apps authenticating into posterly's API surface.
Example:
curl -X POST https://www.poster.ly/api/v1/oauth/clients \
-H "Authorization: Bearer pst_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"client_name": "Agency approval app",
"allowed_redirect_uris": ["https://agency.example.com/oauth/callback"],
"default_scopes": ["accounts:read", "posts:read", "posts:write"]
}'
Dynamic Client Registration (RFC 7591)
POST /api/oauth/register
For hosted MCP clients (Claude.ai, ChatGPT, and similar) that cannot ask a human to allowlist them up front. Send a JSON metadata document and receive a client_id immediately. No auth required. Rate-limited per IP. All registered clients are public PKCE clients; no client_secret is issued. Only supported scopes are granted.
curl -X POST https://www.poster.ly/api/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My MCP host",
"redirect_uris": ["https://my-host.example.com/oauth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"scope": "accounts:read posts:read posts:write"
}'
OAuth token endpoint
POST /api/oauth/token
Authorization-code exchange and refresh-token rotation for any registered client. Access tokens issued here live 1 hour; refresh tokens live 30 days and rotate on every use. Re-using a refresh token revokes the entire chain (token-theft defence).
# Exchange authorization code for tokens
curl -X POST https://www.poster.ly/api/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d 'grant_type=authorization_code&code=...&client_id=dyn_...&code_verifier=...&redirect_uri=...'
# Refresh
curl -X POST https://www.poster.ly/api/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d 'grant_type=refresh_token&refresh_token=pst_oauth_rt_...&client_id=dyn_...'
Discovery: GET /.well-known/oauth-authorization-server advertises the registration endpoint, refresh-token grant, supported scopes, and token endpoint.
Brand and client context
GET /api/v1/brands
GET /api/v1/brands/{id}
GET /api/v1/brands/{id}/accounts
GET /api/v1/brands/{id}/profile
Use brand endpoints when an external workflow needs the same client context posterly uses in the dashboard. They return workspace-scoped brands/clients, assigned social accounts, and saved brand profile guidance such as voice, tone, audience, and positioning. These endpoints require accounts:read.
Create a post
POST /api/v1/posts
Create and schedule a new post. See the API Reference for the full request body schema. media_url and media_urls can be posterly media URLs or third-party HTTP(S) URLs. Third-party assets are copied into posterly storage before the post is saved, so short-lived signed URLs must still be live when the request runs. The new post inherits workspace_client_id from the target social account, so it lands under that account's brand on the calendar.
Platform-specific settings, pass settings to use the same first-class scheduling controls available in the posterly composer. The API maps these into the right platform metadata and publisher fields for you, so you do not need to rely on raw metadata for common platform options:
{
"account_id": "169",
"caption": "Launch notes are live.",
"media_urls": ["https://assets.poster.ly/users/123/reel.mp4"],
"scheduled_at": "2026-05-01T14:00:00Z",
"settings": {
"post_type": "reel",
"first_comment": "Full launch notes: www.poster.ly/updates",
"collaborators": ["partnerbrand"],
"reel_cover_url": "https://example.com/reel-cover.jpg",
"is_trial_reel": true,
"graduation_strategy": "MANUAL"
}
}
Supported settings include:
- Instagram, feed, story, reel, carousel, collaborators, user tags, first comment, alt text, trial Reels, Reel covers, parent-container
is_ai_generated, licensed Reelaudio_id(Facebook Login / Meta-linked only), and optional companion Story posting - Facebook, post, story, reel, cover photo intent, colored text backgrounds, and Reel covers
- YouTube, title, thumbnail, privacy status, made-for-kids, tags, category, playlist,
notify_subscribers,add_shorts_hashtag, and optionalbrand_partner - LinkedIn, document title and filename, organization mentions, polls (question, 2-4 options, 1 day/3 days/1 week/2 weeks), video thumbnail, alt text, and
content_call_to_action_label(including BUY_NOW and SHOP_NOW) withcontent_landing_page. Pasting a URL in the caption auto-generates a link-preview card. - TikTok, video posts and photo slideshows, direct-post privacy, comment/duet/stitch toggles, title, auto-music, commercial disclosure,
video_cover_timestamp_ms(pick the video frame used as the cover on direct-post videos; TikTok does not accept custom uploaded covers), andai_generated(AI-content label) - Pinterest,
post_type(image/video/carousel), board (required via helper), title, destination link,cover_image_url(for video Pins),product_tags, andai_generated - Google Business Profile, standard, event, and offer posts with event schedule, offer details, CTA, video, and EVENT/OFFER
recurrence - X, reply settings (
everyone/following/mentionedUsers/subscribers/verified), polls,paid_partnership,ai_generated(made-with-AI label), and media alt text - Threads, reply controls (incl.
followers_only/parent_post_author_only), polls, topic tags, text attachments, ghost posts, spoilers, reply approvals, GIPHYgif_attachment, media alt text, andshare_to_instagram(accepted but not live: Meta has not approvedthreads_share_to_instagram, so production tokens cannot create the linked Instagram Story) - Telegram, native polls (
{ question, options }), inline CTA buttons ([{ text, url }]), parse mode, videocover/start_timestamp, andis_live_photo - Bluesky, languages, content warnings (
content_label), hidden discovery tags (tags), quote posts (quote_url),disable_quotes, and media alt text. Pasting a URL auto-generates a link-preview card. - Discord, webhook channel posts with text (2,000 chars), up to 10 attachments, and media alt text (
media_alt_texts). Each file can be up to 20 MiB, Discord's default webhook limit. Boosted servers and Nitro can allow more; posterly does not detect those entitlements. Mention parsing is disabled so captions never ping @everyone or roles. - Slack, channel posts with optional Block Kit
markdownand customblocks. Posterly's current buffered upload path has a 10 MiB per-file safety ceiling. - Mastodon, statuses on any instance with
visibility(public/unlisted/private), content warnings (spoiler_text),language,quoted_status_id, nativepoll(media plus poll allowed), and media alt text up to 1,500 characters. Publishes are idempotent. - Dev.to, markdown articles with
title(falls back to the first caption line),tags(up to 4), andcanonical_url. First image becomes the cover. - Hashnode, markdown articles to a Hashnode Pro publication with
title,tags(up to 4), and optionalpublication_idoverride. First image becomes the cover. - WordPress, self-hosted posts with
title(falls back to the first caption line). First image is uploaded as the featured image. - Lemmy, community posts with
community(required, e.g.technology@lemmy.world),title, optional linkurl, andnsfw. First image becomes the link embed.
ai_generated: true (alias ai_disclosure) labels a post as AI-generated where the platform supports it (TikTok, X, YouTube, Pinterest, Instagram).
TikTok photo slideshows, TikTok supports either a single video or a photo slideshow of 1 to 35 images, and never multiple videos. You do not need to set post_type or media_type for images: posterly auto-detects image media and posts it as a slideshow, sending every image you provide. Provide one image for a single-image slideshow or 2 or more images for a multi-image slideshow. To be explicit you can still set post_type to "carousel" (2 or more images) or "image" (single image), or pass settings.media_type: "PHOTO". A post with one video publishes as a standard TikTok video.
Instagram Trial Reels use graduation_strategy: "MANUAL" by default. Set "SS_PERFORMANCE" to let Meta auto-share the Reel with followers if it performs well. Meta controls Trial Reel availability per Instagram account; if the Trial toggle does not appear for that account in the Instagram app, Meta may reject API publishing. Collaborators are not sent for Trial Reels. See Instagram Trial Reels (posterly dashboard login required) for the composer workflow and eligibility check.
X / Threads thread chains, to schedule a multi-post reply chain on X (Twitter) or Threads (Meta), set post_type and pass the matching array in metadata:
{
"account_id": "169",
"scheduled_at": "2026-05-01T14:00:00Z",
"post_type": "x_thread",
"metadata": {
"x_thread_tweets": [
"1/ What's new on posterly. Here's everything we shipped in April.",
"2/ Tag users in Instagram Stories...",
"3/ Threads long-form posts up to 10,000 characters..."
]
}
}
For Threads, use post_type: "threads_thread" and metadata.threads_thread_posts. Each entry is validated against per-platform limits (X: 280 chars, Threads: 500 chars) before insert. URLs are still blocked in X thread segments.
Media rules checked at create time
posterly runs the same platform rules the composer uses, so a post that could never publish is rejected here with a 400 and validation_errors rather than failing later on the platform. Three of these are worth calling out because they are enforced at create time:
| Rule | Code | Applies to |
|---|---|---|
PDFs can only be published to LinkedIn. A .pdf in media_url/media_urls, or post_type: "document", is rejected for every other platform. | pdf_requires_linkedin | all platforms except LinkedIn |
Instagram cannot publish .avi or .mkv video. Export as MP4 (H.264/AAC). | instagram_video_unsupported_container | |
| The Instagram account must have its business account id. Reconnect the account in posterly if you see this. | missing_instagram_business_account_id |
Each of these previously reached the publish step and failed there. They now fail fast at creation with an actionable message.
.mov, .webm and .m4v are not rejected for Instagram. posterly converts those containers to MP4 for you before publishing, so keep sending them as-is. Only .avi and .mkv, which posterly does not convert, are refused.
Create multiple posts
POST /api/v1/posts/batch
Create up to 25 posts in one API request. Each item uses the same body shape as POST /api/v1/posts.
{
"posts": [
{
"account_id": "169",
"caption": "Monday launch notes.",
"scheduled_at": "2026-05-18T14:00:00Z"
},
{
"account_id": "170",
"caption": "Behind the scenes from launch week.",
"media_urls": ["https://assets.poster.ly/users/123/photo.jpg"],
"scheduled_at": "2026-05-19T14:00:00Z"
}
]
}
Batch requests can partially succeed. Successful items are returned in posts with their original index; failed items are returned in errors with index, status, and a sanitized error. Send an Idempotency-Key header to make retries safe; posterly derives a stable idempotency key per item.
Update or delete a post
PATCH /api/v1/posts/{id}
DELETE /api/v1/posts/{id}
Update scheduled or draft content, or delete a draft/scheduled post. Updates can pass the same settings object used when creating posts, which makes API updates line up with the composer controls for every supported platform.
Change post status
PUT /api/v1/posts/{id}/status
Pause a scheduled post, resume/schedule a paused, draft, or failed post, or move a scheduled/failed/paused post back to draft.
{
"status": "paused"
}
Use status: "scheduled" with scheduled_at when scheduling a draft or when a paused post's original time is already in the past. Published, publishing, and processing posts cannot be changed through this endpoint.
Repair and group cleanup
GET /api/v1/posts/{id}/missing
PUT /api/v1/posts/{id}/release-id
DELETE /api/v1/posts/group/{group}
Use /missing to inspect whether a post is missing required content, media, account context, or platform settings before it publishes. release-id stores external release/group metadata for agent workflows. Group delete removes only draft, scheduled, failed, or paused posts and requires confirm=true.
Product feedback (public board)
POST /api/v1/feedback
File a bug, idea, or general feedback item on the same public board as Dashboard → Feedback. This is for product requests and user-facing bug reports, not tool-failure telemetry.
Requires:
- Bearer API key with
posts:writeand active API add-on (browser session fallback is rejected) confirm: trueafter the user explicitly approves the title and category- Rate limit: 5 submissions per day per API key (
product_feedback_write)
{
"category": "bug",
"title": "LinkedIn carousel fails when third image is HEIC",
"description": "When scheduling a 3-image carousel, HEIC on image 3 returns invalid_media without saying which file.",
"confirm": true,
"source": "mcp",
"client": "claude-desktop",
"context": {
"request_id": "req_example123",
"related_tool": "create_post"
}
}
Successful creates return 201 with feedback_id, board_url, and request_id. Inserts add a default vote and follow the same Slack + Hermes triage path as dashboard submissions. Never include API keys, secrets, captions, media URLs, or personal data about third parties. Full request/response schemas are in the OpenAPI spec (ProductFeedbackRequest / ProductFeedbackResponse).
Agent workflow telemetry (private)
POST /api/v1/agent-feedback
Store a private, bounded operational event after a real workflow outcome (tool success, error, or abandoned flow). This does not appear on the public feedback board and does not trigger product Slack/Hermes triage.
Requires a bearer API key with posts:write and active API add-on; browser session fallback is rejected. Limited to 60 events/hour per API key. Payload is strictly schema-bound (source, tool, outcome, optional error_code / comment / context). Never include secrets, prompts, captions, media URLs, or personal data. Events are service-role telemetry retained for 180 days.
Use this when an agent hits a concrete API/MCP failure. Use POST /api/v1/feedback when the human wants a product bug or idea filed publicly.
MCP mirrors both endpoints as submit_product_feedback and submit_agent_feedback.
Upload media
POST /api/v1/media/upload
Upload an image or video to attach to a post. This endpoint accepts base64 JSON on a Vercel API route, so the ~4MB request body limit applies. Base64 inflates the file. It is only for small files (our relay cap is 5MB decoded). Videos and large images must use signed upload so the bytes never enter the Vercel body:
{
"filename": "launch-photo.jpg",
"content_type": "image/jpeg",
"data": "<base64 file data>"
}
For larger files, first request a signed upload URL:
POST /api/v1/media/signed-upload
{
"filename": "launch-video.mp4",
"content_type": "video/mp4",
"size": 28400000
}
The response includes upload_url, public_url, headers, expires_at, and max_size. upload_url is an object-storage signed URL (R2 or Supabase), not a posterly API route. PUT the raw file bytes to that URL with the returned headers (at least the same Content-Type). Do not send Authorization on the PUT. The body must be exactly the declared size bytes. The PUT can come from a browser or a backend; storage CORS allows PUT + Content-Type from any origin.
Signed URLs expire after 15 minutes. After a successful PUT, pass the returned public_url as media_url or inside media_urls when creating posts. Supported media: JPEG, PNG, GIF, WebP, MP4, MOV, and WebM.
The PUT to upload_url does not hit the Vercel ~4MB body limit. Object storage accepts the plan caps below.
Do not PUT the file to /api/v1/media/signed-upload/upload. That legacy relay is a Vercel function and rejects large videos with FUNCTION_PAYLOAD_TOO_LARGE (the 4MB body again).
Signed-upload size limits follow the plan, not a 50MB cap:
| Plan | Images | Videos |
|---|---|---|
| Starter | 50MB | 500MB |
| Pro | 100MB | 750MB |
| Power | 150MB | 1GB |
| Agency | 250MB | 4GB |
There is no POST /api/v1/media/upload/signed. The only signed-upload create path is POST /api/v1/media/signed-upload.
Human media drop (ChatGPT and Claude laptop files)
Agents that cannot PUT a laptop file should mint a no-login drop page instead of create_signed_upload. The usual agent path is MCP create_media_drop, then list_media. The REST equivalents are:
POST /api/v1/media/drop-sessions
{
"filename": "launch.mp4",
"max_files": 1
}
Both request fields are optional. filename is a hint shown to the human. max_files is an integer from 1 to 10 (default 10). The response is:
{
"session_id": "2f9b6a51-6f0e-4b58-9a5b-64b3a3f2f9d1",
"drop_url": "https://www.poster.ly/drop/exampletoken",
"expires_at": "2026-09-09T12:00:00.000Z",
"max_bytes": 4294967296,
"max_files": 10,
"instructions": "Send this link to the user: https://www.poster.ly/drop/exampletoken They open it in a browser with no posterly login and drop the file."
}
Send drop_url (https://www.poster.ly/drop/<token>) to the user. They open it with no dashboard login and drop the file. The drop lasts 24 hours. Chat paperclips never reach MCP. HEIC and PDF are rejected. Plan video caps still apply (see the table above). This is not create_signed_upload: the human's browser PUTs to object storage, not the agent.
After they finish, list the uploaded assets:
GET /api/v1/media?drop_session_id={session_id}
drop_session_id is optional. Omit it to list the newest media for the authenticated user. limit is an integer from 1 to 50 (default 10). Each item includes id, public_url, filename, path, created_at, and may include mime, bytes, and drop_session_id. Pass public_url as media_url or inside media_urls when you validate or create a post.
You can also fetch an existing public asset into posterly storage:
POST /api/v1/media/upload-from-url
{
"url": "https://example.com/launch-photo.jpg",
"filename": "launch-photo.jpg"
}
The fetcher blocks localhost/private IP targets and redirect chains that resolve to private networks.
Use the returned url as media_url or inside media_urls when creating or updating posts.
create_post copies third-party media_url values the same way. That path is only for small files: about 5MB on the server-relay upload, with a 60MB remote-fetch hard cap. Larger files from Cloudflare R2, S3, or another CDN fail with remote_media_too_large (Remote file is too large to fetch through the API). Download the file and use signed-upload instead.
Automatic media analysis
Uploads are automatically registered and analyzed in the background (dimensions, duration, codecs). Upload responses from /api/v1/media/upload and the signed upload PUT include an asset_id field referencing the analyzed asset; it can be null if analysis registration was skipped. You do not need to do anything with it.
Once a video has been analyzed, post creation validates that the file can actually reach the selected platform. Combinations that can never publish are rejected with a structured 400 error instead of failing at publish time:
| Code | Meaning | Example message |
|---|---|---|
video_too_long | The video exceeds the platform's maximum duration | This video is 21m but X allows up to 20m. Trim it to include X. |
video_exceeds_platform_limit | The video is too large for the platform's file size limit | This video is too large for Telegram (20 MB limit). Export it under 20 MB to include Telegram. |
Media that has not been analyzed yet (or was uploaded before this feature) is accepted as before, and anything posterly can adapt automatically is handled without any error.
Find next available slot
GET /api/v1/slots/next
Find the next available posting slot for a given account.
X posting quota
GET /api/v1/x-posting/quota
Returns the current workspace's managed X posting allowance, used and remaining posts, current quota period, URL-block status, active add-on details, and available X quota plans. This requires posts:read.
Account analytics
GET /api/v1/analytics/accounts?account_id={id}&from=YYYY-MM-DD&to=YYYY-MM-DD
Daily snapshots plus a period summary. Social platforms include follower delta, reach, views, and engagement rate; platforms with native dashboards include display_metrics so clients can render exact labels such as Google Business Profile's Profile Views, Search Views, Maps Views, Customer Actions, and Posts. Defaults to the last 30 days. Supported platforms: Instagram, Facebook Pages, LinkedIn, Google Business Profile, Pinterest, YouTube, and Threads. Requires analytics:read.
Post analytics
GET /api/v1/analytics/posts?account_id={id}&from=YYYY-MM-DD&to=YYYY-MM-DD&limit=50&offset=0
Per-post engagement rows (likes, comments, reach, impressions, saves, shares, reposts, quotes, and plays) most-recent-first. For Threads, comments represents replies, while reposts, quotes, and shares remain distinct native metrics. limit defaults to 50, max 200. Same platform support and scope requirement as above.
Performance feedback loop
These three read endpoints and one write endpoint expose the performance feedback loop (post to analytics to per-post insight to performance-aware suggestion). All require analytics:read and a Pro plan or higher; lower tiers get 403 with code: "feature_not_available". Data is scoped to the API key's workspace.
Account performance profile
GET /api/v1/accounts/{id}/performance-profile
Returns the read-only performance profile posterly derives for the account from the last 90 days of per-post analytics: stats (top formats, timing, caption-length bands), an engagement-rate trend, coaching bullets, a narrative summary, and derived_at. When the account is connected but has no profile, performance_profile is null with a reason:
insufficient_data, not enough tracked posts yet.platform_not_measurable, the platform has no per-post analytics (for example Google Business Profile).
{
"performance_profile": {
"social_account_id": 42,
"platform": "instagram",
"username": "brandhandle",
"sample_size": 38,
"window_days": 90,
"engagement_rate_avg": 0.041,
"engagement_rate_trend": "up",
"bullets": ["reel posts average 5.2% engagement (12 posts sampled)"],
"narrative_summary": "Reels posted midweek tend to outperform static images.",
"stats": { "windowDays": 90, "sampleSize": 38, "platform": "instagram" },
"derived_at": "2026-07-03T02:00:00.000Z"
},
"reason": null
}
Post feedback insights
GET /api/v1/analytics/insights?account_id={id}&post_id={id}&checkpoint=24h&limit=20
Per-post feedback insights for recently published posts, most-recent-first. Each item carries the performance tier (great / good / mixed / poor), a diagnosis, a next_action, impact_score, confidence, metrics, and the baseline it was compared against. Optional filters: account_id, post_id, and checkpoint (one of 1h, 6h, 24h, 72h, 7d). limit defaults to 20, max 100.
{
"insights": [
{
"id": "a1b2c3d4-0000-0000-0000-000000000000",
"post_id": 9182,
"platform": "instagram",
"checkpoint": "24h",
"performance_tier": "great",
"title": "Strong 24 hour signal",
"diagnosis": "This reel is ahead of your usual Instagram baseline on reach.",
"next_action": "Turn the hook into a follow-up while the topic is still fresh.",
"impact_score": 90,
"confidence": 0.72,
"metrics": { "primaryViews": 5200, "engagements": 410 },
"baseline": { "basis": "same_media_90d", "primaryViewsPercentile": 0.91 },
"created_at": "2026-07-03T14:05:00.000Z"
}
]
}
Post suggestions
GET /api/v1/suggestions?account_id={id}&status=pending&limit=20
POST /api/v1/suggestions/{id}/dismiss
GET lists proactive post suggestions (evidence-based weekly drafts written in each account's learned voice). Each item includes the caption, platform, account, a rationale tying the draft back to what has performed well, period_key, and status. Filter by account_id and status (pending default, or scheduled / dismissed). limit defaults to 20, max 100.
POST /api/v1/suggestions/{id}/dismiss dismisses a suggestion so it stops appearing. It never overwrites a suggestion that was already turned into a scheduled post (returns 409). This write requires posts:write (the closest existing write scope; suggestions are draft posts).
{
"suggestions": [
{
"id": "f9e8d7c6-0000-0000-0000-000000000000",
"social_account_id": 42,
"platform": "instagram",
"account": { "id": 42, "username": "brandhandle", "platform": "instagram" },
"caption": "Behind the scenes of our midweek reel workflow...",
"rationale": "Reels posted midweek are your top performers over the last 90 days.",
"period_key": "2026-W27",
"status": "pending",
"created_at": "2026-07-01T09:00:00.000Z"
}
]
}
Social inbox
GET /api/v1/inbox/conversations
GET /api/v1/inbox/conversations/{id}
POST /api/v1/inbox/conversations/{id}/reply
GET /api/v1/inbox/comments
GET /api/v1/inbox/comments/{id}
PATCH /api/v1/inbox/comments/{id}
DELETE /api/v1/inbox/comments/{id}
POST /api/v1/inbox/comments/{id}/reply
POST /api/v1/inbox/sync
The public API wraps the dashboard Social Inbox for Instagram DMs and comments, Facebook Page DMs and comments, and Threads replies. Scheduling already covers all 18 platforms; this inbox surface does not add new networks. Requires a Pro plan or higher. Reads need posts:read and viewer access; replies, hide, and sync need posts:write and editor access. Comment delete needs posts:write and admin access. Delete is Instagram and Facebook Page comments only; Threads replies return 400 because they can be hidden, not deleted. DM replies honor Meta 24-hour windows. Facebook has no private comment replies. Threads has no DMs.
Google Business Profile workflows
GET /api/v1/google-business/reviews
POST /api/v1/google-business/reviews/reply
DELETE /api/v1/google-business/reviews/reply
POST /api/v1/google-business/reviews/suggest-reply
GET /api/v1/google-business/review-link
GET /api/v1/google-business/audit
GET /api/v1/google-business/media
POST /api/v1/google-business/media
DELETE /api/v1/google-business/media
Local-business tools expose connected GBP reviews, review request links, live profile audits, review reply workflows, and profile photo/video management over the same API/MCP/CLI surface. Read endpoints require analytics:read or accounts:read; reply and media writes require posts:write plus publisher workspace access. AI reply suggestions cost 1 AI credit per request from the unified credit wallet.
Profile media manages the standing photo and video gallery shown on a location's Maps/Search profile (distinct from media attached to a post). GET /api/v1/google-business/media lists items for one location (account_id or location_id) or across all GBP locations. To add one, first upload the file via the /media endpoints to get a public source_url, then POST with source_url, a category (one of COVER, PROFILE, LOGO, EXTERIOR, INTERIOR, PRODUCT, AT_WORK, FOOD_AND_DRINK, MENU, COMMON_AREA, ROOMS, TEAMS, ADDITIONAL), and optional media_format (PHOTO default, or VIDEO). DELETE takes the media_name returned by GET. COVER and PROFILE are single-slot. Photos are limited to 25MB; videos to ~30s and 75MB and pass Google's review before appearing.
AI generation
POST /api/v1/ai/generate-captions
GET /api/v1/ai/video-options
POST /api/v1/ai/video-function
POST /api/v1/ai/generate-video
GET /api/v1/ai/video-jobs
GET /api/v1/ai/video-jobs/{id}
POST /api/v1/ai/generate-image
/ai/generate-captions generates or adapts brand-aware caption suggestions for one or more platforms. It requires posts:write and costs 1 AI credit per request from the unified credit wallet (included plan credits first, then purchased credits; 402 when neither covers it). It returns suggestions only; it does not create a post.
Every caption is checked against the brief and the workspace's business facts. Prices, dates, times, phone numbers, links and handles that cannot be verified, and any never-say phrases you set on a fact, are removed after one rewrite attempt, so a platform may return fewer variants than count. Bare numbers and percentages are kept unless the brand has facts of that kind on file.
/ai/video-options is a read-only Google Veo and xAI Grok discovery endpoint for models, input modes, durations, resolutions, aspect ratios, and credit-cost estimates. It does not generate video or spend credits.
/ai/video-function runs read-only provider helpers such as estimate_cost and validate_request before generation. Use identifier google_veo or xai_grok. It does not generate video or spend credits.
/ai/generate-video queues a cost-guarded Google Veo or xAI Grok Imagine Video 1.5 job and returns 202 Accepted with a job_id. Veo supports 4/6/8-second jobs; Grok supports 1–15 seconds at 480p, 720p, or 1080p. It spends AI credits from the unified wallet, so show the user the provider, prompt, duration, resolution, and cost before calling it from an agent. Poll /ai/video-jobs/{id} until the job is completed and video_url is available.
{
"prompt": "A clean vertical product teaser for a social scheduling app.",
"provider": "xai",
"aspect_ratio": "9:16",
"duration_seconds": 8,
"resolution": "720p",
"generate_audio": true
}
/ai/generate-image creates Nano Banana or Grok Imagine Image 2.0 images and returns media URLs you can pass to media_url or media_urls when creating posts. Pass provider: "xai" with quality: "low" | "medium" and resolution: "1K" | "2K" for Grok. Google remains the default; included plan credits are spent first, then purchased credits.
AI credits
GET /api/v1/credits
Returns the AI credit wallet the key can spend from: available right now, the monthly included allowance with used and remaining, purchased_balance, plan_tier, and resets_at for the next included-pool reset.
Credits are a shared workspace wallet owned by the billing owner, and every active member's usage draws the same pool. This endpoint reports that wallet rather than a personal figure, so a member's key sees the balance it can actually spend. workspace.role tells you the caller's role.
Requires billing:read. Like the subscription endpoints it does not require the paid API add-on, so you can always read your own balance, including when a spent wallet is what is blocking you. If the wallet lookup degrades it returns 503 with code: "wallet_unavailable" rather than a misleading zero balance.
Subscription management
GET /api/v1/subscription
POST /api/v1/subscription/cancel
POST /api/v1/subscription/pause
POST /api/v1/subscription/resume
POST /api/v1/subscription/downgrade
These endpoints let you read and self-manage your own posterly subscription. GET requires billing:read; the four mutations require billing:write. They act only on the API key owner's subscription.
Unlike the rest of the API, subscription endpoints do not require the paid API add-on. Managing your own subscription is not an API-product feature, and gating it would trap paused or cancel-pending users. They still require a valid, non-revoked key with the right billing scope. If the user has no subscription, these return 404 with code: "no_subscription".
cancel requires a reason from a fixed catalog, matching the dashboard flow: too_expensive, not_using, missing_features, technical_issues, switching_service, temporary_break, other. An invalid or missing reason returns 400 with code: "invalid_reason". Optional feedback (free text) and immediate (defaults to false, cancel at period end) are also accepted.
pause pauses for 30 days and is limited to one pause per 90-day cooldown (429 pause_cooldown otherwise). resume clears a pause. downgrade moves the plan down one tier at the next renewal with no proration, or to an explicit lower tier (starter, pro, power_user, agency) if provided.
curl -X POST https://www.poster.ly/api/v1/subscription/cancel \
-H "Authorization: Bearer pst_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"reason":"too_expensive","immediate":false}'
To reactivate a pending cancellation, restart a lapsed plan, or accept a retention offer, use the posterly dashboard; those flows stay app-only.
Activity and webhooks
GET /api/v1/activity
GET /api/v1/notifications
GET /api/v1/webhooks
POST /api/v1/webhooks
PATCH /api/v1/webhooks/{id}
DELETE /api/v1/webhooks/{id}
POST /api/v1/webhooks/{id}/test
/activity returns recent post activity and publish logs so agents can audit what happened without scraping the dashboard. /notifications is an alias for agent and Postiz-style notification consumers.
Webhooks support webhook.test, post.created, post.updated, post.deleted, post.publishing, post.published, post.failed, account.disconnected, and analytics.synced. Deliveries are signed with X-posterly-Signature using t=<unix>,v1=<hmac_sha256>.
Updates feed
GET /api/v1/updates
Returns the posterly updates feed (the same product news shown at /updates), newest first. Requires accounts:read and an active posterly subscription; without one it returns 403 with code: "subscription_required", exactly like the web page.
Query parameters:
limit, integer 1 to 50, defaults to 10.since, ISO date. Only return updates published on or after this date.include_content, boolean, defaults tofalse. Whentrue, each update includes its full markdown body incontent.
curl "https://www.poster.ly/api/v1/updates?limit=5&since=2026-07-01" \
-H "Authorization: Bearer pst_live_your_key_here"
Each update includes slug, title, date, version, summary, type (feature, improvement, fix, or null), areas (product areas such as composer or api), a canonical url on poster.ly, and optional image/video links.
CLI
The public posterly CLI is the terminal interface over the same /api/v1 surface used by the REST API and MCP. Run it without installing anything globally:
npx @posterly/cli@latest auth:login
npx @posterly/cli@latest doctor --pretty
npx @posterly/cli@latest whoami --pretty
npx downloads and runs the CLI for that command. It does not leave a permanent posterly binary on your shell PATH, so keep using npx @posterly/cli@latest for each command unless you install globally with npm i -g @posterly/cli@latest.
If global install fails with an EACCES error under /usr/local/lib/node_modules, use npx or switch to a user-owned Node install such as nvm. A local npm i @posterly/cli install only exposes ./node_modules/.bin/posterly inside that folder.
After a global install, the same commands can be run as posterly:
posterly auth:login
posterly auth:key
posterly doctor --pretty
posterly whoami --pretty
posterly connect:link instagram --pretty
posterly oauth:create-client --client-name "Agency approval app" --redirect-uri https://agency.example.com/oauth/callback --scopes accounts:read,posts:read,posts:write --pretty
posterly platforms:schema --platform instagram --pretty
posterly accounts:disconnect 123 --confirm --pretty
posterly posts:create --account-id 123 --caption "Launching soon"
posterly posts:status 123 --status paused --confirm --pretty
posterly posts:missing 123 --pretty
posterly posts:release-id 123 --release-id launch-2026-05-16 --group-id campaign-42 --pretty
posterly posts:delete-group campaign-42 --confirm --pretty
posterly gbp:reviews --account-id 123 --unanswered --pretty
posterly gbp:audit 123 --pretty
posterly gbp:media --account-id 123 --pretty
posterly gbp:add-media --account-id 123 --source-url https://cdn.example.com/front.jpg --category COVER --confirm --pretty
posterly gbp:delete-media --account-id 123 --media-name accounts/123/locations/456/media/abc --confirm --pretty
posterly gbp:suggest-reply --account-id 123 --star-rating 5 --review-text "Great service" --pretty
auth:login opens a browser and uses posterly's OAuth + PKCE flow to store a local token in ~/.posterly/config.json. auth:key remains available for CI, servers, and users who prefer to paste a Personal Access Token. Use posterly doctor --pretty to validate Node, the configured API origin, the installed package version, and the API key against /api/v1/whoami. The CLI outputs JSON by default for jq-friendly scripting; use --pretty for formatted output.
For non-production deployments, set --url or POSTERLY_URL. The CLI refuses to send API keys to non-local http:// URLs by default; use https://, http://localhost for local development, or --allow-insecure-url only for a trusted private development endpoint.
Rate limits
API limits are per API key and route-aware:
POST /api/v1/postsandPOST /api/v1/posts/batch: 100 requests per hour- post creation: 1,000 created post items per hour per user across all API keys; a batch of 25 counts as 25 items
POST /api/v1/media/*: 120 requests per hour- read endpoints, including accounts, posts, slots, analytics, and subscription status: 300 requests per hour
POST /api/v1/subscription/*(cancel, pause, resume, downgrade): 30 requests per hourPOST /api/v1/support/chat: 30 requests per hour per userPOST /api/v1/ai/generate-captions: 20 requests per minute per user- authenticated API traffic: 2,000 requests per hour per user across all API keys
Each API response includes X-Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. A 429 response also includes Retry-After, retry_after, limit, remaining, and reset. Pre-authenticated requests are also IP-limited before API key lookup to protect the API from invalid-token floods.
Full reference
See the interactive API Reference for complete endpoint documentation with request/response examples.