# posterly — complete agent reference ## Product facts - Supported platforms: 18 - Platforms: Instagram, Facebook, TikTok, X, LinkedIn, YouTube, Pinterest, Threads, Google Business, Telegram, Bluesky, Discord, Slack, Mastodon, Dev.to, Hashnode, WordPress, Lemmy - API and MCP add-on: $3/month individual or $29/month Agency - Destructive actions require explicit user confirmation. ## Canonical discovery URLs - openApi: https://www.poster.ly/api/openapi - apiReference: https://www.poster.ly/reference - mcp: https://www.poster.ly/mcp - mcpEndpoint: https://www.poster.ly/api/mcp - mcpServerCard: https://www.poster.ly/.well-known/mcp/server-card.json - skillsIndex: https://www.poster.ly/.well-known/agent-skills/index.json - agentSignup: https://www.poster.ly/agents/signup - llms: https://www.poster.ly/llms.txt - llmsFull: https://www.poster.ly/llms-full.txt ## Validated golden path 1. Discover posterly through this file, llms.txt, OpenAPI, or the MCP server card. 2. Start paid agent signup and let the user complete billing and authentication. 3. Call whoami, then list_platforms and list_accounts. 4. Create a browser connection session if the required social account is absent. 5. Call validate_post or POST /api/v1/posts with dry_run: true. 6. Resolve errors and remote_media_not_materialized warnings. 7. Show the exact final post and obtain explicit confirmation. 8. Call create_post with confirm: true, or make the live REST request. ## OpenAPI summary REST API for scheduling and managing social posts in posterly. ### Get your API key 1. Go to `Dashboard -> API & MCP` in posterly. 2. Click **Generate API key**. 3. Copy the key and use it as `Authorization: Bearer pst_live_...`. Agents can create additional scoped API keys with `POST /api/v1/api-keys` and revoke user-created keys with `DELETE /api/v1/api-keys/{id}` after explicit confirmation. Both actions require a dashboard-created API key; OAuth-issued and managed assistant tokens cannot manage API keys. Agents can start a new paid API signup with `POST /api/v1/signup`. This returns a posterly checkout handoff URL; it never returns an API key before billing is active. API page: `https://www.poster.ly/dashboard/api` ### Quickstart ```bash curl -H "Authorization: Bearer POSTERLY_API_KEY" \ https://www.poster.ly/api/v1/accounts ``` ```bash curl -X POST https://www.poster.ly/api/v1/posts \ -H "Authorization: Bearer POSTERLY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id":"123", "platform":"instagram", "caption":"Launching soon", "post_type":"image", "media_urls":["https://assets.poster.ly/users/123/post.jpg"], "settings":{"__type":"instagram","post_type":"post"} }' ``` Use `POST /api/v1/posts/batch` to create up to 25 posts in a single request. ### X posting limits - posterly-managed X posting is limited per workspace monthly X posting cycle. - X posts containing URLs are currently blocked in the API, including bare domains like `poster.ly`. - X thread segments count toward the monthly X allowance one post per tweet segment. - Use `GET /api/v1/x-posting/quota` to inspect the current X allowance before creating posts. ### Rate limits and errors - `POST /api/v1/posts` and `POST /api/v1/posts/batch`: 100 requests/hour per API key. - Post creation is also capped at 1,000 created post items/hour per user across all API keys; a batch of 25 counts as 25 items. - `POST /api/v1/media/*`: 120 requests/hour per API key. - Read endpoints: 300 requests/hour per API key. - Authenticated API traffic is capped at 2,000 requests/hour per user across all API keys. - Pre-authenticated requests are IP-limited before API key lookup to protect against invalid-token floods. - Every authenticated response includes `X-Request-Id`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers. - If rate-limited, the API returns `429` with `retry_after`, `limit`, `remaining`, `reset`, and a `Retry-After` header. ### Operation index - POST /api/oauth/register — Dynamic Client Registration (RFC 7591) - POST /api/oauth/token — OAuth 2.1 token endpoint - GET /api/v1/accounts — List connected social accounts - DELETE /api/v1/accounts/{id} — Disconnect a social account - GET /api/v1/accounts/{id}/learned-voice — Get learned voice for a connected account - GET /api/v1/accounts/{id}/performance-profile — Get performance profile for a connected account - GET /api/v1/accounts/{id}/schema — Get platform schema for a connected account - GET /api/v1/activity — List recent post activity and publish logs - POST /api/v1/agent-feedback — Submit private agent workflow feedback - POST /api/v1/ai/generate-captions — Generate AI caption suggestions - POST /api/v1/ai/generate-image — Generate an AI image - POST /api/v1/ai/generate-video — Create an AI video generation job - POST /api/v1/ai/video-function — Run an AI video helper function - GET /api/v1/ai/video-jobs — List AI video generation jobs - GET /api/v1/ai/video-jobs/{id} — Get one AI video generation job - GET /api/v1/ai/video-options — List AI video generation options - GET /api/v1/analytics/accounts — Get account analytics - GET /api/v1/analytics/insights — List per-post feedback insights - GET /api/v1/analytics/posts — Get post analytics - POST /api/v1/api-keys — Create a scoped API key - DELETE /api/v1/api-keys/{id} — Revoke an API key - GET /api/v1/brands — List brands/clients - GET /api/v1/brands/{id} — Get brand/client by ID - GET /api/v1/brands/{id}/accounts — List social accounts assigned to a brand/client - GET /api/v1/brands/{id}/profile — Get the saved brand profile for a brand/client - GET /api/v1/connect — List account connection handoff options - GET /api/v1/connect/{platform} — Get a dashboard connection handoff for one platform - POST /api/v1/connect/{platform}/credentials — Connect a credential-based account directly - POST /api/v1/connect/{platform}/sessions — Create an account connect session - GET /api/v1/connect/sessions/{session_id} — Get account connect session status - GET /api/v1/credits — Get the AI credit balance - POST /api/v1/feedback — Submit product feedback to the public board - GET /api/v1/google-business/audit — Audit a Google Business Profile - DELETE /api/v1/google-business/media — Remove a photo or video from a Google Business profile - GET /api/v1/google-business/media — List Google Business profile photos and videos - POST /api/v1/google-business/media — Add a photo or video to a Google Business profile - GET /api/v1/google-business/review-link — Get a Google review link - GET /api/v1/google-business/reviews — List Google Business Profile reviews - DELETE /api/v1/google-business/reviews/reply — Delete a Google Business Profile review reply - POST /api/v1/google-business/reviews/reply — Reply to a Google Business Profile review - POST /api/v1/google-business/reviews/suggest-reply — Generate Google review reply suggestions - GET /api/v1/inbox/comments — List inbox comments - DELETE /api/v1/inbox/comments/{id} — Delete an inbox comment - GET /api/v1/inbox/comments/{id} — Get an inbox comment - PATCH /api/v1/inbox/comments/{id} — Update an inbox comment - POST /api/v1/inbox/comments/{id}/reply — Reply to an inbox comment - GET /api/v1/inbox/conversations — List inbox conversations - GET /api/v1/inbox/conversations/{id} — Get an inbox conversation - POST /api/v1/inbox/conversations/{id}/reply — Reply to an inbox conversation - POST /api/v1/inbox/sync — Sync inbox data for an account - GET /api/v1/media — List media assets - POST /api/v1/media/drop-sessions — Create a human media drop - POST /api/v1/media/signed-upload — Get signed media upload URL - POST /api/v1/media/upload — Upload media directly - POST /api/v1/media/upload-from-url — Fetch media from a public URL - GET /api/v1/notifications — List agent notifications and publish activity - GET /api/v1/oauth/clients — List OAuth developer clients - POST /api/v1/oauth/clients — Create OAuth developer client - DELETE /api/v1/oauth/clients/{clientId} — Delete OAuth developer client - PATCH /api/v1/oauth/clients/{clientId} — Update OAuth developer client - GET /api/v1/platforms — List platform schemas and integration possibilities - POST /api/v1/platforms/trigger — Run a platform helper tool - GET /api/v1/posts — List posts - POST /api/v1/posts — Create a post - DELETE /api/v1/posts/{id} — Delete post - GET /api/v1/posts/{id} — Get post by ID - PATCH /api/v1/posts/{id} — Update post - GET /api/v1/posts/{id}/missing — Inspect missing post content/settings - PUT /api/v1/posts/{id}/release-id — Set external release ID on a post - PUT /api/v1/posts/{id}/status — Change post status - POST /api/v1/posts/batch — Create multiple posts - DELETE /api/v1/posts/group/{group} — Delete a group of draft/scheduled posts - POST /api/v1/signup — Start a paid API signup handoff - GET /api/v1/signup/sessions/{session_id} — Poll paid signup handoff status - GET /api/v1/slots/next — Find next available scheduling slots - GET /api/v1/subscription — Get the authenticated user subscription - POST /api/v1/subscription/cancel — Cancel the subscription - POST /api/v1/subscription/downgrade — Downgrade the subscription - POST /api/v1/subscription/pause — Pause the subscription - POST /api/v1/subscription/resume — Resume a paused subscription - GET /api/v1/suggestions — List proactive post suggestions - POST /api/v1/suggestions/{id}/dismiss — Dismiss a post suggestion - POST /api/v1/support/chat — Ask authenticated posterly Support AI - GET /api/v1/updates — List the latest posterly product updates - GET /api/v1/webhooks — List webhook subscriptions - POST /api/v1/webhooks — Create webhook subscription - DELETE /api/v1/webhooks/{id} — Delete webhook subscription - PATCH /api/v1/webhooks/{id} — Update webhook subscription - POST /api/v1/webhooks/{id}/test — Send a test webhook delivery - GET /api/v1/whoami — Get authenticated user and workspace context - GET /api/v1/x-posting/quota — Get the current managed X posting quota ## Skill: compose-post --- name: compose-post description: Schedule or immediately publish a social media post via posterly across 18 supported platforms. license: proprietary --- # Compose a social media post via posterly Use this skill when the user wants to schedule, publish, or queue a social media post. posterly publishes to 18 platforms with one API call: Instagram, Facebook, TikTok, X, LinkedIn, YouTube, Pinterest, Threads, Google Business, Telegram, Bluesky, Discord, Slack, Mastodon, Dev.to, Hashnode, WordPress, and Lemmy. ## Prerequisites 1. The user must have a posterly account with the API add-on ($3/month). Check `https://www.poster.ly/.well-known/oauth-protected-resource` for the canonical token acquisition URL. 2. They must have a Personal Access Token from `https://www.poster.ly/dashboard/api`. Pass it as `Authorization: Bearer pst_live_...`. 3. They must have at least one social account connected. If `list_accounts` returns nothing, redirect them to `https://www.poster.ly/dashboard/connect`. ## Recommended flow 1. **Discover state**, call `whoami` first to confirm auth and learn the default workspace. 2. **List accounts**, call `list_accounts` to find the right `account_id` and confirm the platform is connected. 3. **Find a slot** (optional), if the user said "next available time," call `find_available_slot` and ask them to confirm before posting. 4. **Upload media** (if needed), call `upload_media` with binary data; it returns a posterly-hosted URL. 5. **Validate**, call `validate_post` with the complete payload. Resolve validation errors and explain any `remote_media_not_materialized` warning; dry-run never uploads or reserves media. 6. **Preview and confirm**, show the final account/platform, caption, schedule, media, settings, and workspace. Obtain explicit confirmation. 7. **Create the post**, call `create_post` with the same validated payload and `confirm: true`. 8. **Report the result**, show the post ID and the `View in Posterly` link returned by `create_post` or `create_posts_batch`. Use that returned link instead of inventing one, because posterly routes current-month scheduled posts to Calendar and general/future views to Table. ## Platform-specific guidance - **Instagram**: requires media. Stories vs Feed posts use different `metadata.instagram_post_type`. - **X / Twitter**: 280-char limit (4000 for Premium); only the first image becomes the card. - **LinkedIn**: posting as a Person is the default; Organization posts need a separate connected account. - **TikTok / YouTube**: video required; mind the daily upload caps. - **Google Business Profile**: posts expire after 7 days and don't appear in feeds, they're attached to the business listing. ## Errors - `401 invalid_token` → token is missing/expired. Surface the upgrade flow from the `WWW-Authenticate` header's `token_uri`. - `403 insufficient_subscription` → user is logged in but has no API add-on. Show `error_uri` from `WWW-Authenticate` (the upgrade page) as a CTA. - `403 insufficient_scope` → token doesn't have `posts:write`. Tell the user to regenerate with the right scope. - `429` → rate limited. Honor `Retry-After`. ## Reference - Tool spec: `https://www.poster.ly/api/openapi` - Human docs: `https://www.poster.ly/reference` - MCP server card: `https://www.poster.ly/.well-known/mcp/server-card.json` ## Skill: find-posting-slot --- name: find-posting-slot description: Find the next available time to post that respects a 1-hour gap between posts and a user's preferred posting hours. license: proprietary --- # Find an open posting slot via posterly Use when the user asks "when can I post?", "next available time", or wants to schedule something without picking the time themselves. ## Endpoint `GET /api/v1/slots/next` (also exposed as the MCP tool `find_available_slot`). ## Required input - `Authorization: Bearer pst_live_...` - `account_id` or `account_ids`, the post's destination(s). - `timezone`, IANA TZ database name (e.g. `Europe/London`). **Always pass this explicitly.** Default is `America/New_York`. ## Optional input - `workspace_id`, only avoid collisions with posts in this workspace (otherwise checks all the user's workspaces). - `count`, how many candidate slots to return (default 1, max 10). ## Algorithm (so the agent can explain it) 1. Walk forward from now in 15-minute increments. 2. Reject any time outside 8am-10pm in the given timezone. 3. Reject any time within 60 minutes of an existing scheduled or published post on any of the supplied accounts. 4. Return up to `count` slots. ## Response shape ```json { "slots": [{ "time": "2026-04-19T14:30:00Z", "local_time": "2026-04-19 15:30 BST" }] } ``` Always confirm the suggested time with the user before calling `create_post` with it, agents must not silently pick a slot. ## Reference - API spec: `https://www.poster.ly/api/openapi` - Resource metadata: `https://www.poster.ly/.well-known/oauth-protected-resource` ## Skill: analyze-account-performance --- name: analyze-account-performance description: Pull daily analytics snapshots and a period summary for connected Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, YouTube, or Threads accounts. license: proprietary --- # Analyze social account performance via posterly Use when the user asks "how's my Instagram doing?", "compare last 30 days vs the 30 before", or wants engagement / reach numbers without leaving the agent. ## Coverage - **Instagram**: followers, reach, impressions, profile views, engagement rate. - **LinkedIn (Person)**: follower growth, post impressions, engagement. - **Google Business Profile**: views (search + maps), website clicks, calls, direction requests, photo views. - **Pinterest**: Pin/content metrics when the account has analytics scopes. - **YouTube**: channel and video metrics when the account has analytics scopes. - **Facebook Pages**: Page views, engagement, and per-post metrics. - **Threads**: post views, likes, replies, reposts, quotes, and follower growth. - **Other platforms** (X, TikTok, Bluesky, Telegram): not yet covered by analytics endpoints. ## Endpoints - Account-level: `GET /api/v1/analytics/accounts?account_id=...&from=...&to=...` (MCP: `get_account_analytics`) - Per-post: `GET /api/v1/analytics/posts?account_id=...&from=...&to=...` (MCP: `get_post_analytics`) ## Required input - `Authorization: Bearer pst_live_...` with `analytics:read` scope. - `account_id` (UUID), get from `list_accounts`. - `from` and `to`, ISO 8601 date range (max 90 days). ## Recommended flow 1. Confirm the platform via `list_accounts` first, the response includes the platform string. If it's not in the coverage list, tell the user this account isn't supported yet rather than calling and getting an empty response. 2. Default to last 30 days unless the user says otherwise. 3. Surface the period summary numbers first, then offer to dive into per-post analytics. ## Reference - API spec: `https://www.poster.ly/api/openapi` - Human docs: `https://www.poster.ly/reference` ## Skill: connect-posterly --- name: connect-posterly description: Walk a user through generating a posterly API key (Personal Access Token) and the API add-on subscription required to use it. license: proprietary --- # Connect to posterly Use when an agent needs posterly access for the first time, usually triggered by a `401` or `403` response from `/api/v1/*` with a `WWW-Authenticate` header pointing to the posterly token URI. ## The two-step gate posterly's API is gated behind: 1. A **Personal Access Token** (`pst_live_*`), minted at `https://www.poster.ly/dashboard/api`. 2. The **API add-on** (~$3/month), a Stripe subscription that activates the API for the user. The dashboard handles both in one screen: if the user doesn't have the add-on, they're prompted to subscribe before the "Generate API key" button enables. ## What the agent should do When the user wants posterly to do something but no token is configured: 1. Show a short explanation: "posterly requires a Personal Access Token. It's behind a $3/month API add-on." 2. Link them to `https://www.poster.ly/dashboard/api?upgrade=api_addon` (deep link to the upgrade flow). 3. Ask them to paste the token back to the agent. 4. Store the token securely (per the host agent's secret-storage convention). 5. Verify by calling `whoami`. ## Detecting the gate from API responses - `401 Unauthorized` + `WWW-Authenticate: Bearer ... error="invalid_token"` → no/bad token. Use `token_uri` from the header. - `403 Forbidden` + `WWW-Authenticate: Bearer ... error="insufficient_subscription"` → token is fine but no add-on. Use `error_uri` from the header, it points at the Stripe upgrade flow. - `403 Forbidden` + `WWW-Authenticate: Bearer ... error="insufficient_scope"` → token doesn't include the scope you need (e.g. `posts:write`). Tell the user to regenerate. ## Reference - Token & subscription: `https://www.poster.ly/dashboard/api` - Resource metadata: `https://www.poster.ly/.well-known/oauth-protected-resource` - Pricing: `https://www.poster.ly/dashboard/billing` ## Public guide: REST 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: ```bash 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](/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](/dashboard/api)**. 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](/docs/workspaces/teams-and-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](/dashboard/api)**. ### Node.js No SDK needed; the built-in `fetch` in Node 18+ covers everything: ```js const BASE_URL = "https://www.poster.ly/api/v1"; const headers = { Authorization: "Bearer POSTERLY_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`: ```python import requests BASE_URL = "https://www.poster.ly/api/v1" headers = {"Authorization": "Bearer POSTERLY_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 POSTERLY_API_KEY` and `Content-Type: application/json` - Data: ```json { "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: ```bash curl -X POST https://www.poster.ly/api/v1/webhooks \ -H "Authorization: Bearer POSTERLY_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`, name `Authorization`, value `Bearer POSTERLY_API_KEY` - Body Content Type: `JSON` - Body: ```json { "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=,v1=`) 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 POSTERLY_API_KEY` - Body type: `Raw`, Content type: `JSON (application/json)` - Request content: ```json { "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 accounts - `accounts:write`, disconnect connected social accounts - `posts:read` / `posts:write`, read and schedule posts - `media:write`, upload images and video - `analytics:read`, read per-account and per-post analytics - `billing:read`, read your AI credit balance and posterly subscription status - `billing: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](/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: 1. Call `POST /api/v1/connect/{platform}/sessions`. 2. Show or open the returned `connect_session.connect_url` for the user. 3. Poll `connect_session.poll_url`. 4. Narrate the returned `status_message` until the status becomes `connected`, `failed`, `cancelled`, or `expired`. 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: ```bash curl -H "Authorization: Bearer POSTERLY_API_KEY" \ https://www.poster.ly/api/v1/connect/instagram ``` ```json { "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: ```bash curl -X POST \ -H "Authorization: Bearer POSTERLY_API_KEY" \ -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 ``` ```json { "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. ```bash curl -X POST \ -H "Authorization: Bearer POSTERLY_API_KEY" \ -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 ``` ```json { "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: ```bash curl -X POST https://www.poster.ly/api/v1/oauth/clients \ -H "Authorization: Bearer POSTERLY_API_KEY" \ -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. ```bash 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). ```bash # 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](/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: ```json { "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: For accessibility, prefer the ordered `media_accessibility` array. It keeps a description attached by media position and, when available, stable asset ID: ```json { "media_accessibility": [ { "index": 0, "asset_id": "optional-asset-id", "source_url": "https://assets.poster.ly/image.png", "alt_text": "A chart showing quarterly growth." } ] } ``` The older `media_alt_texts` URL map is still accepted and returned for backwards compatibility. Posterly does not shorten the stored text; destination-specific limits are applied only to the copy sent during publishing. PATCH requests merge entries by `index` and preserve omitted descriptions. An empty string clears that occurrence and suppresses older URL-map text after reload. Send an empty `media_accessibility` array (or empty legacy map) to clear all descriptions. When both forms are supplied, stable entries win for matching occurrences; an empty stable array clears all. Repeated URLs have separate ordered descriptions; a legacy URL-map patch applies to every occurrence of that URL. - **Instagram**, feed, story, reel, carousel, collaborators, user tags, first comment, alt text, trial Reels, Reel covers, parent-container `is_ai_generated`, licensed Reel `audio_id` (Facebook Login / Meta-linked only), and optional companion Story posting - **Facebook**, post, story, reel, cover photo intent, colored text backgrounds, and Reel covers. Facebook Reels through the Graph Reels API must be 90 seconds or shorter (`validate_post` / `POST /api/v1/posts` reject over-limit Reels when duration is known). Use `post_type: "video"` for longer clips. - **YouTube**, title, thumbnail, privacy status, made-for-kids, tags, category, playlist, `notify_subscribers`, `add_shorts_hashtag`, and optional `brand_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) with `content_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), and `ai_generated` (AI-content label) - **Pinterest**, `post_type` (image/video/carousel), board (required via helper), title, destination link, `cover_image_url` (required for video Pins: a stored, probe-verified JPEG or PNG, 20 MiB maximum; aliases `video_cover_url` and `pinterest_cover_image_url`), `product_tags`, and `ai_generated`. Video Pins publish through Pinterest's registered upload and media ID, not a public `video_url` fetch. Image Pins and 2-5 image carousels keep the existing URL flow. - **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, GIPHY `gif_attachment`, media alt text, and `share_to_instagram` (accepted but not live: Meta has not approved `threads_share_to_instagram`, so production tokens cannot create the linked Instagram Story) - **Telegram**, native polls (`{ question, options }`), inline CTA buttons (`[{ text, url }]`), parse mode, video `cover`/`start_timestamp`, and `is_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_accessibility`, preferred; `media_alt_texts`, legacy). 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 `markdown` and custom `blocks`. 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`, and native `poll` (media plus poll allowed on Mastodon 4.6+). Text length (including URL counting), attachment count, types, sizes, and media descriptions follow the connected instance. The 1,500-character alt-text figure is only the conservative discovery fallback. Captions, content warnings, and alt text are validated rather than silently shortened. Retries stay idempotent within Mastodon's documented window; an ambiguous final create or upload response stops automatic retries, so a later retry is not assumed to be duplicate-safe. - **Dev.to**, markdown articles with `title` (falls back to the first caption line), `tags` (up to 4), and `canonical_url`. First image becomes the cover. - **Hashnode**, markdown articles to a Hashnode Pro publication with `title`, `tags` (up to 4), and optional `publication_id` override. 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 link `url`, and `nsfw`. 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)](/dashboard/docs/scheduling/instagram-trial-reels) 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`: ```json { "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` | Instagram | | The Instagram account must have its business account id. Reconnect the account in posterly if you see this. | `missing_instagram_business_account_id` | Instagram | 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`. ```json { "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. ```json { "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](/dashboard/feedback)**. This is for product requests and user-facing bug reports, not tool-failure telemetry. Requires: - Bearer API key with `posts:write` and active API add-on (browser session fallback is rejected) - `confirm: true` after the user explicitly approves the title and category - Rate limit: **5 submissions per day** per API key (`product_feedback_write`) ```json { "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](/api/openapi) (`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: ```json { "filename": "launch-photo.jpg", "content_type": "image/jpeg", "data": "" } ``` For larger files, first request a signed upload URL: ``` POST /api/v1/media/signed-upload ``` ```json { "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 ``` ```json { "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: ```json { "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/`) 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 ``` ```json { "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 (50 MB limit). Export it under 50 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`. Creating or editing an X post saves the post and reserves its posting allowance together. If the allowance is unavailable, the request leaves the saved post unchanged. Retrying a successful X create with the same `Idempotency-Key` returns the existing result without reserving the allowance again. ### 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). ```json { "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. ```json { "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). ```json { "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](/docs/ai-features/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. ```json { "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. ```bash curl -X POST https://www.poster.ly/api/v1/subscription/cancel \ -H "Authorization: Bearer POSTERLY_API_KEY" \ -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=,v1=`. ### Updates feed ``` GET /api/v1/updates ``` Returns the posterly updates feed (the same product news shown at [/updates](/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 to `false`. When `true`, each update includes its full markdown body in `content`. ```bash curl "https://www.poster.ly/api/v1/updates?limit=5&since=2026-07-01" \ -H "Authorization: Bearer POSTERLY_API_KEY" ``` 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](/cli)** is the terminal interface over the same `/api/v1` surface used by the REST API and MCP. Run it without installing anything globally: ```bash 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`: ```bash 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/posts` and `POST /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 hour - `POST /api/v1/support/chat`: 30 requests per hour per user - `POST /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](/reference) for complete endpoint documentation with request/response examples. ## Public guide: MCP Server posterly speaks the [Model Context Protocol](https://modelcontextprotocol.io) so any MCP-compatible AI client can list connected accounts, schedule posts, and pull analytics on the user's behalf. Two connection options are available, pick the one that fits your client. New accounts that start from [/mcp](/mcp) pay for a plan plus API & MCP in **one Stripe subscription**, charged immediately (Starter + API is $10/month). That path has no free trial. After checkout, create a key at [Dashboard → API & MCP](/dashboard/api). ## Two connection options ### 1. Stdio (npm package), desktop AI clients For Claude Desktop, ChatGPT Desktop, Cursor, Windsurf, Cline, and other clients that run an MCP server as a local subprocess. You do not need to install anything globally. Add this to your client's MCP config and let `npx` run the current server: ```json { "mcpServers": { "posterly": { "command": "npx", "args": ["-y", "posterly-mcp-server@latest"], "env": { "POSTERLY_API_KEY": "POSTERLY_API_KEY" } } } } ``` > **Quick setup, copy for your AI:** Paste this into Claude, ChatGPT, or Cursor and it will configure everything for you: > > *"I want to connect posterly via MCP. Add this to my MCP config: `{ "mcpServers": { "posterly": { "command": "npx", "args": ["-y", "posterly-mcp-server@latest"], "env": { "POSTERLY_API_KEY": "YOUR_KEY" } } } }`, merge it with my existing config, then remind me to restart."* > > Replace `YOUR_KEY` with your API key from [Dashboard → API & MCP](/dashboard/api). If the user has not signed up yet, install the same server without `POSTERLY_API_KEY`. The public tools `get_agent_signup_info`, `start_signup`, and `get_signup_session` work before a key exists, so the AI can start paid signup, send the posterly checkout handoff URL, and poll progress while the user pays and sets a password in the browser. ### 2. HTTP, browser-based AI clients For Claude in Chrome, ChatGPT (developer mode apps), Cursor in browser, Grok Bot, or any web-based or cloud agent that can't spawn a local process. Dedicated connect pages: [Claude](/claude), [ChatGPT](/chatgpt), [Cursor](/cursor), [Grok Bot](/grokbot), [Poke](/poke), [Hermes](/hermes), and [OpenClaw](/openclaw). First-schedule walkthroughs: [ChatGPT](/blog/how-to-use-posterly-with-chatgpt), [Claude](/blog/how-to-use-posterly-with-claude), [Cursor](/blog/how-to-use-posterly-with-cursor), [Poke](/blog/how-to-use-posterly-with-poke), [Hermes](/blog/how-to-use-posterly-with-hermes), [OpenClaw](/blog/how-to-use-posterly-with-openclaw), [Grok Bot Marketplace](/blog/how-to-use-posterly-with-grok-bot). Media bodies: [Solving the 4MB MCP limit](/blog/solving-the-4mb-mcp-limit). - **Endpoint**: `POST https://www.poster.ly/api/mcp` - **Wire format**: JSON-RPC 2.0 (single request, no SSE in v1) - **Auth header**: `Authorization: Bearer POSTERLY_API_KEY` - **Capability hint**: `GET /api/mcp` returns server info without auth. Quick smoke test from any terminal: ```bash curl -s https://www.poster.ly/api/mcp \ -H "Authorization: Bearer POSTERLY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` Both connection options use the same API key, the same paywall, and the same route-aware API limits. They expose the same authenticated toolset, but media upload ergonomics differ by transport because hosted HTTP MCP tool calls are JSON only. Create-post calls are limited to 100 requests per hour per key. This does not mean you can only schedule 100 posts per hour: use `create_posts_batch` to create up to 25 posts in one confirmed request. Media writes and read-only calls have separate higher limits. User-created API keys are tier-based: Starter 1, Pro 2, Power 3, Agency 4. Keys can also be scoped to one workspace; connection handoff and webhook tools use the scoped workspace automatically and reject a different `workspace_id`. ## Managed connection through Smithery [Smithery](https://smithery.ai/servers/awpthorp/posterly) provides a managed connection to the same hosted HTTP MCP endpoint. It uses posterly OAuth, so users authorize their own account without pasting a posterly API key into Smithery. 1. Open the [posterly server on Smithery](https://smithery.ai/servers/awpthorp/posterly). 2. Add or connect it to your Smithery toolbox. 3. Sign in to posterly, or create an account if needed. 4. Review the requested account, post, media, and analytics scopes. 5. Select **Allow**, then return to Smithery and test with `whoami` or `list_accounts`. Each OAuth connection belongs to the posterly user who approved it. Other Smithery users cannot access that user's accounts, posts, media, or analytics. An active posterly plan plus the API/MCP add-on is still required; Smithery does not provide free posterly API access. For permission explanations, example prompts, and troubleshooting, read the [Smithery social media MCP setup guide](/blog/smithery-social-media-mcp-server). ## Media upload: where the 4MB limit exists, and how to get past it Vercel caps **request bodies to posterly API routes** at about **4MB**. Hosted MCP (`POST /api/mcp`) is JSON on that same path, so a base64 file inside `upload_media` counts against it. Base64 also inflates the file, so hosted `upload_media` is only for small images. Our relay cap is 5MB decoded; treat anything larger as signed upload. This is **not** an MCP-protocol file limit, and it is **not** posterly's plan storage cap. It is the function body on Vercel. | Path | Hits the ~4MB Vercel body? | What to use it for | | --- | --- | --- | | Hosted MCP `upload_media` (base64 JSON to `/api/mcp`) | Yes | Small images only | | REST `POST /api/v1/media/upload` (base64) | Yes | Small images only | | Legacy `PUT /api/v1/media/signed-upload/upload` | Yes | Do not use. Returns `FUNCTION_PAYLOAD_TOO_LARGE` on large videos | | `upload_media_from_url` / `create_post` with a third-party `media_url` | The JSON call is small. posterly then **fetches** the URL on the server | Small public files. About 5MB on the relay path, 60MB remote-fetch hard cap. Large R2/S3 URLs fail with `remote_media_too_large` | | MCP `create_signed_upload`, then **PUT bytes to `upload_url`** | No. The PUT goes to object storage (R2/Supabase), not Vercel | Videos and large images. Plan caps: Starter 500MB, Pro 750MB, Power 1GB, Agency 4GB | | MCP `create_media_drop`, human opens `/drop/` | No. The browser PUTs to object storage | ChatGPT web and Claude.ai laptop files. Agent then calls `list_media` | | Stdio `upload_media` with `file_path` | No. The npm server reads the disk and uses signed upload for large files | Cursor, Claude Code, Claude Desktop, Codex, Hermes: give a real local path | | CLI `media:signed-upload` then PUT | No | Scripts and coding agents that can HTTP PUT | | Dashboard composer and Media Library | No. They already use signed / direct storage | Humans in the app | `create_signed_upload` is the bypass when the agent can PUT the file itself (know `filename`, `content_type`, and exact `size`, then PUT raw bytes to `upload_url` with the returned headers, then pass `public_url` to `create_post`). Cursor, Claude Code, and Codex can do that. ChatGPT web and Claude.ai cannot PUT a laptop file, and chat paperclips never reach MCP. For those clients call `create_media_drop`, send the user `drop_url` (`https://www.poster.ly/drop/`), wait, then call `list_media`. HEIC and PDF are rejected. Plan video caps still apply: Starter 500MB, Pro 750MB, Power 1GB, Agency 4GB. Why the 4MB number exists, and which tool to call on each runtime: [Solving the 4MB MCP limit](/blog/solving-the-4mb-mcp-limit). Local stdio MCP can read a local file path and automatically use signed upload for larger media. Use this when the agent is on the same machine as the file. Hosted HTTP MCP has no binary channel. For larger media on a client that **can** PUT: - Call `create_signed_upload` with `filename`, `content_type`, and `size`. - PUT the raw file bytes to the returned `upload_url` (object storage). Do not PUT to a `poster.ly` API route. - Pass `public_url` into `validate_post` / `create_post`. Do not pass a 500MB to 2GB Cloudflare R2, S3, or other remote URL straight to `create_post`. That fetch fails with `remote_media_too_large`. Download it (or PUT it) via signed upload instead. Uploaded media is analyzed automatically in the background. If a video can never reach a selected platform (for example, it is longer than the platform allows, or too large for the platform's file size limit), `create_post` rejects that combination with an actionable message such as "This video is too large for Telegram (50 MB multipart limit). Export it under 50 MB to include Telegram." Relay the message to the user. Incomplete or non-standard colour tags do not reject `create_post` or `validate_post`. `validate_post` / `dry_run: true` may include a `video_requires_color_safe_pipeline` warning. At publish time, posterly converts the video when the analysis allows, or publishes the original file. You do not need to re-export Rec.709 before including Facebook or Instagram. Compatible originals, remuxes, and video-copy repairs still pass. See [Video colour compatibility](/docs/creative-studio/media-library#video-colour-compatibility). `create_post` still rejects, at creation time, media combinations that no platform call could satisfy: a video that is too long or below a platform minimum, a PDF sent anywhere other than LinkedIn, an `.avi` or `.mkv` video sent to Instagram, and an Instagram account that is missing its business account id (which needs reconnecting in posterly). Relay those messages rather than retrying. The `.mov`, `.webm` and `.m4v` containers remain accepted at creation. Whether a repair is needed depends on the inspected input and destination; compatible originals can pass through unchanged. ## Agent response style MCP signup and connect tools return user-facing guidance by default. Agents should narrate progress in plain language, send the secure browser links, and avoid showing raw curl commands, HTTP payloads, or JSON unless the user asks for debugging. Use `debug: true` only when you deliberately need the raw signup or connect response for troubleshooting. The tools that support this are `start_signup`, `get_signup_session`, `get_connect_link`, `create_connect_session`, and `get_connect_session`. Post tools also return posterly dashboard links. After creating, listing, reading, or deleting posts, share the returned `View in posterly` link so the user lands in the right dashboard view. Current-month scheduled posts open in Calendar with the post selected; general/future post lists fall back to Table. ## Auto-discovery for agents posterly publishes the standard `.well-known` discovery endpoints so agents can find everything programmatically: | URL | Purpose | | --- | --- | | `/.well-known/mcp/server-card.json` | MCP server card, describes both connection options + tools | | `/.well-known/api-catalog` | RFC 9727 linkset describing the REST API surface | | `/.well-known/oauth-authorization-server` | RFC 8414, authorize/token/register endpoints, refresh-token grant | | `/.well-known/oauth-protected-resource` | RFC 9728, bearer methods, scopes, where to mint tokens | | `/.well-known/agent-skills/index.json` | Cloudflare Agent Skills index, links to SKILL.md files | | `/api/openapi` | Full OpenAPI 3.1 spec for the REST API | Public marketing pages also emit RFC 8288 `Link` headers pointing at all of the above, so agents that crawl `https://www.poster.ly` discover the API surface without any prior configuration. ## Auth & billing handoff (RFC 6750) When an agent calls a tool with a missing/invalid token or no API add-on, posterly returns a `WWW-Authenticate` header that tells the agent exactly what to do, surface this to the user and they can subscribe inside the agent conversation. - **401 invalid_token**, `WWW-Authenticate: Bearer ... token_uri="https://www.poster.ly/dashboard/api"` - **403 insufficient_subscription**, `WWW-Authenticate: Bearer ... error_uri="https://www.poster.ly/dashboard/api?upgrade=api_addon"` - **403 insufficient_scope**, `WWW-Authenticate: Bearer ... scope="posts:write"` - **403 origin_not_allowed**, browser caller from an Origin not on the trusted MCP host list (Claude, ChatGPT, posterly, localhost, plus `MCP_TRUSTED_ORIGINS`). - **429**, body includes `retry_after`, `limit`, `remaining`, and `reset`, plus a `Retry-After` header. ## Hosted MCP clients (Claude.ai, ChatGPT, and similar) Browser-hosted MCP clients can complete a full OAuth connect flow without manual allowlisting via RFC 7591 Dynamic Client Registration: - **`POST /api/oauth/register`**, send a JSON metadata document (`client_name`, `redirect_uris`, `scope`), receive a `client_id` immediately. - The new client starts the standard authorization-code + PKCE flow at `https://www.poster.ly/oauth/authorize`. - If you are new and still need a plan, posterly sends you to choose a plan and then returns you to the consent screen. The API add-on is still required before the access token activates. - The consent screen flags self-registered clients so the user can decide whether to trust the source. - **Access tokens** issued via OAuth live 1 hour. **Refresh tokens** live 30 days and rotate on every use; reuse revokes the chain. - `grant_type=refresh_token` at `/api/oauth/token` to renew. The discovery document at `/.well-known/oauth-authorization-server` advertises `registration_endpoint`, `token_endpoint`, and the supported grants so MCP hosts can wire this up automatically. API keys still work for stdio MCP and scripts. ## Available tools The local stdio server exposes 89 tools. The public setup tools work without `POSTERLY_API_KEY`; authenticated tools require a posterly API key. The HTTP MCP connection is authenticated and exposes 86 tools. - **get_mcp_status**, show the installed server version, latest npm version, MCP endpoint health, API auth health, and update guidance. - **get_agent_signup_info**, explain the safe agent-led signup flow before an API key exists. - **start_signup**, start paid signup and return a posterly checkout handoff URL plus signup poll URL. - **get_signup_session**, poll checkout, payment, password, and agent-access status. - **whoami**, show the user, workspaces, and scopes your key can access. - **create_api_key**, create a new scoped API key after explicit confirmation. The calling key must be a dashboard-created key, and new scopes cannot exceed the calling key. - **delete_api_key**, revoke a user-created API key after explicit confirmation. The endpoint refuses to revoke the key currently authenticating the request. - **list_accounts**, show all connected social accounts. - **disconnect_account**, disconnect a connected social account after explicit confirmation. It preserves reconnect metadata, archives Instagram analytics, and emits `account.disconnected` webhooks. - **get_connect_link**, inspect dashboard connection links, provider readiness, required scopes, and connected account counts. - **connect_account**, connect a credential-based account (telegram, bluesky, discord, wordpress, devto, hashnode, lemmy) directly, no browser session needed. - **create_connect_session**, create a guided browser handoff for connecting a social account. - **get_connect_session**, poll connection progress while the user logs in, approves OAuth, or enters credentials. - **list_oauth_clients / create_oauth_client / update_oauth_client / delete_oauth_client**, manage self-serve public OAuth 2.1 + PKCE developer clients after confirmation. - **list_platforms** / **get_platform_schema**, inspect supported integrations, post types, settings fields, media limits, analytics support, and planned provider possibilities. - **trigger_platform_helper**, run helper discovery such as `pinterest.boards`, `youtube.playlists`, `tiktok.creator_info`, `linkedin.recent_mentions`, or `x.quota`. - **list_brands**, show brands/clients available in the current workspace context. - **get_brand**, inspect one brand/client in detail. - **list_brand_accounts**, show the social accounts assigned to a brand/client. - **get_brand_profile**, read the saved brand voice/profile for a brand/client. Also returns `learned_voice`: the voice posterly has learned from each connected account's real published captions. - **get_learned_voice**, read the voice learned from one account's real published captions (the "learned captions" shown on the dashboard), keyed by social account ID. Returns a narrative summary, voice traits, style guidelines, and observed posting habits. Read-only; never overwrites the saved brand profile. - **list_posts** / **get_post** / **get_post_missing**, browse scheduled content and inspect missing content/settings before publish. - **ask_support**, ask authenticated docs-backed support questions with read-only account/post diagnostics. If the user asks for a human, agent, real person, or an escalation, Support AI files a ticket. Agent-initiated handoff still uses `request_human` plus `confirm_escalation`. - **create_post**, schedule or immediately publish one post. Pass `thread_posts: string[]` (2+ entries) for X or Threads chains, and `platform_settings` for platform-specific composer controls. For media, pass URLs returned by the media upload tools. - **validate_post**, validate the complete payload without creating a post, copying remote media, consuming post-item/storage quota, reserving X quota, emitting webhooks, or enqueueing publication. Resolve warnings, show the final preview, and ask for explicit confirmation before calling `create_post`. - **submit_agent_feedback**, submit bounded private operational telemetry after a concrete workflow outcome. Never include secrets, prompts, captions, media URLs, personal data, or fabricated outcomes. - **submit_product_feedback**, file a product bug, idea, or feedback on the public posterly feedback board after the user explicitly approves the title and category (`confirm=true`). Do not use this for tool failures; use `submit_agent_feedback` for private operational telemetry. - **create_posts_batch**, create up to 25 confirmed posts in one API request. Each item accepts the same fields as `create_post`, and partial failures return the item index. Third-party media URLs are copied into posterly storage before each post is saved. - **update_post** / **update_post_status** / **update_post_release_id** / **delete_post** / **delete_post_group**, manage scheduled content, repair release/group metadata, and clean up grouped drafts/scheduled posts after confirmation. - **upload_media**, small base64 files on hosted HTTP (Vercel ~4MB body). Stdio `file_path` uses signed upload for large files. - **upload_media_from_url**, fetch a public image/video URL into posterly storage with SSRF protections (small files; large R2/S3 URLs fail). - **create_signed_upload**, mint an object-storage PUT URL so large files never enter the Vercel body. The client must PUT the bytes. - **create_media_drop**, mint a no-login `https://www.poster.ly/drop/` page so a human can upload a laptop file. Chat paperclips still never reach MCP. - **list_media**, list newest media assets, or files from one drop session, then pass `public_url` to `validate_post` / `create_post`. - **find_available_slot**, find the next open posting slot honoring the 1h gap. - **generate_captions**, generate or adapt brand-aware caption suggestions without creating a post. Captions are checked against the brief and the workspace's [business facts](/docs/ai-features/business-facts); unverifiable prices, dates, times, links, phone numbers and handles are removed, as is any never-say phrase set on a fact, so fewer variants than requested may come back. - **generate_image**, create AI images with Nano Banana. - **get_video_options**, inspect read-only Veo video models, durations, input modes, and credit-cost estimates. - **run_video_function**, run read-only Veo helpers for cost estimation and request validation. - **generate_video**, queue a cost-guarded Veo video job after user confirmation. - **get_credits**, read the AI credit balance for the caller's workspace wallet. Read-only, spends nothing. - **get_video_job**, poll one Veo job or list recent video jobs until `video_url` is ready. - **get_account_analytics**, follower growth, reach, views, engagement rate for social platforms, and API-provided native `display_metrics` such as Google Business Profile's Profile Views, Search Views, Maps Views, Customer Actions, and Posts (Instagram, Facebook Pages, LinkedIn, Google Business Profile, Pinterest, YouTube, and Threads). Accepts `presentation: "compact" | "table" | "json"` so Telegram can stay bullet-based while Markdown clients or custom renderers can choose tables/structured data. - **get_post_analytics**, per-post likes, comments, reach, impressions, saves, shares, plays. Also accepts `presentation: "compact" | "table" | "json"`. - **get_performance_profile**, read the read-only performance profile posterly derives for one account from the last 90 days of per-post analytics (top formats, timing, caption-length patterns, an engagement-rate trend, and a narrative summary), keyed by social account ID. Requires a Pro plan or higher. - **get_post_insights**, list per-post feedback-loop insights for recently published posts: the performance tier (great/good/mixed/poor), a diagnosis, a suggested next action, metrics, and the baseline each was compared against. Filter by `account_id`, `post_id`, and `checkpoint` (1h, 6h, 24h, 72h, 7d). Requires a Pro plan or higher. - **list_post_suggestions**, list evidence-based weekly post drafts written in each account's learned voice, each with a `rationale` tying it back to what has performed well. Filter by `account_id` and `status` (pending, scheduled, dismissed). Requires a Pro plan or higher. - **dismiss_suggestion**, dismiss a post suggestion after user confirmation. Never dismisses a suggestion that was already turned into a scheduled post. - **list_conversations**, list Instagram and Facebook Page inbox DMs. Threads has no DMs. Requires a Pro plan or higher. - **get_conversation**, get one inbox conversation and its messages. - **send_message**, send a DM reply after explicit confirmation. Honors Meta 24-hour messaging windows. - **list_comments**, list Instagram, Facebook Page, and Threads inbox comments. - **get_comment**, get one inbox comment and its replies. - **reply_to_comment**, reply to a comment after explicit confirmation. Facebook has no private comment replies; Threads is public-only. - **update_comment**, hide/unhide a comment or mark it read. Hide is reversible and does not delete. - **delete_comment**, delete an Instagram or Facebook Page inbox comment after confirmation. Irreversible. Admin only. Threads replies cannot be deleted; hide them with update_comment. - **sync_inbox**, pull the latest inbox data for one Instagram, Facebook, or Threads account, with a 30-second cooldown. - **list_google_business_reviews**, list GBP reviews for one location or every accessible location. Returns every review Google holds (it pages past Google's own 50-per-page cap), with the full untruncated review text and its date. Filter by `rating` and `unanswered`, and page with `limit` (default 200, max 1000) and `offset`; `has_more` tells you when more remain. Each location also reports its `total_review_count` and `average_rating` from Google, plus an `error` field, so a location with no reviews is distinguishable from one that failed to sync. - **get_google_business_review_link / audit_google_business_profile**, generate review request links and run local-profile audits. - **suggest_google_business_review_reply**, generate brand-aware reply options without posting. - **reply_google_business_review / delete_google_business_review_reply**, manage public GBP review replies after explicit confirmation. - **list_google_business_media**, list the photos and videos on a GBP profile gallery (the media shown on Maps and Search). - **add_google_business_media / delete_google_business_media**, add a photo/video to the profile gallery from a public URL, or remove one, after explicit confirmation. > Google Business tip: pass `account_id` (the integer `id` from `list_accounts`) to every GBP tool. If you use `location_id` instead, it is the numeric `location_id` from `list_accounts` (the GBP location id), not the `ChIJ...` Place ID that `get_google_business_review_link` returns. Passing the Place ID resolves to no account and returns "Google Business Profile account not found". - **list_activity**, read the agent notifications feed of post activity, publish attempts, failures, and retries. - **get_updates**, get the latest posterly product updates and news from the updates feed. Requires an active posterly subscription. - **list_webhooks**, inspect webhook subscriptions and delivery status. - **create_webhook** / **update_webhook** / **delete_webhook** / **test_webhook**, manage signed webhook subscriptions after explicit confirmation. - **get_x_posting_quota**, inspect workspace X posting allowance and URL-block status. - **get_subscription**, read the user's posterly subscription status, tier, cancel/pause state, and period end. Requires the `billing:read` scope. - **cancel_subscription**, cancel the subscription after explicit confirmation. The agent must ask the user for a cancellation `reason` first (a fixed catalog value) and pass `confirm: true`; `immediate` defaults to cancel-at-period-end. Requires the `billing:write` scope. - **pause_subscription**, pause the subscription for 30 days (one pause per 90-day cooldown) after `confirm: true`. Requires the `billing:write` scope. - **resume_subscription**, resume a paused subscription. Requires the `billing:write` scope. - **downgrade_subscription**, downgrade one tier (or to an explicit lower `tier`) at the next renewal after `confirm: true`. Requires the `billing:write` scope. > Subscription management runs on the same API key but does not require the paid API add-on, so a paused or cancel-pending user can still resume or manage their plan. The destructive actions (`cancel_subscription`, `pause_subscription`, `downgrade_subscription`) require `confirm: true` after explicit user confirmation, and `cancel_subscription` also requires a `reason`. Reactivating a pending cancellation, restarting a lapsed plan, and retention offers stay in the dashboard. ## Usage examples In Claude, ChatGPT, or any MCP client: - "Schedule a LinkedIn post about our Q1 results for tomorrow at 10am" - "What accounts do I have connected to posterly?" - "Disconnect the old Instagram account after I confirm the exact account ID" - "Pause post 971 after I confirm it is the right scheduled post" - "Upload this image and create an Instagram post with it" - "Schedule this as an Instagram Reel, add @partner as a collaborator, and use this image as the Reel cover" - "Schedule this YouTube video as unlisted, add these tags, and put it in our launch playlist" - "Post this TikTok with comments on, duet off, stitch off, and public privacy" - "Turn this Google Business update into an event post with a Learn more CTA" - "How did my Instagram perform over the last 14 days?" - "Which of my recent LinkedIn posts got the most engagement?" - "Post this 5-tweet thread on my X account at 3pm tomorrow" (uses `thread_posts` for X / Threads chains) `platform_settings` covers the same scheduling controls exposed by the composer: Instagram feed/story/reel/carousel options plus parent-container `is_ai_generated` and licensed Reel `audio_id` (Facebook Login / Meta-linked only), Facebook stories/reels/backgrounds, YouTube thumbnails/privacy plus `notify_subscribers`, `add_shorts_hashtag`, and optional `brand_partner`, LinkedIn document, mention, poll, and `content_call_to_action_label` settings (including BUY_NOW and SHOP_NOW), TikTok video and photo slideshow controls (including `video_cover_timestamp_ms` to pick the cover frame on direct-post videos), Pinterest board/link/title/`product_tags` plus a required stored JPEG/PNG `cover_image_url` for video Pins (20 MiB maximum; registered upload, not a public `video_url` fetch), Google Business event/offer/CTA/recurrence fields, X polls, reply settings (incl. `subscribers`/`verified`), and `paid_partnership`, Threads reply controls, polls, topic tags, text attachments, ghost posts, spoilers, reply approvals, GIPHY, and `share_to_instagram`, Telegram polls, inline CTA buttons, video cover, and live photo, Slack markdown/blocks, Mastodon visibility/quote/poll (media plus poll allowed on 4.6+; text, media, and alt-text limits follow the instance; ambiguous final creates stop automatic retries), and Bluesky language, alt-text, content warnings, hidden tags, quote posts, and disable-quotes settings. Pass `ai_generated: true` to label a post as AI-generated on TikTok, X, YouTube, Pinterest, and Instagram. For media descriptions, prefer `media_accessibility: [{ index, asset_id?, source_url?, alt_text }]`. The legacy `media_alt_texts` URL map remains accepted. The ordered format keeps alt text with the original media item through carousel reordering and platform-specific image preparation. Updates merge entries by index and preserve omitted descriptions. An empty string clears that occurrence, while an empty array (or empty legacy map) clears all descriptions. Stable entries take precedence when both forms are supplied. For TikTok you do not need to set a post type for images. posterly auto-detects image media and schedules it as a photo slideshow of 1 to 35 images (sending every image), so "Schedule these 5 photos as a TikTok" just works. A single video schedules as a standard TikTok video. See the [MCP marketing page](/mcp) for client-specific setup screenshots. ## Claude Code plugin alternative If you use Claude Code, you can install the official posterly plugin instead of editing MCP config by hand. The plugin bundles this MCP server plus the agent skill and a guided `/posterly:setup` flow. ``` /plugin marketplace add awpthorp/posterly-agent /plugin install posterly@posterly-agent ``` Full steps: [Claude Code plugin](/docs/integrations/claude-plugin). ## Public guide: CLI The posterly CLI (`@posterly/cli` on npm) gives you terminal access to the same public API that powers the [REST API](/docs/integrations/api) and [MCP](/docs/integrations/mcp) integrations. Use it for scripts, CI jobs, support playbooks, local debugging, and AI coding agents that can run shell commands. ## Quick start No install needed: ```bash npx -y @posterly/cli@latest auth:login npx -y @posterly/cli@latest doctor --pretty npx -y @posterly/cli@latest accounts:list --pretty ``` Or install globally for a permanent `posterly` command: ```bash npm i -g @posterly/cli@latest posterly doctor --pretty ``` ## Requirements - Node.js 20 or newer - A posterly account with the API add-on ($3/month on any plan) - An API key from [Dashboard -> Settings -> API Keys](https://www.poster.ly/dashboard/api), or browser login via `auth:login` ## Authentication Pick whichever fits your environment: - **`posterly auth:login`**: opens your browser for OAuth (PKCE). Best for humans on their own machine. - **`posterly auth:key`**: paste an API key into a hidden prompt. It is stored in `~/.posterly/config.json` with restrictive permissions. - **`POSTERLY_API_KEY` environment variable**: best for CI, scripts, and agents. Nothing is written to disk. Check your setup any time: ```bash posterly doctor --pretty ``` Doctor verifies your Node version, the API origin, that a key is configured, that the key works against `/api/v1/whoami`, and whether a newer CLI version is published. It exits nonzero if anything fails, so you can use it as a CI preflight step. ## Output is JSON by default Every command prints JSON to stdout, so output pipes cleanly into `jq`, CI logs, or agent tooling. Add `--pretty` when a human is reading. ```bash posterly posts:list --status scheduled | jq '.posts[].caption' ``` ## Command reference | Area | Commands | | --- | --- | | Auth | `auth:login`, `auth:key`, `auth:status`, `auth:logout` | | Health | `doctor`, `version`, `whoami` | | Accounts | `accounts:list`, `accounts:disconnect`, `connect:link` | | Brands | `brands:list`, `brands:get`, `brands:accounts`, `brands:profile` | | Platforms | `platforms:list`, `platforms:schema`, `platforms:trigger` | | Posts | `posts:list`, `posts:get`, `posts:create`, `posts:update`, `posts:status`, `posts:missing`, `posts:release-id`, `posts:delete`, `posts:delete-group`, `posts:batch` | | Scheduling | `slots:next` | | Media | `media:upload`, `media:signed-upload`, `media:upload-from-url` | | Analytics | `analytics:account`, `analytics:posts` | | Google Business | `gbp:reviews`, `gbp:review-link`, `gbp:audit`, `gbp:media`, `gbp:add-media`, `gbp:delete-media`, `gbp:suggest-reply`, `gbp:reply`, `gbp:delete-reply` | | AI | `ai:captions`, `ai:image`, `ai:video-options`, `ai:video-function`, `ai:generate-video`, `ai:video-jobs`, `ai:video-job` | | Operations | `activity:list`, `notifications:list`, `x:quota` | | Webhooks | `webhooks:list`, `webhooks:create`, `webhooks:update`, `webhooks:delete`, `webhooks:test` | | OAuth clients | `oauth:clients`, `oauth:create-client`, `oauth:update-client`, `oauth:delete-client` | Run `posterly help` for flags, or see the [API reference](/reference) for full request and response details; every command maps 1:1 to a `/api/v1` route. ## Common workflows Schedule a post: ```bash posterly posts:create \ --account-id \ --caption "Launching soon" \ --scheduled-at 2026-07-10T09:00:00Z ``` Find open posting slots, then schedule into one: ```bash posterly slots:next --count 3 --timezone America/New_York --pretty ``` Generate AI captions before drafting: ```bash posterly ai:captions --platforms instagram,linkedin --brief "Announcing our summer sale" --tone playful --count 3 --pretty ``` Generate an AI image for a post: ```bash posterly ai:image "A minimal flat-lay of a desk with coffee" --aspect-ratio 1:1 --style photographic --pretty ``` Upload a small image (this still goes through the API as JSON, so the Vercel ~4MB body limit applies): ```bash posterly media:upload ./photo.jpg posterly posts:create --account-id --caption "New drop" --media-url ``` For video or anything that would exceed that body, mint a signed upload then PUT the file to object storage (not to a posterly API route): ```bash posterly media:signed-upload launch.mp4 --content-type video/mp4 --size 28400000 --pretty # PUT the raw bytes to the returned upload_url with the returned headers # Then pass public_url as --media-url on posts:create ``` Stdio MCP `upload_media` with `file_path` does the same bypass automatically. Dashboard uploads never go through the 4MB body. See [MCP media limits](/docs/integrations/mcp#media-upload-where-the-4mb-limit-exists-and-how-to-get-past-it). Catch and inspect failures: ```bash posterly posts:list --status failed --pretty posterly posts:missing --pretty ``` ## Safety guardrails - Destructive commands (`posts:delete`, `posts:delete-group`, `accounts:disconnect`, `webhooks:delete`, `gbp:reply`, `gbp:add-media`, `gbp:delete-media`, and others) refuse to run without an explicit `--confirm` flag. - The CLI refuses to send API keys to non-local `http://` origins. - AI commands that spend credits (`ai:image`, `ai:generate-video`) call cost-guarded endpoints; use `ai:video-function estimate_cost` to preview video costs first. - Keys are read from a hidden prompt or environment variable so they stay out of shell history. ## Using the CLI with AI agents The CLI is the easiest way to give a terminal-capable coding agent (Claude Code, OpenClaw, Cursor, and similar) full posterly access: JSON output, no MCP configuration, and built-in confirmation gates on destructive actions. ### Claude Code plugin (recommended for Claude Code) Install the official plugin so Claude Code gets the posterly skill, bundled MCP server, and guided setup: ``` /plugin marketplace add awpthorp/posterly-agent /plugin install posterly@posterly-agent ``` Then set your API key in the plugin userConfig (or export `POSTERLY_API_KEY`) and run `npx -y @posterly/cli@latest doctor --pretty`. See [Claude Code plugin](/docs/integrations/claude-plugin) for the full walkthrough. ### Skills CLI (any skill-compatible agent) Install the posterly agent skill, which teaches agents the CLI, MCP, and REST interfaces together: ```bash npx skills add awpthorp/posterly-agent ``` Then set `POSTERLY_API_KEY` in the agent's environment. See the [agents overview](https://www.poster.ly/agents) and [MCP setup](/docs/integrations/mcp) for the MCP-based alternative. ## CI usage ```yaml # GitHub Actions example - name: Schedule release post env: POSTERLY_API_KEY: ${{ secrets.POSTERLY_API_KEY }} run: | npx -y @posterly/cli@latest doctor --no-npm npx -y @posterly/cli@latest posts:create \ --account-id "$ACCOUNT_ID" \ --caption "v2.0 is live" \ --scheduled-at "$PUBLISH_AT" ``` Rate limits and scopes match the REST API: see [API documentation](/docs/integrations/api) for details. ## Public guide: Agent discovery posterly exposes a complete public discovery graph so an AI agent can understand the product before asking a user to configure credentials. ## 1. Discover posterly Start with one of these public resources: - [`llms.txt`](https://www.poster.ly/llms.txt), concise product and link index - [`llms-full.txt`](https://www.poster.ly/llms-full.txt), deterministic agent reference - [OpenAPI 3.1](https://www.poster.ly/api/openapi), REST request and response schemas - [API reference](https://www.poster.ly/reference), interactive API documentation - [MCP server card](https://www.poster.ly/.well-known/mcp/server-card.json), hosted and stdio transport metadata - [Agent skills index](https://www.poster.ly/.well-known/agent-skills/index.json), task-oriented instructions - [MCP setup](https://www.poster.ly/mcp), client configuration - [Smithery listing](https://smithery.ai/servers/awpthorp/posterly), managed hosted MCP connection with posterly OAuth Do not scrape the dashboard to infer capabilities. Use `list_platforms`, `get_platform_schema`, or the OpenAPI contract for the current platform rules. ## 2. Start paid agent access API and MCP access require a paid posterly plan plus the API add-on. Send the user to [agent signup](https://www.poster.ly/agents/signup), or use the public MCP signup tools before an API key exists. The signup flow never returns an API key before billing is active. The user completes checkout, password setup, and any provider authorization in their browser. ## 3. Authenticate and confirm identity After the user supplies a dashboard-created API key or completes an approved OAuth handoff: 1. call `whoami`; 2. inspect the returned workspaces and scopes; 3. call `list_accounts`; 4. ask which account and workspace the user intends to operate. Never print, log, or place an API key in a post payload. ## 4. Connect a social account when needed If the required account is absent, create a connection session for the chosen platform. Give the user the returned `connect_url` and poll the session. Provider OAuth and credentials stay in the user's browser; the agent receives bounded status updates. Credential-based platforms (telegram, bluesky, discord, wordpress, devto, hashnode, lemmy) can also be connected headlessly: call `GET /api/v1/connect/{platform}` (or MCP `get_connect_link`) to discover the required `credential_fields`, then submit them with `POST /api/v1/connect/{platform}/credentials` (or MCP `connect_account`). Prefer scoped secrets such as app passwords, bot tokens, webhook URLs, and API tokens over primary passwords, and tell the user that any credential they share passes through the conversation. ## 5. Get media into posterly storage Chat attachments never become MCP arguments. Pick a path that matches the client: - **Small public URL:** `upload_media_from_url`, or pass `media_url` on validate/create. Server fetch caps: about 5MB relay, 60MB hard cap. Large R2/S3 URLs fail with `remote_media_too_large`. - **Small file as JSON:** hosted `upload_media` (base64). This hits the **Vercel ~4MB request body** on `/api/mcp` and `/api/v1/media/upload`. Use it for small images only. - **Large local file on a machine the agent can read:** stdio `upload_media` with `file_path`, or `create_signed_upload` then PUT the bytes to `upload_url`. That PUT goes to object storage and **does not** hit the 4MB Vercel body. Plan video caps are Starter 500MB, Pro 750MB, Power 1GB, Agency 4GB. - **ChatGPT web / Claude.ai:** chat paperclips never reach MCP, and the model cannot PUT a laptop file. Call `create_media_drop`, send the user `drop_url` (`https://www.poster.ly/drop/`, no dashboard login), wait, then `list_media` (optionally with `drop_session_id`), then validate/create. HEIC and PDF are rejected. Plan video caps still apply. This is not `create_signed_upload`. Codex, Claude Code, and Cursor can still use `file_path` or `create_signed_upload` then PUT. Then gather the final account, caption, schedule, media URL from the step above, post type, platform settings, and workspace. Then use either: - MCP `validate_post`; or - `POST /api/v1/posts` with `"dry_run": true`. Dry-run performs the same normalization and platform validation as live create, but it does not create a post, copy remote media, consume post-item or storage quota, reserve X quota, emit webhooks, or enqueue publishing. Third-party media can produce `remote_media_not_materialized`. This means the URL was checked but not copied; live create still performs storage copy and final byte validation. ## 6. Show the preview and obtain confirmation Resolve validation errors and warnings. Show the user: - account and platform; - complete final caption or thread; - scheduled time in the user's timezone; - attached media; - platform settings; - target workspace. Obtain explicit confirmation for that exact post. Validation is not permission to publish. ## 7. Schedule the live post After confirmation, call MCP `create_post` with `confirm: true`, or make the live REST request with `dry_run` omitted. Reuse the validated payload. Return the post ID, status, schedule, and posterly dashboard link. ## 8. Report product issues or private telemetry (optional) Keep these two paths separate: - **Public product board:** when the human asks to report a bug or suggest a feature, call MCP `submit_product_feedback` or `POST /api/v1/feedback` with `confirm: true` after they approve the title and category. This lands on the same board as the dashboard Feedback page (5 submissions/day per API key). - **Private ops telemetry:** after a concrete tool success, error, or abandoned workflow, call MCP `submit_agent_feedback` or `POST /api/v1/agent-feedback`. Do not use this for roadmap ideas, and never put secrets, prompts, captions, media URLs, or personal data in the payload. Schemas and examples live in the [OpenAPI](/api/openapi) document (`ProductFeedbackRequest`, `AgentFeedbackRequest`). For complete field documentation, continue with the [REST API](/docs/integrations/api), [MCP](/docs/integrations/mcp), or [CLI](/docs/integrations/cli) guide.