# 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 - 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}/sessions — Create an account connect session - GET /api/v1/connect/sessions/{session_id} — Get account connect session status - 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 - 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 ``` ## 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 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. Keys created from the dashboard include the `billing` scopes so a connected AI tool can manage your own subscription; 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 before returning data or creating resources. ## 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}/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 } } ``` ### 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"] }' ``` ### 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. **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: - **Instagram**, feed, story, reel, carousel, collaborators, user tags, first comment, alt text, trial Reels, Reel covers, and optional companion Story posting - **Facebook**, post, story, reel, cover photo intent, colored text backgrounds, and Reel covers - **YouTube**, title, thumbnail, privacy status, made-for-kids, tags, category, playlist, `notify_subscribers`, and `add_shorts_hashtag` - **LinkedIn**, document title and filename, organization mentions, polls (question, 2-4 options, 1 day/3 days/1 week/2 weeks), video thumbnail, and alt text. 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` (for video Pins), and `ai_generated` - **Google Business Profile**, standard, event, and offer posts with event schedule, offer details, CTA, and video - **X**, reply settings (`everyone`/`following`/`mentionedUsers`/`subscribers`/`verified`), polls, `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, media alt text, and `share_to_instagram: true` to also share the published post to the Instagram account linked to the Threads profile as a Story (requires Instagram sharing enabled for the Threads profile in the Threads app) - **Telegram**, native polls (`{ question, options }`), inline CTA buttons (`[{ text, url }]`), and parse mode - **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 (10 MB each), and media alt text (`media_alt_texts`). Mention parsing is disabled so captions never ping @everyone or roles. - **Mastodon**, statuses on any instance with `visibility` (public/unlisted/private), content warnings (`spoiler_text`), `language`, and media alt text. Publishes are idempotent. - **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 your 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). **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`. ### Upload media ``` POST /api/v1/media/upload ``` Upload an image or video to attach to a post. This endpoint accepts base64 data and is best for small files up to 5MB decoded: ```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 PUT /api/v1/media/signed-upload/upload?token=... ``` ```json { "filename": "launch-video.mp4", "content_type": "video/mp4", "size": 28400000 } ``` Upload the raw file bytes to the returned `upload_url` with `PUT`. The upload request must use the same content type and stay within the signed size limit. Signed upload tokens expire after 15 minutes. After upload, 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. Images can be up to 10MB; videos can be up to 50MB. 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. #### 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 4m 20s but X allows up to 2m 20s. Trim it to include X. | | `video_exceeds_platform_limit` | The video is too large for the platform's file size limit | This video is too large for Telegram (20 MB limit). Export it under 20 MB to include Telegram. | Media that has not been analyzed yet (or was uploaded before this feature) is accepted as before, and anything posterly can adapt automatically is handled without any error. ### Find next available slot ``` GET /api/v1/slots/next ``` Find the next available posting slot for a given account. ### X posting quota ``` GET /api/v1/x-posting/quota ``` Returns the current workspace's managed X posting allowance, used and remaining posts, current quota period, URL-block status, active add-on details, and available X quota plans. This requires `posts:read`. ### Account analytics ``` GET /api/v1/analytics/accounts?account_id={id}&from=YYYY-MM-DD&to=YYYY-MM-DD ``` Daily snapshots plus a period summary. Social platforms include follower delta, reach, views, and engagement rate; platforms with native dashboards include `display_metrics` so clients can render exact labels such as Google Business Profile's Profile Views, Search Views, Maps Views, Customer Actions, and Posts. Defaults to the last 30 days. Supported platforms: Instagram, Facebook Pages, LinkedIn, Google Business Profile, Pinterest, YouTube, and Threads. Requires `analytics:read`. ### Post analytics ``` GET /api/v1/analytics/posts?account_id={id}&from=YYYY-MM-DD&to=YYYY-MM-DD&limit=50&offset=0 ``` Per-post engagement rows (likes, comments, reach, impressions, saves, shares, plays) most-recent-first. `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" } ] } ``` ### 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 use the AI Caption Assist allowance. **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`, uses the AI Caption Assist allowance, and returns suggestions only; it does not create a post. `/ai/video-options` is a read-only Veo 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 Veo helper functions such as `estimate_cost` and `validate_request` before generation. It does not generate video or spend credits. `/ai/generate-video` queues a cost-guarded Veo job and returns `202 Accepted` with a `job_id`. It can spend included or purchased Veo credits, so show the user the prompt, model, 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.", "aspect_ratio": "9:16", "duration_seconds": 8, "resolution": "720p", "model": "fast", "generate_audio": true } ``` `/ai/generate-image` creates Nano Banana images and returns media URLs you can pass to `media_url` or `media_urls` when creating posts. ### 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 - 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. ## 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 Atlas, Cursor in browser, or any web-based agent that can't spawn a local process. - **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 limits by transport Local stdio MCP can read a local file path and automatically use signed upload for larger media. Use this option when the agent is running on the same machine as the file and needs to upload videos or larger images. Hosted HTTP MCP and WebMCP do not have a native binary/file channel. Their `upload_media` tool accepts base64 JSON and is intended for small files up to 5MB decoded. For larger media, use one of these routes: - Use `upload_media_from_url` when the asset already has a public direct image/video URL that allows server-side GET requests. - Use `create_signed_upload` only from clients that can also `PUT` the raw file bytes to the returned `upload_url`; then pass the returned `public_url` to `create_post`. - Use the REST API, CLI, dashboard composer, or local stdio MCP when the agent only has a local file and cannot perform that raw-byte PUT itself. 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 (20 MB limit). Export it under 20 MB to include Telegram." Relay the message to the user; media posterly can adapt automatically is never rejected. `create_post` also rejects, at creation time, media combinations that no platform call could satisfy: 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). These used to be accepted and then fail at publish. Relay the message rather than retrying. Consistent with the paragraph above, video containers posterly converts for itself are never rejected: `.mov`, `.webm` and `.m4v` are normalised to MP4 before an Instagram post publishes, so pass them straight through. ## 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-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"` - **429**, body includes `retry_after`, `limit`, `remaining`, and `reset`, plus a `Retry-After` header. ## Available tools The local stdio server exposes 75 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 72 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. - **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. Human handoff requires explicit confirmation. - **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. - **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**, upload an image or video. - **upload_media_from_url**, fetch a public image/video URL into posterly storage with SSRF protections. - **create_signed_upload**, get a direct upload URL for larger media. - **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. - **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_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_google_business_reviews / get_google_business_review_link / audit_google_business_profile**, inspect GBP reviews, 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, Facebook stories/reels/backgrounds, YouTube thumbnails/privacy/playlists plus `notify_subscribers` and `add_shorts_hashtag`, LinkedIn document, mention, and poll settings, TikTok video and photo slideshow controls (including `video_cover_timestamp_ms` to pick the cover frame on direct-post videos), Pinterest board/link/title fields, Google Business event/offer/CTA fields, X polls and reply settings (incl. `subscribers`/`verified`), Threads reply controls, polls, topic tags, text attachments, and `share_to_instagram` (also share the published Threads post to the linked Instagram account as a Story), Telegram polls and inline CTA buttons, 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, and Pinterest. 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 media and use it in a post: ```bash posterly media:upload ./photo.jpg posterly posts:create --account-id --caption "New drop" --media-url ``` 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. ## 5. Build and validate the complete post Gather the final account, caption, schedule, media, 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. For complete field documentation, continue with the [REST API](/docs/integrations/api), [MCP](/docs/integrations/mcp), or [CLI](/docs/integrations/cli) guide.