# SocialAPI > SocialAPI is a unified social media API and MCP server: one REST API to read and answer comments, DMs, reviews, and mentions, run Automations, and publish posts across the major social platforms. Support differs per platform. > ## Agent Instructions > Base URL: https://api.social-api.ai/v1. Send your API key in the Authorization header: Authorization: Bearer . > Every error response from /v1 is JSON: { "error": { "code", "message", "meta" (optional) }, "request_id" }. Branch on error.code; use error.message only for logs. > A 429 response carries a Retry-After header, in seconds, when the limit has a known wait. Authenticated responses can carry X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset for the per-minute request budget your SocialAPI account shares across every API key (see /guides/rate-limits). > Paginated list endpoints return next_cursor, at the top level or inside pagination (see /guides/pagination). Pass it as the cursor query parameter to get the next page. An absent or empty next_cursor means you have the last page. > Treat every ID as an opaque string: do not parse it or assume a format. > Support differs by platform and by operation. Check /platforms/overview before assuming an operation works on a platform. An unsupported operation returns 501 with error.code resource.not_supported. # Connect a social account Source: https://docs.social-api.ai/api-reference/accounts/connect-a-social-account /api-reference/openapi.json post /accounts/connect Initiates account connection. For OAuth2 platforms returns an auth_url to redirect the user to. For apikey/credentials platforms stores the credentials immediately and returns account_id. For WhatsApp, open auth_url to run Meta's signup. When the WhatsApp Business account holds several numbers, the redirect to your redirect_uri carries status=selection_required and a connection_id: list the numbers with GET /v1/accounts/pending/{connection_id} and pick one with POST /v1/accounts/pending/{connection_id}/select. Set the top-level redirect_uri field when connecting WhatsApp so the result reaches a page you control. # Create Pinterest board Source: https://docs.social-api.ai/api-reference/accounts/create-pinterest-board /api-reference/openapi.json post /accounts/{id}/boards Creates a board on Pinterest and returns it. # Delete Pinterest board Source: https://docs.social-api.ai/api-reference/accounts/delete-pinterest-board /api-reference/openapi.json delete /accounts/{id}/boards/{boardId} Deletes the board on Pinterest along with every pin saved to it. This cannot be undone. # Disconnect a social account Source: https://docs.social-api.ai/api-reference/accounts/disconnect-a-social-account /api-reference/openapi.json delete /accounts/{id} Disconnects a connected account. It will no longer appear in the accounts list and all API calls using this account will return 404. # Exchange OAuth code Source: https://docs.social-api.ai/api-reference/accounts/exchange-oauth-code /api-reference/openapi.json post /oauth/exchange Exchanges an OAuth authorization code for a connected social account. Use this when you handle the OAuth callback on your own server (proxy flow). For a single account the response is a flat ConnectAccountResponse (HTTP 201). When a single OAuth flow connects multiple accounts (e.g. Facebook Pages) the response is OAuthCallbackResponse. # Get a pending account selection Source: https://docs.social-api.ai/api-reference/accounts/get-a-pending-account-selection /api-reference/openapi.json get /accounts/pending/{connection_id} Returns the candidates of a pending connection: the Google Business Profiles of a Google login that manages more than one, the LinkedIn member profile and company Pages of a LinkedIn authorization that offers more than one, or the phone numbers of a WhatsApp Business account that holds several. Choose one with the select endpoint. # Get creator info for publishing Source: https://docs.social-api.ai/api-reference/accounts/get-creator-info-for-publishing /api-reference/openapi.json get /accounts/{id}/creator-info Returns platform-specific creator settings needed before publishing (privacy levels, interaction toggles, posting availability). Currently supported for TikTok only; other platforms return 501. # Get Pinterest account summary Source: https://docs.social-api.ai/api-reference/accounts/get-pinterest-account-summary /api-reference/openapi.json get /platforms/pinterest/accounts/{id}/summary Returns the connected Pinterest account's public profile counts: followers, pins, boards, and monthly views. # List account pages Source: https://docs.social-api.ai/api-reference/accounts/list-account-pages /api-reference/openapi.json get /accounts/{id}/pages Returns pages (e.g. Facebook Pages) associated with a connected account. # List connected accounts Source: https://docs.social-api.ai/api-reference/accounts/list-connected-accounts /api-reference/openapi.json get /accounts Returns all active social media accounts connected to the authenticated API key's workspace. # List Pinterest boards Source: https://docs.social-api.ai/api-reference/accounts/list-pinterest-boards /api-reference/openapi.json get /accounts/{id}/boards Returns the boards for a connected Pinterest account. # List Pinterest pins Source: https://docs.social-api.ai/api-reference/accounts/list-pinterest-pins /api-reference/openapi.json get /platforms/pinterest/accounts/{id}/pins Returns the connected Pinterest account's pins. # Re-sync a connected account's posts Source: https://docs.social-api.ai/api-reference/accounts/re-sync-a-connected-accounts-posts /api-reference/openapi.json post /accounts/{id}/sync Refreshes one connected account's posts: new posts appear, engagement counts update, and Instagram and Facebook media links in platform_media are renewed. Instagram covers up to the 500 most recent posts, Facebook up to the 500 most recent per Page; other platforms cover up to the 25 most recent. Available on platforms that support listing posts. Returns once the refresh completes. Each account can be synced once every 5 minutes. # Select the account or Pages to connect from a pending connection Source: https://docs.social-api.ai/api-reference/accounts/select-the-account-or-pages-to-connect-from-a-pending-connection /api-reference/openapi.json post /accounts/pending/{connection_id}/select Promotes a pending connection into a connected account. For Google, LinkedIn and WhatsApp, pass platform_account_id to choose exactly one candidate. For Facebook, pass page_ids with the subset of candidate Pages to assign to the brand. First selection wins; the pending connection is then consumed. # Update account page Source: https://docs.social-api.ai/api-reference/accounts/update-account-page /api-reference/openapi.json patch /accounts/{id}/pages/{pageId} Update a page's is_default or is_active status. # Update an account's permissions Source: https://docs.social-api.ai/api-reference/accounts/update-an-accounts-permissions /api-reference/openapi.json patch /accounts/{id} Replaces the permissions of a connected account. Send null for every permission, including ones added later. Removing a permission takes effect at once and immediately deletes the comments, messages, analytics and posts not published through SocialAPI that it covers, and adding the permission back does not restore them. Posts published through SocialAPI are kept. Adding a permission takes effect at once when the platform already granted it, otherwise reconnect the account to ask for it. Check effective_permissions in the response. # Update Pinterest board Source: https://docs.social-api.ai/api-reference/accounts/update-pinterest-board /api-reference/openapi.json patch /accounts/{id}/boards/{boardId} Update a board. Privacy accepts only PUBLIC or SECRET. # Get account analytics history Source: https://docs.social-api.ai/api-reference/analytics/get-account-analytics-history /api-reference/openapi.json get /accounts/{id}/analytics Returns daily history for a connected account: follower levels, reach, and the other metrics the platform publishes. For the account's current figures, use the live insights routes instead. Each series names its metric kind (gauge for a level you may diff, flow for a per-day count you may sum) and the timezone its days were cut on. Every point also carries how many subjects contributed to that day, always present and always at least 1, even for a single-subject account. # Get brand analytics history Source: https://docs.social-api.ai/api-reference/analytics/get-brand-analytics-history /api-reference/openapi.json get /brands/{id}/analytics Returns daily history for every account and page in a brand, summed per metric per day. A brand total always reflects the accounts the brand holds right now. Read subjects_counted before drawing a chart. A brand whose accounts did not all report on a given day produces a lower sum for that day, and without the count that reads as a decline instead of a gap. # Get post analytics history Source: https://docs.social-api.ai/api-reference/analytics/get-post-analytics-history /api-reference/openapi.json get /posts/{pid}/analytics Returns daily history for a published post: how its engagement counters accrued, one point per day. This is the history route. For the post's current figures, use GET /v1/posts/{pid}/metrics instead. History does not extend back before the post was first tracked, so an older post may have no earlier points. # List active announcements Source: https://docs.social-api.ai/api-reference/announcements/list-active-announcements /api-reference/openapi.json get /announcements Returns service announcements that are currently visible, newest first. Each item carries a `seen` flag for the calling user. `window_start` and `window_end` describe the announced maintenance window and are absent for non-maintenance announcements. Free operation, consumes no credits. # Mark an announcement as seen Source: https://docs.social-api.ai/api-reference/announcements/mark-an-announcement-as-seen /api-reference/openapi.json post /announcements/{id}/seen Records that the calling user has seen an announcement, so it is not shown again. Idempotent: repeat calls succeed without changing anything. Free operation, consumes no credits. # Create API key Source: https://docs.social-api.ai/api-reference/api-keys/create-api-key /api-reference/openapi.json post /keys Generates a new API key for the authenticated user. Optionally restrict it to a set of scopes (see GET /keys/scopes) and/or brands. Empty scopes or brand_ids mean unrestricted. # List API key scopes Source: https://docs.social-api.ai/api-reference/api-keys/list-api-key-scopes /api-reference/openapi.json get /keys/scopes Returns the catalog of scopes assignable to restricted API keys. # List API keys Source: https://docs.social-api.ai/api-reference/api-keys/list-api-keys /api-reference/openapi.json get /keys Returns all API keys for the authenticated user, including each key's scope and brand restrictions. # Revoke API key Source: https://docs.social-api.ai/api-reference/api-keys/revoke-api-key /api-reference/openapi.json delete /keys/{id} Deactivates an API key. This is irreversible. # Rotate API key Source: https://docs.social-api.ai/api-reference/api-keys/rotate-api-key /api-reference/openapi.json post /keys/{id}/rotate Issues a new secret for an existing key, keeping its id, name, scopes, and brand restrictions. The old key value stops working immediately. The new raw key is shown once. Requires a full-access key or the dashboard. # Update API key Source: https://docs.social-api.ai/api-reference/api-keys/update-api-key /api-reference/openapi.json patch /keys/{id} Edits an existing key's name, scopes, and brand restrictions in place. The key value is unchanged (no new secret is issued). Empty scopes or brand_ids mean unrestricted. Requires a full-access key or the dashboard. # Create an Automation Source: https://docs.social-api.ai/api-reference/automations/create-an-automation /api-reference/openapi.json post /automations Creates an Automation on an Instagram or Facebook account. When a comment or DM on the account matches its filters, its actions are carried out within seconds, with no code on your side. Each action costs what the same API call costs: reply_comment and send_dm cost 1 credit, private_reply and hide_comment are free. The first step's actions depend on the trigger: a comment trigger allows hide_comment, reply_comment and private_reply; a dm trigger allows send_dm. Steps after start run when the person taps one of your postback buttons, and may only send DMs. An account holds at most 50 Automations. Comments and messages sent by the account itself never trigger it; on Facebook that is the Page, so a Page admin commenting from their personal profile does trigger it. A dm Automation answers the same person at most 5 times an hour. Besides automations:write, a scoped key needs the scope each action's own API call needs: inbox:write for hide_comment, reply_comment and private_reply, dms:send for send_dm. Send follow_gate instead of steps to build a Follow gate: the commenter gets a private reply with a button, and a tap sends the link if they follow the account, otherwise a follow-first message with a button to try again. Instagram only. # Delete an Automation Source: https://docs.social-api.ai/api-reference/automations/delete-an-automation /api-reference/openapi.json delete /automations/{id} Deletes an Automation and its run history. Buttons it already sent stop doing anything. # Get an Automation Source: https://docs.social-api.ai/api-reference/automations/get-an-automation /api-reference/openapi.json get /automations/{id} Returns one Automation with its fired, sent and failed counts. # List an Automation's runs Source: https://docs.social-api.ai/api-reference/automations/list-an-automations-runs /api-reference/openapi.json get /automations/{id}/firings Lists the Automation's runs from the last 30 days, newest first, with the outcome of each action. A failed action carries the error code the same API call would have returned. # List Automations Source: https://docs.social-api.ai/api-reference/automations/list-automations /api-reference/openapi.json get /automations Lists your Automations, newest first. Filter to one account with account_id. # Update an Automation Source: https://docs.social-api.ai/api-reference/automations/update-an-automation /api-reference/openapi.json patch /automations/{id} Updates the fields you send and keeps the rest. Set status to paused to stop the Automation and to active to resume it. steps replaces the whole list. Changed steps or post_ids must meet the same rules as creation. Renaming or pausing needs only automations:write; resuming, or changing steps, post_ids or once_per_person, also needs each action's own scope. # List plans Source: https://docs.social-api.ai/api-reference/billing/list-plans /api-reference/openapi.json get /billing/plans Returns every subscription tier with prices and resource limits, ordered from cheapest to most expensive. Each tier is priced in USD and in EUR. -1 means unlimited for limits and custom pricing for prices. # Create a brand Source: https://docs.social-api.ai/api-reference/brands/create-a-brand /api-reference/openapi.json post /brands Creates a new brand for grouping connected social accounts. Each brand can have one account per platform. # Delete a brand Source: https://docs.social-api.ai/api-reference/brands/delete-a-brand /api-reference/openapi.json delete /brands/{id} Deletes a brand and disconnects all its connected accounts. # List brands Source: https://docs.social-api.ai/api-reference/brands/list-brands /api-reference/openapi.json get /brands Returns all brands for the authenticated user with their connected account counts. # Update a brand Source: https://docs.social-api.ai/api-reference/brands/update-a-brand /api-reference/openapi.json patch /brands/{id} Renames a brand. # Event summary counts Source: https://docs.social-api.ai/api-reference/events/event-summary-counts /api-reference/openapi.json get /events/summary Returns aggregated event counts for the requested window: totals, failure counts, breakdowns by category and error category, plus inbox-specific breakdown. Useful for dashboard summary cards. # List developer events Source: https://docs.social-api.ai/api-reference/events/list-developer-events /api-reference/openapi.json get /events Returns a paginated, filtered slice of the unified event log. Events cover post lifecycle, inbox activity, account changes, and webhook deliveries. Retention varies by plan tier; the response includes the user's current retention window. # Create analytics export Source: https://docs.social-api.ai/api-reference/exports/create-analytics-export /api-reference/openapi.json post /accounts/{id}/export Enqueue an async analytics export job for a connected account. Subject to plan limits (exports per month, cooldown hours, concurrent export cap). For some TikTok accounts, include_transcript and include_vision are refused with 400 account.export_enrichment_not_supported; export again without them. # Get export status and result Source: https://docs.social-api.ai/api-reference/exports/get-export-status-and-result /api-reference/openapi.json get /exports/{id} Returns the current status, progress, result download URL (presigned Excel), and report URL for an export job. # Get export videos Source: https://docs.social-api.ai/api-reference/exports/get-export-videos /api-reference/openapi.json get /exports/{id}/videos Returns the video data for a completed export, including metrics and optional AI-enriched fields (transcript, vision summary). # List analytics exports Source: https://docs.social-api.ai/api-reference/exports/list-analytics-exports /api-reference/openapi.json get /exports Returns the 50 most recent export jobs for the authenticated user. Optionally filter by status. # Assign a Facebook Page to a brand Source: https://docs.social-api.ai/api-reference/facebook/assign-a-facebook-page-to-a-brand /api-reference/openapi.json post /platforms/facebook/logins/{loginId}/pages/{pageId}/assign Assigns one Page from this login to the given brand. Each Page belongs to exactly one brand, so a Page currently held by another brand of yours is moved to the target brand. Each brand can connect up to 5 Facebook Pages, a hard cap. Assigning beyond that limit is refused. # Get daily Facebook Page insights Source: https://docs.social-api.ai/api-reference/facebook/get-daily-facebook-page-insights /api-reference/openapi.json get /platforms/facebook/accounts/{id}/insights/daily Returns one point per day for one Page of a connected Facebook account: followers at the end of the day, plus that day's views, reach and post engagements. Days with no data are omitted. Same permission errors as the Page insights summary. # Get Facebook Page insights summary Source: https://docs.social-api.ai/api-reference/facebook/get-facebook-page-insights-summary /api-reference/openapi.json get /platforms/facebook/accounts/{id}/summary Returns trailing-30-day Page insight aggregates (followers, views, reach, post engagements, net follower change) for one Page of a connected Facebook account. A Page connected before 2026-10-17, or whose login declined the insights permission, returns 403 platform.facebook.insights_scope_missing and must be reconnected. # List a Facebook login's Pages Source: https://docs.social-api.ai/api-reference/facebook/list-a-facebook-logins-pages /api-reference/openapi.json get /platforms/facebook/logins/{loginId}/pages Lists every Page this login can manage, live from Meta, merged with assignment state. Pages granted after the last OAuth exchange appear here without re-authenticating. assignable reflects ownership only; the 5-page-per-brand cap is enforced when assigning. # Send feedback Source: https://docs.social-api.ai/api-reference/feedback/send-feedback /api-reference/openapi.json post /feedback Submit feedback (bug report, feature request, or general comment). Routed to the SocialAPI team. # Add media to a Google Business Profile location Source: https://docs.social-api.ai/api-reference/google/add-media-to-a-google-business-profile-location /api-reference/openapi.json post /platforms/google/accounts/{id}/media Attaches one photo or video to a location's public profile under the given category. media takes exactly one item in the same form the publishing endpoints accept: a stored media library id (source_type media_id), a public URL (url), or a reference to bytes you uploaded to Google yourself (platform_attachment_id). The photo or video format is derived from the item you supply, so you never set Google's own upload fields. Google limits a location to 10 edits per minute, and that limit cannot be raised. # Assign a Google Business Profile location to a brand Source: https://docs.social-api.ai/api-reference/google/assign-a-google-business-profile-location-to-a-brand /api-reference/openapi.json post /platforms/google/logins/{loginId}/locations/{locationId}/assign Assigns one location from this login to the given brand, without re-doing OAuth. Each location belongs to exactly one brand, and each brand holds at most one Google Business Profile. # Get Google Business Profile performance metrics Source: https://docs.social-api.ai/api-reference/google/get-google-business-profile-performance-metrics /api-reference/openapi.json get /platforms/google/accounts/{id}/insights Returns daily time series for a location: how many people saw it on Google Search and Maps, and how many then called, asked for directions, messaged, booked, ordered, or clicked through to the website. start_date and end_date are inclusive and formatted YYYY-MM-DD. metric takes a comma-separated list and defaults to every metric. A day with no activity comes back as a zero rather than a gap. Google publishes no retention window and no freshness lag for this data, so a recent day may still be filling in and an old range may return nothing. # Get one Google Business Profile media item Source: https://docs.social-api.ai/api-reference/google/get-one-google-business-profile-media-item /api-reference/openapi.json get /platforms/google/accounts/{id}/media/{mediaId} Returns one photo or video from a location's profile. mediaId accepts either the bare id or the full Google resource name returned by the list endpoint. # Get the search keywords that surfaced a Google Business Profile Source: https://docs.social-api.ai/api-reference/google/get-the-search-keywords-that-surfaced-a-google-business-profile /api-reference/openapi.json get /platforms/google/accounts/{id}/search-keywords Returns the keywords people searched before seeing the location, aggregated by month over an inclusive YYYY-MM range. A low-volume keyword comes back with threshold and is_threshold=true instead of value: Google withholds the exact count and gives an upper bound, so treating a threshold as a count overstates the keyword. limit is capped at 100 by Google. # List a Google Business Profile location's media Source: https://docs.social-api.ai/api-reference/google/list-a-google-business-profile-locations-media /api-reference/openapi.json get /platforms/google/accounts/{id}/media Lists the photos and videos on a location's public profile: its logo, cover, interior shots, and so on. This is profile media, not the media attached to a post. Set contributed=true to read the separate, read-only gallery of photos customers submitted; those items carry an attribution block that Google requires you to display as provided, unaltered. # List a Google login's Business Profile locations Source: https://docs.social-api.ai/api-reference/google/list-a-google-logins-business-profile-locations /api-reference/openapi.json get /platforms/google/logins/{loginId}/locations Lists every Business Profile location this login can manage, live from Google, merged with assignment state. Locations added in Google Business Profile after the last authorization appear here without re-authenticating. assignable reflects ownership only. login_email identifies the Google account itself, and may be empty on older connections. # Remove media from a Google Business Profile location Source: https://docs.social-api.ai/api-reference/google/remove-media-from-a-google-business-profile-location /api-reference/openapi.json delete /platforms/google/accounts/{id}/media/{mediaId} Deletes one photo or video you added to a location's profile. Photos contributed by customers cannot be deleted through the API and return a permission error; use the takedown_url on the item instead. # Update a Google Business Profile location Source: https://docs.social-api.ai/api-reference/google/update-a-google-business-profile-location /api-reference/openapi.json put /platforms/google/accounts/{id} Writes the location's public profile: name, description, phone numbers, address, website, categories, opening hours, service area. Only the fields you send are touched. A field you omit is left as it is, and a field you send with an empty value is CLEARED, which is how you remove a phone number or a website. Fields Google owns are not writable and are rejected: place_id, maps_uri, new_review_uri and the status flags are output-only, and language_code is fixed when the location is created. Google moderates these edits, so a 200 means the change was accepted for review rather than already live on Search and Maps; read the location back to see what is live and check has_pending_edits. Google allows 10 edits per minute per Business Profile and that limit cannot be raised. Pass validate_only=true to have Google check the change without applying it, which is the only way to test against a real profile since the API has no sandbox. # Delete a comment Source: https://docs.social-api.ai/api-reference/inbox-comments/delete-a-comment /api-reference/openapi.json delete /inbox/comments/{postId}/{commentId} Deletes a comment on the platform and removes it from the inbox. Not all platforms support comment deletion - check the can_delete capability flag. # Hide a comment Source: https://docs.social-api.ai/api-reference/inbox-comments/hide-a-comment /api-reference/openapi.json post /inbox/comments/{postId}/{commentId}/hide Hides a comment on the platform so it is not visible to the public, and marks it hidden in the inbox. Supported on Instagram, Facebook, Threads (replies from other accounts), X (replies to the account's own posts), YouTube, Bluesky (replies in threads the account started), and TikTok (default connection, not a creator connection; comments written by other people). Check the can_hide capability flag. On Threads, an account connected before 2026-10-17 gets an authorization error until it is reconnected. # Like a comment Source: https://docs.social-api.ai/api-reference/inbox-comments/like-a-comment /api-reference/openapi.json post /inbox/comments/{postId}/{commentId}/like Likes a comment on the platform as the connected account. Supported on Facebook, LinkedIn Pages, and Instagram accounts connected with Facebook Login. Instagram accounts connected with Instagram Login cannot like comments. Check the can_like capability flag. # List commented posts Source: https://docs.social-api.ai/api-reference/inbox-comments/list-commented-posts /api-reference/openapi.json get /inbox/comments Returns inbox posts that have comments, ordered by most-recently-commented. Supports filtering by account, platform, minimum comment count, and date range. Uses cursor-based pagination. YouTube commented posts only appear when the request is scoped with account_id (they are absent from the unfiltered aggregate), and that list returns a single page (no cursor). # List comments on a post Source: https://docs.social-api.ai/api-reference/inbox-comments/list-comments-on-a-post /api-reference/openapi.json get /inbox/comments/{postId} Returns comments for a specific inbox post, including per-platform capability flags (can_reply, can_delete, can_hide, can_like, can_private_reply). Uses cursor-based pagination. # List replies to a comment Source: https://docs.social-api.ai/api-reference/inbox-comments/list-replies-to-a-comment /api-reference/openapi.json get /inbox/comments/{postId}/{commentId}/replies Returns paginated replies to a specific comment. Same response shape as top-level comments. Uses cursor-based pagination. # Reply to a comment Source: https://docs.social-api.ai/api-reference/inbox-comments/reply-to-a-comment /api-reference/openapi.json post /inbox/comments/{postId} Posts a reply on the platform. If comment_id is provided, replies to that specific comment (threaded reply); otherwise replies to the post itself (top-level comment). Consumes 1 interaction credit. # Send a private reply to a commenter Source: https://docs.social-api.ai/api-reference/inbox-comments/send-a-private-reply-to-a-commenter /api-reference/openapi.json post /inbox/comments/{postId}/{commentId}/private-reply Sends a direct message to the author of a comment. Only supported on Instagram and Facebook. Meta allows one private reply per comment, within 7 days of the comment; a second attempt or an expired window returns HTTP 400 with error code platform.instagram.private_reply_ineligible or platform.facebook.private_reply_ineligible. Once a comment has spent its one reply, or the comment is older than 7 days, further attempts are refused and do not count against your outbound message rate; the error carries a reason of already_replied, platform_refused or window_expired. The 400 is terminal: retrying it will never succeed. Set buttons (1 to 3) to send the reply as a button template with text as its body; a tap on a postback button arrives as a dm.postback event. Set attachment instead of text to send one image, video, audio or file: a private reply is a single message, so it carries either text (with optional buttons) or one attachment, never both. You cannot message the commenter again until they respond; once they do, the conversation continues within the standard 24-hour messaging window. Check the can_private_reply capability flag. # Unhide a comment Source: https://docs.social-api.ai/api-reference/inbox-comments/unhide-a-comment /api-reference/openapi.json delete /inbox/comments/{postId}/{commentId}/hide Restores a previously hidden comment so it is publicly visible again. Supported on the same platforms as hiding: Instagram, Facebook, Threads, X, YouTube, Bluesky, and TikTok. On Threads, an account connected before 2026-10-17 gets an authorization error until it is reconnected. # Unlike a comment Source: https://docs.social-api.ai/api-reference/inbox-comments/unlike-a-comment /api-reference/openapi.json delete /inbox/comments/{postId}/{commentId}/like Removes a like from a comment on the platform. Supported on Facebook, LinkedIn Pages, and Instagram accounts connected with Facebook Login. Check the can_like capability flag. # React to a message Source: https://docs.social-api.ai/api-reference/inbox-conversations/react-to-a-message /api-reference/openapi.json post /inbox/conversations/{id}/messages/{messageId}/reaction Adds or replaces the account's emoji reaction on a message. Send the emoji character itself. Supported on Instagram and Facebook Messenger. # Remove a reaction from a message Source: https://docs.social-api.ai/api-reference/inbox-conversations/remove-a-reaction-from-a-message /api-reference/openapi.json delete /inbox/conversations/{id}/messages/{messageId}/reaction Removes the account's emoji reaction from a message. # List Accounts with reviews Source: https://docs.social-api.ai/api-reference/inbox-reviews/list-accounts-with-reviews /api-reference/openapi.json get /inbox/reviews Lists your review-capable connected Accounts, each with its live review summary (average rating, total count, last review time). Use this to build the reviews inbox navigation, then GET /inbox/reviews/{account_id} for one Account's reviews. # List reviews for an Account Source: https://docs.social-api.ai/api-reference/inbox-reviews/list-reviews-for-an-account /api-reference/openapi.json get /inbox/reviews/{account_id} Returns one page of reviews for a single connected Account, newest first by default. Supported on Google Business Profile and App Store Connect. App Store Connect reviews also carry a title; App Store Connect does not report an average rating, so summary.average_rating is 0 for those Accounts. # Reply to a review Source: https://docs.social-api.ai/api-reference/inbox-reviews/reply-to-a-review /api-reference/openapi.json post /inbox/reviews/{id}/reply Posts a public reply to a review on the platform (Google Business Profile, App Store Connect). Consumes 1 interaction credit. On Google, replies are moderated: a successful response returns state PENDING and the reply is not publicly visible until it becomes APPROVED. A rejected reply is never published, and the reason is reported as policy_violation on the review. On App Store Connect, replying again replaces your earlier response, and a response can take up to 24 hours to appear on the App Store. # Get a conversation Source: https://docs.social-api.ai/api-reference/inbox/get-a-conversation /api-reference/openapi.json get /inbox/conversations/{id} Returns a single inbox conversation by ID. # Get inbox media Source: https://docs.social-api.ai/api-reference/inbox/get-inbox-media /api-reference/openapi.json get /inbox/media/{id} Returns the bytes of an inbound attachment. 504 means retry shortly, not gone. # Get inbox sync status Source: https://docs.social-api.ai/api-reference/inbox/get-inbox-sync-status /api-reference/openapi.json get /inbox/sync Reports whether a sync is running for the given target and when that target last completed one. Poll this after POST /inbox/sync until state leaves "syncing", then refetch the conversation or message list. # List inbox conversations Source: https://docs.social-api.ai/api-reference/inbox/list-inbox-conversations /api-reference/openapi.json get /inbox/conversations Returns paginated DM conversations for the authenticated user, optionally filtered by account, platform, and status. # List messages in a conversation Source: https://docs.social-api.ai/api-reference/inbox/list-messages-in-a-conversation /api-reference/openapi.json get /inbox/conversations/{id}/messages Returns paginated messages for an inbox conversation, newest first. # Mark conversation as read Source: https://docs.social-api.ai/api-reference/inbox/mark-conversation-as-read /api-reference/openapi.json post /inbox/conversations/{id}/read Resets the unread count on a conversation to zero. # Send a message in a conversation Source: https://docs.social-api.ai/api-reference/inbox/send-a-message-in-a-conversation /api-reference/openapi.json post /inbox/conversations/{id}/messages Sends an outgoing DM via the connected platform account and records it in the conversation. # Start an inbox sync Source: https://docs.social-api.ai/api-reference/inbox/start-an-inbox-sync /api-reference/openapi.json post /inbox/sync Refreshes an account's inbox from the platform. Add conversation_id to refresh a single thread instead. Returns immediately: poll GET /inbox/sync until state leaves "syncing". A sync already running for the same target is returned as-is rather than duplicated. # Update conversation status Source: https://docs.social-api.ai/api-reference/inbox/update-conversation-status /api-reference/openapi.json patch /inbox/conversations/{id} Sets the status of a conversation to active or archived. # Accept an Instagram collaboration invite Source: https://docs.social-api.ai/api-reference/instagram/accept-an-instagram-collaboration-invite /api-reference/openapi.json post /platforms/instagram/accounts/{id}/media/{mediaId}/collab/accept Accepts a pending collaboration invite on an Instagram media object, publishing the co-authored post to this account's feed. Call with the invited account, not the publishing account. Invites expire 14 days after they are sent. # Decline an Instagram collaboration invite Source: https://docs.social-api.ai/api-reference/instagram/decline-an-instagram-collaboration-invite /api-reference/openapi.json post /platforms/instagram/accounts/{id}/media/{mediaId}/collab/decline Declines a pending collaboration invite on an Instagram media object. The post will not appear on this account's feed. Call with the invited account, not the publishing account. # Get an Instagram messaging participant's profile Source: https://docs.social-api.ai/api-reference/instagram/get-an-instagram-messaging-participants-profile /api-reference/openapi.json get /platforms/instagram/accounts/{id}/participants/{participantId}/profile Returns the profile Instagram exposes for a messaging participant, including the follow relationship in both directions (is_user_follow_business, is_business_follow_user). Works for an Instagram account connected either directly or through a linked Facebook Page. The follow relationship is current as of the call; do not cache it, since it changes the moment someone unfollows. Instagram only returns this data for users who have consented by messaging your account or tapping an icebreaker or the persistent menu; for anyone else this returns 403 platform.instagram.230, which means Instagram will not disclose the relationship and is NOT the same as "does not follow". Branch on that error rather than treating it as a negative. Fields name and profile_pic are absent for users who have set neither; the follow fields are still returned. profile_pic URLs expire after a few days, so do not store them. # Get Instagram account insights summary Source: https://docs.social-api.ai/api-reference/instagram/get-instagram-account-insights-summary /api-reference/openapi.json get /platforms/instagram/accounts/{id}/summary Returns live profile counts plus trailing-30-day insight totals (reach, views, interactions, engaged accounts, net follower change) for a connected Instagram account. The totals need the analytics.read permission. An account without it returns 403 account.permission_missing: add analytics.read to the account's permissions or reconnect the account. If Instagram itself has withdrawn the insights access, the next call can return 403 platform.instagram.insights_scope_missing instead: reconnect the account. # List active Instagram stories Source: https://docs.social-api.ai/api-reference/instagram/list-active-instagram-stories /api-reference/openapi.json get /platforms/instagram/accounts/{id}/stories Returns the currently-active stories for a connected Instagram account. Stories are ephemeral and expire about 24 hours after posting; each item carries is_story and a computed expires_at in metadata. Returns 501 for non-Instagram accounts. # List collaborators on an Instagram post Source: https://docs.social-api.ai/api-reference/instagram/list-collaborators-on-an-instagram-post /api-reference/openapi.json get /platforms/instagram/accounts/{id}/media/{mediaId}/collaborators Returns the collaborators invited on a co-authored Instagram post and the state of each invite (accepted, pending, declined). Readable only by the account that published the media: Meta scopes co-authored media data to the publisher, so a collaborating account cannot read this edge. Collaborators can only be invited when the post is created (platform_data.collaborators); Instagram exposes no API to add one to an already-published post. # API reference Source: https://docs.social-api.ai/api-reference/introduction Look up the SocialAPI REST API base URL, authentication, request and response format, and error envelope before you open an endpoint. The SocialAPI REST API reads and writes your connected accounts' comments, direct messages (DMs), reviews, mentions, posts, and analytics over HTTPS with JSON. This page covers what every endpoint shares. The endpoint pages in the reference below list each operation's parameters, request body, and responses. ## Base URL Every endpoint lives under one base URL: ```text theme={null} https://api.social-api.ai/v1 ``` ## Authentication Every request carries a token on the `Authorization` header. SocialAPI accepts two token types: an API key (`sapi_key_...`) that you create and keep on your server, and an OAuth 2.1 access token that an app receives after a SocialAPI user approves it. A [scoped API key](/guides/scoped-keys) limits a key to some scopes and brands. ```bash theme={null} curl https://api.social-api.ai/v1/accounts \ -H "Authorization: Bearer $SOCAPI_KEY" ``` See [Authentication](/guides/authentication) to create and rotate keys, and [OAuth for MCP clients](/integrations/oauth) for the OAuth flow. ## Request format * Send a JSON body with `Content-Type: application/json`. Two endpoints take a multipart form instead: `POST /v1/media/upload` and `POST /v1/posts/import`. * Write every timestamp in ISO 8601 UTC, for example `2026-10-02T09:00:00Z`. * Send `X-Request-ID` to set your own request ID: see [Debugging](/guides/debugging). ## Response format A successful response is JSON with an HTTP `2xx` status. List endpoints return a page of results and a cursor: see [Pagination](/guides/pagination). Every object has an `id` that you store and send back unchanged: see [IDs](/guides/ids). A failed request to `/v1` returns a non-`2xx` status and this body: ```json theme={null} { "error": { "code": "validation.field_required", "message": "Required field is missing: platform", "meta": { "field": "platform" } }, "request_id": "a1b2c3d4" } ``` Branch on `error.code`, never on `error.message`. `request_id` equals the `X-Request-ID` response header: quote it when you report a failed request. [Errors](/guides/errors) lists every code with its HTTP status, and [Debugging](/guides/debugging) explains `request_id`. ## Endpoint groups Endpoint pages cover these areas: | Group | What it covers | | - | - | | Accounts | Connect, list, reconnect, and disconnect accounts, and manage Pages and locations | | Brands and Invites | Group accounts by brand and invite clients to connect theirs | | Inbox | Comments, DMs, and reviews across platforms | | Mentions | Mentions of a connected account | | Automations | Rules that reply to Instagram and Facebook comments and DMs | | Posts | Publish, schedule, update, retry, and delete posts, and read post metrics | | Media | Upload and manage media files | | Analytics and Exports | Stored account, brand, and post history, and exports | | Webhooks | Webhook endpoints and their deliveries | | Events, Usage, and Announcements | The events log, plan usage, and product announcements | | Billing | The plan catalog: see [Plans and usage](/guides/plans) | | API Keys and Users | API keys and your SocialAPI profile | | Platform groups | Endpoints under `/v1/platforms/` for one platform, such as WhatsApp, Google, Instagram, TikTok, and YouTube | > [card] [Inbox](/guides/inbox) > > Comments, DMs, and reviews across platforms. > [card] [Publishing](/guides/publishing) > > Create, schedule, and manage posts. > [card] [Webhooks](/guides/webhooks) > > Get notified of events in real time. > [card] [Errors](/guides/errors) > > Every error code and how to handle it. # Create an invite link Source: https://docs.social-api.ai/api-reference/invites/create-an-invite-link /api-reference/openapi.json post /invites Creates a single-use invite link for an external user to connect a social account to a brand. The brand must not already have a connected account for the specified platform, and no active (unused, unexpired) invite may exist for the same brand+platform combination. # List invites for a brand Source: https://docs.social-api.ai/api-reference/invites/list-invites-for-a-brand /api-reference/openapi.json get /invites Returns all invites (active and used/expired) for a brand, ordered by created_at DESC. # Redeem an invite link Source: https://docs.social-api.ai/api-reference/invites/redeem-an-invite-link /api-reference/openapi.json get /invite/{token} Public endpoint (no auth). Validates the invite token and redirects the user to the platform's OAuth authorization page. On successful OAuth callback, the account is connected to the invite creator's brand. # Revoke an invite Source: https://docs.social-api.ai/api-reference/invites/revoke-an-invite /api-reference/openapi.json delete /invites/{id} Marks an invite as used (revoked), preventing it from being redeemed. # Connect a LinkedIn profile or Page from a login Source: https://docs.social-api.ai/api-reference/linkedin/connect-a-linkedin-profile-or-page-from-a-login /api-reference/openapi.json post /platforms/linkedin/logins/{loginId}/profiles/{profileId}/assign Connects the member profile or one company Page of a LinkedIn login to the given brand, without re-doing OAuth. Each LinkedIn account belongs to one brand. A brand can hold several LinkedIn accounts. # Get LinkedIn insights Source: https://docs.social-api.ai/api-reference/linkedin/get-linkedin-insights /api-reference/openapi.json get /platforms/linkedin/insights Returns the follower count plus per-post performance for a connected LinkedIn account. For a company Page, `posts` covers the Page's recent posts. For a personal profile, `posts` covers the 5 most recent posts published to it through SocialAPI, and `clicks` and `engagement_rate` are always 0. `kind` is `organization` for a Page and `member` for a profile. # List the profile and Pages of a LinkedIn login Source: https://docs.social-api.ai/api-reference/linkedin/list-the-profile-and-pages-of-a-linkedin-login /api-reference/openapi.json get /platforms/linkedin/logins/{loginId}/profiles Lists the member profile and every company Page a LinkedIn login can connect, with assignment state. A Page the member started administering after the last authorization appears without re-authenticating. A login authorized with connection_type profile lists the member only, and one authorized with connection_type organization lists its Pages only. loginId is the login_id of any LinkedIn account connected through that login. # Delete a media file Source: https://docs.social-api.ai/api-reference/media/delete-a-media-file /api-reference/openapi.json delete /media/{id} # Get a presigned media upload URL Source: https://docs.social-api.ai/api-reference/media/get-a-presigned-media-upload-url /api-reference/openapi.json get /media/upload-url Returns a presigned PUT URL for client-side media upload. Upload the file directly via HTTP PUT to the returned URL, then call POST /v1/media/:id/verify to confirm. # Get storage usage Source: https://docs.social-api.ai/api-reference/media/get-storage-usage /api-reference/openapi.json get /media/storage Returns total bytes used, the plan limit, and file count for the authenticated user. # List media files Source: https://docs.social-api.ai/api-reference/media/list-media-files /api-reference/openapi.json get /media Returns a paginated list of uploaded media files for the authenticated user. # Upload media (server-side) Source: https://docs.social-api.ai/api-reference/media/upload-media-server-side /api-reference/openapi.json post /media/upload Upload a media file directly through the API. # Verify a media upload Source: https://docs.social-api.ai/api-reference/media/verify-a-media-upload /api-reference/openapi.json post /media/{id}/verify Confirms that a presigned-URL upload completed successfully and marks the media as ready. # List brand mentions Source: https://docs.social-api.ai/api-reference/mentions/list-brand-mentions /api-reference/openapi.json get /accounts/{id}/mentions Fetches posts where your brand account was mentioned or tagged. Results are sorted newest-first. Returns 501 for platforms that do not support mention tracking. On Instagram this returns only media your account was tagged in: @mentions written inside a post caption or inside a comment are never included, because Meta exposes no endpoint to poll them, and they are not currently delivered as mention.received webhooks either. On Threads, an account connected before 2026-10-17 gets an authorization error until it is reconnected. # Reply to a mention Source: https://docs.social-api.ai/api-reference/mentions/reply-to-a-mention /api-reference/openapi.json post /accounts/{id}/mentions/reply Replies, as a public comment, to a mention of the connected account, including on a third-party post where the normal comment-reply endpoint is not permitted. Provide media_id (the media/post the mention is on); set comment_id only when the mention was in a comment (omit it to reply to a mention in the post caption). Supported on Instagram, Facebook, Threads and Bluesky. Consumes 1 interaction credit. # Get a migration Source: https://docs.social-api.ai/api-reference/migration/get-a-migration /api-reference/openapi.json get /migrations/{provider} Returns your migration from a provider: the authorisation text to accept before starting, and once started, your accounts, the Brands and plan they need, and progress. If the provider cannot be read, the response is still 200 with `assessment` null and `assessment_error` set. The assessment can be up to 5 minutes old unless `refresh` is true. Dashboard sessions only; API keys get 403. # Start a migration Source: https://docs.social-api.ai/api-reference/migration/start-a-migration /api-reference/openapi.json post /migrations Starts a migration from another provider with an API key from your account there, and returns the first assessment of your accounts. Calling it again replaces the key. You must accept the current authorisation text: send its `version` from `GET /v1/migrations/{provider}`. Dashboard sessions only; API keys get 403. # Stop a migration Source: https://docs.social-api.ai/api-reference/migration/stop-a-migration /api-reference/openapi.json delete /migrations/{provider} Stops the migration from a provider. Requests sent through the base URL with that key stop working. Dashboard sessions only; API keys get 403. # Bulk import posts from CSV Source: https://docs.social-api.ai/api-reference/posts/bulk-import-posts-from-csv /api-reference/openapi.json post /posts/import Upload a CSV file to create multiple posts. Use dry_run=true to validate without creating. Columns, in order: text, title, account_ids, scheduled_at, visibility, first_comment, platform_data, board_id. Only text is required. account_ids is pipe separated. # Create or schedule a post Source: https://docs.social-api.ai/api-reference/posts/create-or-schedule-a-post /api-reference/openapi.json post /posts Creates a post for one or more connected accounts. Set `publish_now: true` to publish immediately: the post is accepted with status `publishing` and delivery runs in the background (so slow reel/video uploads do not block the request). The terminal status (`published`, `partial`, or `failed`) arrives via the `post.published`/`post.partial`/ `post.failed` webhooks and by polling GET /posts/{pid}. Set `scheduled_at` to queue the post for future publication. With neither flag the post is saved as a draft (this is the default, even when targets are provided); publish it later with POST /posts/{pid}/publish. Set `stagger` to publish the targets one after another, separated by a random delay between `min_seconds` and `max_seconds`, instead of all at once; the post is then returned with status `scheduled`. Set `queue_id` instead of `scheduled_at` to schedule the post at the earliest free time of a Queue; the time is in `scheduled_at` on the response. Consumes 1 post credit when the post is published or scheduled; saving a draft is free. # Delete or cancel a post Source: https://docs.social-api.ai/api-reference/posts/delete-or-cancel-a-post /api-reference/openapi.json delete /posts/{pid} Deletes a draft/scheduled/failed post locally, or deletes a published post from every platform that supports it. Platforms that do not support deletion (or fail) keep their copy live and the post is kept rather than removed. Returns a per-platform result. # Get a single post Source: https://docs.social-api.ai/api-reference/posts/get-a-single-post /api-reference/openapi.json get /posts/{pid} Fetch a post with all platform delivery details. Accepts the SocialAPI post ID or a platform post ID (the platform's native ID for a published target). # Get platform constraints Source: https://docs.social-api.ai/api-reference/posts/get-platform-constraints /api-reference/openapi.json get /posts/validate Returns media and text constraints for all supported platforms. Cacheable. # Get post metrics Source: https://docs.social-api.ai/api-reference/posts/get-post-metrics /api-reference/openapi.json get /posts/{pid}/metrics Fetches engagement metrics from platform APIs for a published post. Accepts the SocialAPI post ID or a platform post ID. Instagram targets add views, reach and interactions, plus follows, profile visits and profile actions on feed posts, and watch time, skip rate and crossposted views on Reels. A metric the platform does not report for a post is omitted. If an Instagram target returns none of them, reconnect the account to grant the insights permission. # List all posts Source: https://docs.social-api.ai/api-reference/posts/list-all-posts /api-reference/openapi.json get /posts Returns posts with rich filtering and cursor pagination. Supports filtering by status, platform, account_ids, date range, and text search. # Publish a draft post now Source: https://docs.social-api.ai/api-reference/posts/publish-a-draft-post-now /api-reference/openapi.json post /posts/{pid}/publish Publishes a post in `draft` or `scheduled` status immediately. The post is accepted with status `publishing` and delivery runs in the background (so slow reel/video uploads do not block the request); the terminal status arrives via the `post.published`/ `post.partial`/`post.failed` webhooks and by polling GET /posts/{pid}. Use this to publish a post that was saved as a draft, or to publish a scheduled post ahead of its time. A post in a Queue publishes now and leaves the Queue. Consumes 1 post credit. # Retry a failed post Source: https://docs.social-api.ai/api-reference/posts/retry-a-failed-post /api-reference/openapi.json post /posts/{pid}/retry Re-attempts publication of a post in `failed` or `partial` status. Consumes 1 post credit. # Unpublish a post Source: https://docs.social-api.ai/api-reference/posts/unpublish-a-post /api-reference/openapi.json post /posts/{pid}/unpublish Unpublishes a published post from a specific account (if account_id provided) or all accounts. # Update a post Source: https://docs.social-api.ai/api-reference/posts/update-a-post /api-reference/openapi.json patch /posts/{pid} Updates the text, media, schedule time, targets, or hidden status of a pending post. Only posts in `draft`, `scheduled`, or `failed` status can be updated. `queue_id` moves the post to another Queue, and `queue_id: null` takes it out of its Queue and keeps its time. Omit `targets` to keep the current ones. Sending `targets` replaces them all. `scheduled_at` must be in the future, and a scheduled post must keep at least one target. # Validate post content Source: https://docs.social-api.ai/api-reference/posts/validate-post-content /api-reference/openapi.json post /posts/validate Dry-run validation. Returns errors for hard limit violations and warnings for best-practice issues. # Create a Queue Source: https://docs.social-api.ai/api-reference/queues/create-a-queue /api-reference/openapi.json post /queues Creates a Queue on a Brand: a weekly list of posting times in one timezone. A post created with queue_id takes the earliest free time. day is a lowercase English weekday name and time is HH:MM, both in the Queue's timezone. A Brand holds at most 20 Queues and always has exactly one default. The first Queue of a Brand becomes its default, and is_default: true makes this one the default. # Delete a Queue Source: https://docs.social-api.ai/api-reference/queues/delete-a-queue /api-reference/openapi.json delete /queues/{qid} Deletes a Queue. Its posts keep their scheduled times and are no longer queued; released_count is how many there were. If it was the default, the Brand's oldest remaining Queue becomes the default. # Get a Queue Source: https://docs.social-api.ai/api-reference/queues/get-a-queue /api-reference/openapi.json get /queues/{qid} Returns one Queue. # List Queues Source: https://docs.social-api.ai/api-reference/queues/list-queues /api-reference/openapi.json get /queues Lists your Queues, grouped by Brand, oldest first. Filter to one Brand with brand_id. # Preview a Queue's next times Source: https://docs.social-api.ai/api-reference/queues/preview-a-queues-next-times /api-reference/openapi.json get /queues/{qid}/preview Lists the next free times of a Queue as RFC 3339 with the Queue's local offset, soonest first. Works on a paused Queue. A forecast, not a reservation: send queue_id, never copy a previewed time into scheduled_at. # Update a Queue Source: https://docs.social-api.ai/api-reference/queues/update-a-queue /api-reference/openapi.json patch /queues/{qid} Updates the fields you send and keeps the rest. slots replaces the whole list. Set active to false to pause the Queue: posts already in it keep their times, and no new post can join it. is_default can only be set to true, which makes this Queue the Brand's default; to change the default, set another Queue's is_default to true. Posts already queued keep their times unless you send reshuffle_existing: true, which moves them onto the new slots in their current order and returns reshuffled_count. # Get Threads insights Source: https://docs.social-api.ai/api-reference/threads/get-threads-insights /api-reference/openapi.json get /platforms/threads/insights Returns engagement metrics for a single Threads post (level=media) or for the connected account (level=account). An account connected before 2026-10-17 returns 403 platform.threads.insights_scope_missing until it is reconnected. Media metrics are views, likes, replies, reposts, quotes and shares. Account metrics are those five plus clicks, followers_count and follower_demographics. shares is media-only and clicks is account-only. follower_demographics requires exactly one breakdown (country, city, age or gender) and rejects since or until. # Look up a public Threads profile (beta) Source: https://docs.social-api.ai/api-reference/threads/look-up-a-public-threads-profile-beta /api-reference/openapi.json get /platforms/threads/profile-lookup Beta. Resolves a public Threads profile by exact username and returns its public metrics. Requires the threads_profile_discovery Threads permission, which is pending Meta App Review, so calls may return an upstream authorization error for accounts outside the app's tester roster until that permission is granted. Matching is exact, with no fuzzy or prefix search. Only profiles with 100 or more followers are discoverable, anything else returns 404. The upstream budget is 1000 lookups per user per rolling 24 hours. # Get TikTok account summary Source: https://docs.social-api.ai/api-reference/tiktok/get-tiktok-account-summary /api-reference/openapi.json get /platforms/tiktok/accounts/{id}/summary # Re-sync a TikTok account's videos Source: https://docs.social-api.ai/api-reference/tiktok/re-sync-a-tiktok-accounts-videos /api-reference/openapi.json post /platforms/tiktok/accounts/{id}/sync # Get platform usage limits Source: https://docs.social-api.ai/api-reference/usage/get-platform-usage-limits /api-reference/openapi.json get /accounts/{id}/limits Returns platform-specific quota data for a connected account as a key/value map where values are remaining counts. Keys are platform-defined - for example Instagram returns `posts_remaining`. An empty object is returned for platforms without usage limits. # Get resource usage for the current billing period Source: https://docs.social-api.ai/api-reference/usage/get-resource-usage-for-the-current-billing-period /api-reference/openapi.json get /usage Returns brands, posts, and interactions consumed during the current billing period alongside the plan limits. -1 means unlimited (all paid plans). Period boundaries follow your subscription cycle, or a rolling 30-day window on the free plan. # Delete user account Source: https://docs.social-api.ai/api-reference/users/delete-user-account /api-reference/openapi.json delete /users/me Permanently deletes the authenticated user account and all associated data (API keys, connected accounts, usage logs). Any active subscription is cancelled immediately, with no refund for the remainder of the current billing period. This action is irreversible. # Get current user Source: https://docs.social-api.ai/api-reference/users/get-current-user /api-reference/openapi.json get /users/me Returns the authenticated user's profile (id, email, plan tier, onboarding flag, beta_tester flag, allowed_platforms list, billing_blocked state). # Update current user Source: https://docs.social-api.ai/api-reference/users/update-current-user /api-reference/openapi.json patch /users/me Updates the authenticated user's profile. Supports setting onboarding=false to mark onboarding as complete, and optionally recording use_case, referral, job_role and signup_metadata. The response reflects the current billing_blocked state, unaffected by this update. The response includes the caller's team role: "admin", "developer", or "" for the account owner. # Create webhook endpoint Source: https://docs.social-api.ai/api-reference/webhooks/create-webhook-endpoint /api-reference/openapi.json post /webhooks Registers a new webhook endpoint and performs a live reachability ping. If the endpoint is unreachable or returns an unexpected response, the request fails with 400 (webhook_verification_failed). The generated secret is returned once - store it securely. # Delete webhook endpoint Source: https://docs.social-api.ai/api-reference/webhooks/delete-webhook-endpoint /api-reference/openapi.json delete /webhooks/{id} Removes a webhook endpoint. Future events will not be delivered to this URL. # Get webhook delivery Source: https://docs.social-api.ai/api-reference/webhooks/get-webhook-delivery /api-reference/openapi.json get /webhooks/{id}/deliveries/{did} Returns the full record of a single webhook delivery attempt, including request payload, response status, response body excerpt, and timing. # Get webhook endpoint Source: https://docs.social-api.ai/api-reference/webhooks/get-webhook-endpoint /api-reference/openapi.json get /webhooks/{id} Returns the configuration and aggregate delivery stats for a single webhook endpoint owned by the caller. # List available webhook event types Source: https://docs.social-api.ai/api-reference/webhooks/list-available-webhook-event-types /api-reference/openapi.json get /webhooks/events Returns the catalog of webhook event types you can subscribe to, grouped by category (posts, inbox, accounts). # List webhook deliveries Source: https://docs.social-api.ai/api-reference/webhooks/list-webhook-deliveries /api-reference/openapi.json get /webhooks/{id}/deliveries Returns recent delivery attempts for a webhook endpoint. Optionally filter by status (success, failed, pending) or event_type. Cursor-paginated. # List webhook endpoints Source: https://docs.social-api.ai/api-reference/webhooks/list-webhook-endpoints /api-reference/openapi.json get /webhooks Returns all webhook endpoints for the authenticated user. # Retry webhook delivery Source: https://docs.social-api.ai/api-reference/webhooks/retry-webhook-delivery /api-reference/openapi.json post /webhooks/{id}/deliveries/{did}/retry Requeues a failed webhook delivery for another attempt. Returns the updated delivery record. # Send test webhook delivery Source: https://docs.social-api.ai/api-reference/webhooks/send-test-webhook-delivery /api-reference/openapi.json post /webhooks/{id}/test Sends a synthetic event payload to the webhook endpoint to validate signature, reachability, and consumer handling. Returns the delivery result. # Update webhook endpoint Source: https://docs.social-api.ai/api-reference/webhooks/update-webhook-endpoint /api-reference/openapi.json patch /webhooks/{id} Partially updates a webhook endpoint. All fields are optional; at least one must be provided. # Create a message template Source: https://docs.social-api.ai/api-reference/whatsapp/create-a-message-template /api-reference/openapi.json post /platforms/whatsapp/accounts/{id}/templates Submits a new message template to the WhatsApp Business Account (WABA) for review. The request body is the raw WhatsApp Cloud API template payload (name, category, language, components). New templates start in PENDING status; Meta reviews them and the status transitions to APPROVED or REJECTED asynchronously. # Delete a message template Source: https://docs.social-api.ai/api-reference/whatsapp/delete-a-message-template /api-reference/openapi.json delete /platforms/whatsapp/accounts/{id}/templates Deletes a template by name from the WhatsApp Business Account (WABA) associated with this connected account. WhatsApp deletes all language variants that share the given name. Templates cannot be deleted while they are being used in active campaigns. # Deregister a phone number from WhatsApp Business Source: https://docs.social-api.ai/api-reference/whatsapp/deregister-a-phone-number-from-whatsapp-business /api-reference/openapi.json post /platforms/whatsapp/phone-numbers/{id}/deregister Releases the phone number from WhatsApp Business, freeing it for use with another Business Solution Provider (BSP). This does not delete the connected account on SocialAPI; the account remains until it is disconnected explicitly. # Edit a message template Source: https://docs.social-api.ai/api-reference/whatsapp/edit-a-message-template /api-reference/openapi.json put /platforms/whatsapp/templates/{id} Updates the components of an existing message template by its WhatsApp template ID. Only APPROVED templates can be edited, and editing resets the template to PENDING for re-review. APPROVED templates are limited to one edit per day. # Get a message template Source: https://docs.social-api.ai/api-reference/whatsapp/get-a-message-template /api-reference/openapi.json get /platforms/whatsapp/templates/{id} Returns a single message template by its WhatsApp template ID. Requires an active WhatsApp account on your workspace. The template is identified by its WhatsApp template ID. # Get a WhatsApp Business Profile Source: https://docs.social-api.ai/api-reference/whatsapp/get-a-whatsapp-business-profile /api-reference/openapi.json get /platforms/whatsapp/phone-numbers/{id}/business-profile Returns the public business profile for a WhatsApp phone number, including about text, address, description, contact email, websites, and vertical. The profile is visible to customers in the WhatsApp chat header. # Get phone number registration status Source: https://docs.social-api.ai/api-reference/whatsapp/get-phone-number-registration-status /api-reference/openapi.json get /platforms/whatsapp/phone-numbers/{id}/status Returns whether a phone number is verified and registered with WhatsApp Business. Used by client UIs to decide which step of the registration flow to show. # List message templates Source: https://docs.social-api.ai/api-reference/whatsapp/list-message-templates /api-reference/openapi.json get /platforms/whatsapp/accounts/{id}/templates Returns all message templates configured on the WhatsApp Business Account (WABA) associated with this connected account. Templates include approved, pending, rejected, and paused entries. Use the after cursor from paging.cursors to fetch subsequent pages. # Register a phone number with WhatsApp Business Source: https://docs.social-api.ai/api-reference/whatsapp/register-a-phone-number-with-whatsapp-business /api-reference/openapi.json post /platforms/whatsapp/phone-numbers/{id}/register Registers a previously verified phone number with WhatsApp Business and sets the 6-digit two-step verification PIN. The phone number must be verified via verify-code first. The PIN must be retained by the caller: losing it triggers a mandatory 7-day wait before it can be reset. A number that stays on the WhatsApp Business app is already registered and is refused with 409 whatsapp.business_app_number. # Request a phone number verification code Source: https://docs.social-api.ai/api-reference/whatsapp/request-a-phone-number-verification-code /api-reference/openapi.json post /platforms/whatsapp/phone-numbers/{id}/request-code Asks WhatsApp to send a one-time verification code to the phone number via SMS or voice call. The code is delivered to the underlying phone line, not to the API caller; follow up with verify-code once the user receives it. Step 1 of the phone-number registration flow (request-code, verify-code, register). # Send a template message Source: https://docs.social-api.ai/api-reference/whatsapp/send-a-template-message /api-reference/openapi.json post /platforms/whatsapp/accounts/{id}/send-template Sends an approved WhatsApp template message to a recipient phone number. Template messages can be sent outside the 24-hour customer service window. The template must already be APPROVED on the WABA, and the language code must match an approved variant. Component parameter order must match the template's placeholders. The response includes message_id, the SocialAPI message ID for the send, which matches the id field on the dm.status.* webhooks for this message. # Update a WhatsApp Business Profile Source: https://docs.social-api.ai/api-reference/whatsapp/update-a-whatsapp-business-profile /api-reference/openapi.json put /platforms/whatsapp/phone-numbers/{id}/business-profile Updates one or more fields of the public business profile (about, address, description, email, websites, vertical, profile_picture_handle). Only provided fields are changed. The profile picture itself is changed by passing a profile_picture_handle obtained from the Resumable Upload API; profile_picture_url is read-only. # Validate a message template Source: https://docs.social-api.ai/api-reference/whatsapp/validate-a-message-template /api-reference/openapi.json post /platforms/whatsapp/templates/validate Dry-run validation of a WhatsApp message template against the Cloud API rules (name format, component cardinality, header format, text limits, placeholder sequencing, button constraints). Returns errors for hard violations that WhatsApp would reject and warnings for best-practice issues. Performs no network call and does not submit the template. # Verify a phone number with a code Source: https://docs.social-api.ai/api-reference/whatsapp/verify-a-phone-number-with-a-code /api-reference/openapi.json post /platforms/whatsapp/phone-numbers/{id}/verify-code Submits the 6-digit code received via SMS or voice to prove ownership of the phone number. Once verified, the phone number can be registered with WhatsApp Business. Codes expire quickly; if verification fails, request a new code. # Edit a published YouTube video (title, description, visibility, Studio tags) Source: https://docs.social-api.ai/api-reference/youtube/edit-a-published-youtube-video-title-description-visibility-studio-tags /api-reference/openapi.json patch /platforms/youtube/accounts/{id}/videos/{videoId} # Edit a YouTube comment Source: https://docs.social-api.ai/api-reference/youtube/edit-a-youtube-comment /api-reference/openapi.json patch /platforms/youtube/comments/{commentId} Edits the text of one of the connected account's own YouTube comments via comments.update. Only the comment author (the connected account) can edit. YouTube only. # Get YouTube channel summary Source: https://docs.social-api.ai/api-reference/youtube/get-youtube-channel-summary /api-reference/openapi.json get /platforms/youtube/accounts/{id}/summary # Moderate a YouTube comment Source: https://docs.social-api.ai/api-reference/youtube/moderate-a-youtube-comment /api-reference/openapi.json post /platforms/youtube/comments/{commentId}/moderate Sets the moderation status of a YouTube comment (published, held_for_review, or rejected), optionally banning the author. ban_author is only valid with status=rejected. YouTube only. # Re-sync a YouTube account's videos Source: https://docs.social-api.ai/api-reference/youtube/re-sync-a-youtube-accounts-videos /api-reference/openapi.json post /platforms/youtube/accounts/{id}/sync # Changelog Source: https://docs.social-api.ai/changelog Changes to the SocialAPI API and documentation, newest first. Subscribe to the RSS feed to follow new entries. **2026-11-04** LinkedIn ## One LinkedIn connect for member profiles and company Pages, open to every account Company Page and profile connections no longer need beta access and no longer return `403 billing.platform_not_available`. Page comments, mentions, and insights, and profile comment replies, likes, deletes, and insights, are available to every SocialAPI account. Breaking: `platform: "linkedin_page"` now returns `400 validation.platform_invalid` on `POST /v1/accounts/connect` and `POST /v1/invites`. What to change: * Connect member profiles and company Pages with `platform: "linkedin"`. * Send company Page publish options in `platform_data.linkedin`. Keys under `platform_data.linkedin_page` are ignored without a warning. * Company Pages connected before this change keep their `acc_` IDs and now have `platform: "linkedin"` in `GET /v1/accounts`, on posts, and in webhook payloads. `GET /v1/accounts` also returns `connection_type: "organization"` for them. * Filter posts and the inbox on `platform=linkedin`: `platform=linkedin_page` matches nothing there. In `GET /v1/events`, events recorded before this change keep `platform: "linkedin_page"`. * Reconnect member profiles connected with `platform: "linkedin"` before this change to add comment replies and insights to them. What is new: * One LinkedIn authorization offers the member profile and every company Page the member administers. When it offers more than one, the redirect carries `status=selection_required` and you choose one with `POST /v1/accounts/pending/{connection_id}/select`. `metadata.connection_type` (`profile` or `organization`) narrows the offer. * `GET /v1/accounts` returns `connection_type` (`profile` or `organization`) on every LinkedIn account, and `login_id` on LinkedIn accounts connected from this change on, with the exception described on [LinkedIn](/platforms/linkedin#connect-more-profiles-and-pages-from-a-login). * `GET /v1/platforms/linkedin/logins/{loginId}/profiles` and `POST /v1/platforms/linkedin/logins/{loginId}/profiles/{profileId}/assign` connect the login's other profile and Pages to any brand with no new consent screen. * A brand can hold more than one LinkedIn account. * `visibility: "connections_only"` now works on member profiles. Company Page posts are now checked against the LinkedIn file and text limits before publishing. See [LinkedIn](/platforms/linkedin#connect-an-account) and [LinkedIn publishing](/platforms/linkedin/publishing). **2026-11-04** Publishing ## Publishing now to an account that needs reconnecting returns 403 `POST /v1/posts` without `scheduled_at` or `queue_id`, `POST /v1/posts/{pid}/publish`, and `POST /v1/posts/{pid}/retry` now return `403 account.reconnection_required` when a target account must be reconnected, and publish nothing. Before, the request was accepted and the target then failed with `publishing.delivery.account_load_failed`. `error.meta.id` names the account. Scheduled posts and drafts are still accepted. See [Reconnect an account](/guides/connect-accounts#reconnect-an-account). **2026-11-02** TikTok ## Hidden TikTok comments listed again TikTok leaves hidden comments out of its comment list. `GET /v1/inbox/comments/{postId}` now adds back, with `is_hidden: true`, a top-level comment that appeared in the list and that you then hid with `POST /v1/inbox/comments/{postId}/{commentId}/hide`, when the first page of results has `pagination.has_more: false` and the request uses the default filters. A comment hidden in the TikTok app is not added back. See [TikTok comments](/platforms/tiktok#comments). **2026-11-02** App Store Connect ## App Store Connect reviews Connect an app with your App Store Connect API key (`platform: "app_store_connect"`), list its App Store reviews with `GET /v1/inbox/reviews/{account_id}`, and reply with `POST /v1/inbox/reviews/{id}/reply`. Reviews now carry `title` on platforms whose reviews have one. See [App Store Connect](/platforms/app-store-connect). **2026-10-30** Accounts ## brand\_id on the selection\_required redirect A `selection_required` redirect now carries `brand_id` when you sent `brand_id` on `POST /v1/accounts/connect`. Without it, the select response returns the brand. See [Redirect parameters](/guides/connect-accounts#redirect-parameters). **2026-10-30** Threads ## Threads per-profile limit returns 429 When Threads refuses a call because too many calls were made for one Threads profile, the response is now `429 platform.threads.rate_limit` instead of `502 platform.threads.80016`. Wait for `Retry-After` when it is present, or at least 60 seconds, then retry with backoff. See [Platform limits](/guides/rate-limits#platform-limits). **2026-10-29** Billing ## billing\_currency on /users/me `GET /v1/users/me` now returns `billing_currency`, the currency your subscription is billed in: `usd`, `eur`, or an empty string when you never subscribed. See [Plans](/guides/plans). **2026-10-29** Threads ## platform.threads.insights\_scope\_missing Threads insights on an account that does not hold `threads_manage_insights` now return `403 platform.threads.insights_scope_missing` until the account is reconnected. Before, these calls returned `422 platform.threads.10` or a `403` with a `platform.threads.` error code. See [Threads scopes](/platforms/threads#scopes). **2026-10-29** Analytics ## Empty series list when there is no history The account, brand, and post history endpoints now return `"series": []` instead of `"series": null` for a range with no stored points, in the REST API and the MCP tools. See [Read stored history](/guides/analytics#read-stored-history). **2026-10-28** Plans ## Plans in EUR Every paid plan is now sold in USD and in EUR, and you pick the currency at checkout. A subscription keeps the currency it started in, including when you change plan. `GET /v1/billing/plans` returns `monthly_price_eur` and `annual_price_eur`. See [Plans](/guides/plans). **2026-10-26** Threads ## Threads comment and mention events delivered Threads accounts now deliver `comment.received` when a person replies to one of your posts, and `mention.received` when another account mentions yours. Before, neither event reached your endpoint. Your account's own replies under its posts do not emit `comment.received`. See [Threads webhook events](/platforms/threads#webhook-events). **2026-10-26** Rate limits ## Meta posting blocks return 429 When Meta temporarily blocks a Facebook, Instagram, or Threads account from posting (Meta error code 368), the response is now `429 platform.{platform}.rate_limit` instead of a `502`. Facebook error codes 9 and 613 also return `429 platform.facebook.rate_limit` now. Wait for `Retry-After` when it is present, or at least 60 seconds, then retry with backoff. See [Platform limits](/guides/rate-limits#platform-limits). **2026-10-26** Publishing ## Posts to Telegram accounts are refused `POST /v1/posts` and `PATCH /v1/posts/{pid}` now return `422 account.platform_unsupported` when a target account is on Telegram, which does not publish posts. `error.meta.account_id` names the target to remove. Before, the post was accepted and its target failed later. See [Publish a post errors](/guides/errors#publish-a-post). **2026-10-26** Facebook ## Closed Messenger window reported as window\_closed A Facebook DM refused because the 24-hour messaging window is closed now returns `400 platform.facebook.window_closed` when Messenger reports the window as closed with error subcode 2018278. Before, that refusal returned `403 platform.facebook.permission_denied`. See [Inbox errors](/guides/errors#inbox-comments-messages-and-reviews). **2026-10-26** WhatsApp ## Connect WhatsApp with a redirect `POST /v1/accounts/connect` with `platform: "whatsapp"` now returns an `auth_url`, like every other platform with a consent screen. Send your customer there and they come back to your `redirect_uri` with `account_id`. There is nothing to embed or host, and no domain to register with us, so the flow works from any domain. Set `onboarding: "business_app"` to connect a number that stays on the WhatsApp Business app. When the WhatsApp Business Account holds several numbers, the redirect carries `status=selection_required` and you pick one with `POST /v1/accounts/pending/{connection_id}/select`. See [Connecting](/platforms/whatsapp#connecting). **2026-10-25** Comments ## Attachments in private replies A private reply can now send an image, video, audio file, or document instead of text, on Instagram and Facebook. Set `attachment` on `POST /v1/inbox/comments/{postId}/{commentId}/private-reply` with a `url`, `media_id`, or `platform_attachment_id` source. A private reply is one message, so `attachment` cannot be combined with `text` or `buttons`. See [Send a private reply](/guides/comments#send-a-private-reply). A media URL the platform cannot fetch or accept now returns `422 platform.instagram.attachment_failed` or `422 platform.facebook.attachment_failed`, on DMs and private replies. It returned `404 platform.instagram.100` or `404 platform.facebook.100` on a DM before, and `400 private_reply_ineligible` on a private reply. See [Errors](/guides/errors). **2026-10-23** Accounts ## brand\_id on the connect response and redirect A connect that completes without a consent screen (HTTP 201 from `POST /v1/accounts/connect`, for Telegram and Bluesky) now returns `brand_id`, and a successful connect redirect carries `brand_id` with `account_id`. When one authorization connects more than one account, the redirect carries `brand_ids` with `account_ids`, in the same order. See [Redirect parameters](/guides/connect-accounts#redirect-parameters). **2026-10-21** Rate limits ## Request rate limit documented The per-minute request budget your SocialAPI account shares across every API key, its `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers, and the `429 ratelimit.exceeded` error are now documented. See [Request rate limit](/guides/rate-limits#request-rate-limit). **2026-10-21** Facebook ## Facebook Page selection documented A Facebook connection, and an Instagram connection through Facebook Login, ends with `status=selection_required` when `facebook_page_selection` is `true` on your SocialAPI account. Choose Pages with `page_ids` on `POST /v1/accounts/pending/{connection_id}/select`. See [Choose Facebook Pages](/guides/connect-accounts#choose-facebook-pages). **2026-10-21** Errors ## Lowercase keys in meta.issues Breaking: each entry of `error.meta.issues` on `400 publishing.validation_failed` now uses the documented lowercase keys (`type`, `platform`, `field`, `message`, `target`, `segment_index`). It used `Type`, `Platform`, `Field`, and `Message` before. Read the lowercase keys. See [Publishing](/guides/publishing#validate-before-you-publish). **2026-10-21** Accounts ## Specific error\_description for account.already\_linked A redirect with `error=account.already_linked` now carries an `error_description` you can show the person, which says whether the brand already holds another account on the platform or the account belongs to another of your brands. See [Connect accounts](/guides/connect-accounts#redirect-parameters). **2026-10-19** Scoped keys ## inbox:write on comment delete, like, and YouTube moderation Deleting, liking, and unliking a comment, and moderating or editing a YouTube comment, now need `inbox:write`, in the REST API and in the MCP tools. A scoped key without it gets `403 auth.insufficient_scope`. A brand-restricted key can run these calls and reply to a review only on accounts in its brands: another brand's account returns `404`. Full-access keys are unaffected. A reply to a mention refused with `403 account.permission_missing` no longer costs an interaction credit. See [Scoped API keys](/guides/scoped-keys#scopes). **2026-10-18** Webhooks ## brand\_id on webhook events Webhook events now carry `brand_id` next to `account_id`, so one endpoint serving more than one customer can route each event by brand. `post.scheduled` carries it on each item of `targets[]`, and `post.published`, `post.partial`, and `post.failed` on each item of `results[]`. `dm.status.*` events now also carry `account_id`. See [Route events by brand](/guides/webhook-events#route-events-by-brand). **2026-10-17** Threads ## Threads mentions, insights, reply moderation, and reply\_control Threads mentions, insights, and hiding or unhiding replies are no longer in beta, and `reply_control` in a Threads target's `platform_data` now sets who can reply. A Threads connect requests `threads_manage_mentions`, `threads_manage_replies`, and `threads_manage_insights`, each with its permission: see [Scopes](/platforms/threads#scopes). An account connected before 2026-10-17 does not hold them, and those calls return `403` with a `platform.threads.` error code until the account is reconnected. `reply_control` applies only on an account whose last connect was on or after 2026-10-17 with `comments.write` or with no `permissions` selection: on any other account it is not sent and comes back as a validation warning. Profile discovery stays in beta. See [Threads](/platforms/threads). **2026-10-17** Facebook ## Facebook Page insights `GET /v1/platforms/facebook/accounts/{id}/summary` and `GET /v1/platforms/facebook/accounts/{id}/insights/daily` are open to every key and no longer return `403 platform.facebook.beta_required`. A connect with the `analytics.read` permission requests `read_insights`. A Page connected before 2026-10-17 returns `403 platform.facebook.insights_scope_missing` until the account is reconnected. Facebook DMs still send no `message_tag`. See [Page insights](/platforms/facebook#page-insights). **2026-10-17** Instagram ## Instagram through Facebook Login `POST /v1/accounts/connect` for `instagram` accepts `"metadata": {"connection_type": "facebook_login"}` for every key, which connects the account through Facebook Login for Business and its linked Facebook Page. It publishes the same posts as Instagram Login, including carousels and Stories, sends and reads the same DMs, including attachments, interactive messages, and reactions, handles collaboration invites, and also deletes published posts, likes comments, and sets the paid-partnership label. It sends no `message_tag`, and `content_type` is optional on it. See [Connect through Facebook Login](/platforms/instagram#connect-through-facebook-login). **2026-10-16** Publishing ## Stagger post targets `POST /v1/posts` and `social_api_create_post` accept `stagger` with `min_seconds` and `max_seconds`. The targets publish one after another in the order listed, each a random delay in that range after the previous one, starting at `scheduled_at` or immediately with `publish_now`. The post is returned as `scheduled`, and each target's planned time is in `targets[].scheduled_at`. `stagger` on a draft, or outside `0 <= min_seconds <= max_seconds <= 86400`, returns `400 validation.field_invalid`. See [Stagger targets](/guides/scheduling#stagger-targets). **2026-10-16** TikTok ## TikTok creator connection `POST /v1/accounts/connect` for `tiktok` accepts `"metadata": {"connection_type": "creator"}`, which connects the account through TikTok's Content Posting API instead of TikTok API for Business, so a personal or creator account does not need to be a TikTok Business Account. A creator connection publishes posts and drafts, lists posts, and reads creator info and the account summary. It has no comments or mentions. Connects without `connection_type` are unchanged. See [Creator connection](/platforms/tiktok#creator-connection). **2026-10-16** YouTube ## YouTube comments turned off, and rate-limit status Listing the comments of a YouTube video with comments turned off returns `422 platform.youtube.comments_disabled` instead of `501 resource.not_supported`. Stop polling that video until the channel owner turns comments back on. When the YouTube daily quota is used up, calls return `429 platform.youtube.rate_limit` as documented, instead of `502 platform.youtube.api_error`. See [YouTube](/platforms/youtube). **2026-10-15** LinkedIn ## LinkedIn profile analytics (beta) `GET /v1/platforms/linkedin/insights` now returns `kind` (`organization` or `member`). A LinkedIn personal profile connected with `connection_type` `profile` returns its follower count and counts for its 5 most recent posts published through SocialAPI, and `GET /v1/accounts` returns `connection_type` on these accounts. The profile connection requires beta access: an account without it gets `403 billing.platform_not_available`. See [Profile analytics](/platforms/linkedin#insights). **2026-10-14** Publishing ## Platform options on post targets Each post target returns `platform_data` with the platform-specific options you set when the post was created or last updated, so a saved post reopens with its options. Chain continuation posts are returned in segments, not in `platform_data`. See [Publishing](/guides/publishing#where-platform-data-goes). **2026-10-13** Accounts ## Account permissions Send `permissions` on `POST /v1/accounts/connect` or `POST /v1/invites` to give a connected account only the permissions you need. `GET /v1/accounts` returns `permissions` and `effective_permissions`, and `PATCH /v1/accounts/{id}` changes them. A call the account lacks a permission for returns `403 account.permission_missing`, except publishing, which reports it on the target, and the other calls listed in [Account permissions](/guides/permissions#what-an-account-can-do-right-now). **2026-10-13** Documentation ## Documentation restructure The documentation has new URLs, grouped into Guides, Platforms, AI agents, and API reference. Old URLs redirect to the new pages. Start at [Supported platforms](/platforms/overview). **2026-10-13** Pinterest ## Pinterest boards AI agents can list a Pinterest account's boards with `social_api_list_pinterest_boards`. See [MCP tools reference](/integrations/tools). **2026-10-13** TikTok ## TikTok comments and mentions TikTok accounts support comments: list comments and replies on the account's posts, reply to them, like them, hide comments other people wrote, and delete comments the connected account wrote. A `comment.received` event fires when someone else comments, and a TikTok Business Account can list the posts and comments that mention it and reply to a mention made in a comment. On a TikTok account connected before 2026-10-13, comment and mention calls return `501 resource.not_supported` until you reconnect the account. See [TikTok comments](/platforms/tiktok#comments) and [TikTok mentions](/platforms/tiktok#mentions). **2026-10-13** Accounts ## TikTok connects through TikTok API for Business TikTok connects, invites, and reconnects authorize the account through TikTok API for Business. Accounts that were already connected keep working until their owner reconnects. See [TikTok](/platforms/tiktok). **2026-10-13** Publishing ## TikTok posts capped at 13 per 24 hours SocialAPI caps each TikTok account at 13 posts per 24 hours, below TikTok's limit of 15 per day. A post beyond the cap returns `429 publishing.velocity_limit` with a `Retry-After` header. See [Posting velocity caps](/guides/rate-limits#posting-velocity-caps). **2026-10-13** Billing ## Refused publishes and retries use no post credit A publish or retry that returns `429 publishing.velocity_limit` does not use a post credit. See [Posting velocity caps](/guides/rate-limits#posting-velocity-caps) and [Plans](/guides/plans). **2026-10-12** Instagram ## Per-post Instagram insights `GET /v1/posts/{pid}/metrics` now returns insights for an Instagram post, such as views, reach, interactions, and saves. See [Instagram live metrics](/platforms/instagram/publishing#live-metrics-and-insights). **2026-10-12** LinkedIn ## LinkedIn escapes reserved characters Characters LinkedIn reserves for formatting are escaped, so plain text appears as typed. Hashtags and mentions still use the syntax in [Text formatting](/platforms/linkedin/publishing#text-formatting). **2026-10-06** Accounts ## Sync cooldown per account `POST /v1/accounts/{id}/sync` allows 1 request per account every 5 minutes. A second request inside the window returns `429 accounts.sync_rate_limited`. See [Refresh an account's posts](/guides/analytics#refresh-an-accounts-posts). **2026-10-06** Instagram and Facebook ## Photos and videos on post targets Instagram and Facebook post targets return `platform_media`, the images and videos of the published post, including posts published outside SocialAPI. See [Instagram publishing](/platforms/instagram/publishing). **2026-10-06** Instagram and Facebook ## Up to 500 posts per Instagram account and Facebook Page Connecting an Instagram or Facebook account, and `POST /v1/accounts/{id}/sync`, cover up to the 500 most recent posts of an Instagram account and of each Facebook Page. See [Refresh an account's posts](/guides/analytics#refresh-an-accounts-posts). **2026-10-01** Threads ## Threads connect fixed in some regions Connecting a Threads account from some regions ended on the Threads home page instead of the authorization screen. Connect now opens the authorization screen from every region. See [Threads](/platforms/threads). **2026-09-30** WhatsApp ## Banned WhatsApp numbers cannot be connected Connecting a WhatsApp number that Meta has banned fails with `403 whatsapp.number_banned`. Reconnecting does not lift the ban. See [Errors](/guides/errors#whatsapp). **2026-09-30** WhatsApp ## Numbers Meta no longer serves are flagged for reconnection When Meta stops serving a WhatsApp number for a connection, the account is marked `reconnect_recommended` with the reason `whatsapp_number_unavailable`, and an `account.disconnected` webhook fires once. See [Account status](/guides/connect-accounts#account-status). **2026-09-29** Automations ## Multi-step Automations, buttons, and the Follow gate Automations run multi-step flows with postback buttons, and Instagram Automations can send a link only to people who follow the account. See [The Follow gate](/guides/automations#the-follow-gate). **2026-09-29** WhatsApp ## WhatsApp numbers that stay in the WhatsApp Business app A number that stays registered in the WhatsApp Business app can connect through Embedded Signup. Messages you send from the app appear in your inbox as outgoing messages, and imported message history arrives without firing webhooks. See [Keep using the WhatsApp Business app](/platforms/whatsapp#keep-using-the-whatsapp-business-app). **2026-09-29** Mentions ## Reply to Threads mentions `POST /v1/accounts/{id}/mentions/reply` now replies to a mention on Threads. Threads support is in beta. See [Reply to a mention](/guides/mentions#reply-to-a-mention). **2026-09-28** Automations ## Keyword Automations An Automation on an Instagram or Facebook account can react only to comments or messages that contain your keywords. See [Automations](/guides/automations). **2026-09-28** Threads ## Full Threads account insights Account insights on Threads now include likes, clicks, replies, reposts, quotes, and follower counts, along with follower demographics. Those values were missing from responses before. See [Threads insights](/platforms/threads#insights). **2026-09-27** Instagram ## Instagram Reels require a video Validation reports an error for a Reel draft that has no video. Use `content_type` `feed` for an image. See [Supported media](/platforms/instagram/publishing#2-supported-media). **2026-09-24** Instagram and Facebook ## More platform\_data options on Instagram and Facebook Instagram posts accept AI-generated labels, paid partnership, user tags, audio name, and thumbnail offset in `platform_data`. Facebook posts accept place, country targeting, link carousels, and video options. Validation reports a warning for any key the platform does not accept. See [Platform data fields](/platforms/instagram/publishing#platform-data-fields). **2026-09-22** Analytics ## Stored analytics history SocialAPI stores daily history for accounts, brands, and posts, and returns it from the account, brand, and post analytics endpoints. See [Analytics](/guides/analytics). **2026-09-22** Billing ## Billing block fields on /v1/users/me `GET /v1/users/me` returns `billing_blocked`, `billing_blocked_at`, and `billing_block_reason`. Use them to tell a customer why calls are refused and whether paying lifts the block. See [Past-due accounts](/guides/plans#past-due-accounts). **2026-09-22** Accounts ## Last used time for API keys API key responses include `last_used_at`, the time of the key's latest request, up to 1 minute out of date. The field is absent when the key has never been used. See [List keys](/guides/authentication#list-keys). **2026-09-21** Pinterest ## Pinterest posting limits A Pinterest publish target allows 1,000 posts per 24 hours and has no hourly cap. See [Posting velocity caps](/guides/rate-limits#posting-velocity-caps). **2026-09-20** Google Business Profile ## Google platform\_data keys Google Business Profile posts send only the `platform_data` fields that Google accepts. Other keys are dropped, so a key meant for another platform no longer fails the whole post. See [Platform data fields](/platforms/google/publishing#platform-data-fields). **2026-09-20** Inbox ## Comments report liked\_by\_viewer Each comment includes `liked_by_viewer`, which is `true` when the connected account liked it through SocialAPI. Instagram accounts connected with Facebook Login can like comments too. See [Like and unlike a comment](/guides/comments#like-and-unlike-a-comment). **2026-09-19** MCP ## Output schemas for MCP tools Every MCP tool declares an output schema, so a client can read the shape of each result before it calls the tool. See [MCP tools reference](/integrations/tools). **2026-09-17** Direct messages ## Reply to a specific message Messages carry `reply_to.platform_id` when they reply to an earlier message. See [Read messages](/guides/direct-messages#read-messages). **2026-09-17** Accounts ## Team role on /v1/users/me `GET /v1/users/me` returns `role`: `admin`, `developer`, or an empty string for the account Owner. See [Account permissions](/guides/permissions). **2026-09-16** Pinterest ## Pinterest Connect a Pinterest account to publish Pins to a board, and to list, create, rename, and delete its boards with `/v1/accounts/{id}/boards`. `GET /v1/platforms/pinterest/accounts/{id}/pins` lists the account's Pins, and `GET /v1/platforms/pinterest/accounts/{id}/summary` returns its follower, Pin, board, and view counts. See [Pinterest](/platforms/pinterest). **2026-09-14** Billing ## Profile packs on Empire Business (Empire) accounts add Profile packs automatically when their brand count reaches the scale threshold, 90% of the allowance by default, as Pro (Full Send) accounts do. See [Profile packs](/guides/plans#profile-packs). **2026-09-14** MCP ## Write tools declare readOnlyHint false MCP write tools send `readOnlyHint: false`, so a client knows which tools change data. See [MCP tools reference](/integrations/tools). **2026-09-12** Instagram ## Instagram story replies arrive as DMs A reply to one of your Instagram stories arrives as a `dm.received` event, with the story in the `story` field. See [Webhook events](/guides/webhook-events#direct-messages). **2026-09-10** Billing ## Automatic Profile packs on Full Send Pro (Full Send) accounts add a Profile pack of 50 brands automatically when brands reach the scale threshold, 90% of the allowance by default. See [Profile packs](/guides/plans#profile-packs). **2026-09-08** WhatsApp ## Register Cloud API numbers Register a WhatsApp Cloud API number with `POST /v1/platforms/whatsapp/phone-numbers/{id}/request-code`, `verify-code`, and `register`. Registration handles the two-step verification PIN. See [Connecting](/platforms/whatsapp#connecting). **2026-09-08** Facebook ## Move a Page between brands Assigning a Facebook Page to a brand moves it from the brand that holds it now. See [One login, many brands](/platforms/facebook#one-login-many-brands). **2026-09-07** Inbox ## Private reply eligibility errors A private reply that Instagram or Facebook refuses returns `platform.instagram.private_reply_ineligible` or `platform.facebook.private_reply_ineligible`. Examples are a comment older than the 7-day window, or a second private reply to the same comment. See [Send a private reply](/guides/comments#send-a-private-reply). **2026-09-06** YouTube ## YouTube suspended accounts When YouTube suspends or closes the account or its channel, every call for that account returns `403` with `platform.youtube.account_suspended`. See [Errors](/guides/errors#x-linkedin-youtube-and-zalo). **2026-09-06** Instagram and Facebook ## Button taps are saved as messages When someone taps a button in an Instagram or Facebook conversation, the tap is saved as an inbox message and raises `dm.postback`. See [Read messages](/guides/direct-messages#read-messages). **2026-09-06** Instagram and Facebook ## platform\_id on DM webhook events `dm.received`, `dm.sent`, and `dm.postback` carry `platform_id` in `data`. The value matches the `platform_id` of the same message in the API. See [Webhook events](/guides/webhook-events#direct-messages). **2026-09-05** Direct messages ## Closed messaging windows return the same error After Instagram or Facebook refuses a DM because the 24-hour messaging window has closed, later sends to that conversation return the same `400` error code as the first refusal, with `meta.blocked_locally` set to `true`. The block lifts when the person messages you again, or 24 hours after the refusal, whichever comes first. Sends that carry a `message_tag` are never blocked this way. See [The messaging window](/guides/direct-messages#the-messaging-window). **2026-09-05** WhatsApp ## Connect WhatsApp from your own page WhatsApp Embedded Signup runs in three legs: your backend starts the connection, Meta's popup collects the number, and your backend completes it. See [Connecting](/platforms/whatsapp#connecting). **2026-09-04** Publishing ## Posting velocity caps Each publish target can publish 25 posts per hour, and each platform has a 24-hour cap. Publishing past a cap returns `429 publishing.velocity_limit` with `Retry-After`, and creates nothing. A scheduled post is held until the window clears, not failed. See [Posting velocity caps](/guides/rate-limits#posting-velocity-caps). **2026-09-02** Accounts ## Re-sync an account's posts `POST /v1/accounts/{id}/sync` re-reads up to 100 of an account's most recent posts from the platform. See [Refresh an account's posts](/guides/analytics#refresh-an-accounts-posts). **2026-09-01** Threads ## Threads platform\_data warnings Threads draft validation reports a warning for each `platform_data` key it will ignore, so you can see which settings will not apply before you publish. See [Platform data fields](/platforms/threads/publishing#platform-data-fields). **2026-08-31** Google ## Update a Google Business Profile location `PUT /v1/platforms/google/accounts/{id}` changes a location's public profile: name, description, phone numbers, address, website, categories, opening hours, and service area. Only the fields you send are changed, and sending an empty value clears that field. Google moderates each edit, so a successful response means the edit was accepted for review. See [Update a location](/platforms/google/locations#update-a-location). **2026-08-31** Google ## Photos and videos on Google locations `GET`, `POST`, and `DELETE` on `/v1/platforms/google/accounts/{id}/media` manage the photos and videos on a location's public profile. Photos customers contributed can be read with `contributed=true`, but they cannot be deleted through the API. See [Profile media](/platforms/google/locations#profile-media). **2026-08-31** Google ## Google performance metrics and search keywords `GET /v1/platforms/google/accounts/{id}/insights` returns daily counts of how people found a location on Search and Maps and what they did next. `GET /v1/platforms/google/accounts/{id}/search-keywords` returns the monthly keywords that surfaced it. Google withholds exact counts for low-volume keywords, which come back as a threshold instead of a value. See [Performance metrics](/platforms/google#performance-metrics). **2026-08-31** Google ## Review reply moderation events Google moderates review replies. `review.updated` is sent when a reply is approved or rejected, with `change` set to `reply_approved` or `reply_rejected`. See [Review events](/platforms/google/reviews#review-events). **2026-08-31** Bluesky ## Bluesky Connect a Bluesky account with an app password to publish posts and read comments, mentions, and direct messages. Direct messages need an app password with direct message access. See [Bluesky](/platforms/bluesky). **2026-08-31** WhatsApp ## Send media with a caption and filename Messages can include media with an optional caption and filename. See [Sending media](/platforms/whatsapp#sending-media). **2026-08-31** WhatsApp ## Interactive messages Send a message with up to 3 reply buttons, or with a list menu. When a contact taps an option, it arrives as an inbound message in the same `messages` array as text and media. See [Interactive messages](/platforms/whatsapp#interactive-messages). **2026-08-31** WhatsApp ## Inbound WhatsApp media Media that contacts send is available from `GET /v1/inbox/media/{id}`, which returns the file. If the file is not ready yet, the call returns `504`, so retry it. See [Inbound media](/platforms/whatsapp#inbound-media). **2026-08-31** WhatsApp ## Template sends return a message ID A template send returns `message_id`, the SocialAPI ID of the message. See [Send a template](/platforms/whatsapp#send-a-template). **2026-08-30** Google ## Review replies show their moderation state A reply you send to a Google review returns `state`, which is `PENDING` until Google approves it. A reply is not published at the moment you send it. See [Replies are moderated](/platforms/google/reviews#replies-are-moderated). **2026-08-30** WhatsApp ## WhatsApp webhooks include account and conversation IDs Breaking: `dm.received` for WhatsApp now sets `account_id` to the SocialAPI account ID, and sets `conversation_id`. Before, `account_id` held the WhatsApp phone number ID, so update code that read it as one. See [dm.received](/guides/webhook-events#dmreceived). **2026-08-30** Facebook ## Private replies reject buttons A private reply that includes `buttons` returns `400 validation.field_not_supported` with `field` set to `buttons`, and the message asks you to send text only. Before, the buttons were dropped without an error. See [Private replies](/platforms/facebook#private-replies). **2026-08-29** Google ## Google logins show their account email `GET /v1/platforms/google/logins/{loginId}/locations` returns `login_name` and `login_email`, so you can tell Google logins apart. See [Connect another location to a brand](/platforms/google/locations#connect-another-location-to-a-brand). **2026-08-29** Google ## Assign new locations without reconnecting After a Google login is connected, its Business Profile locations can be assigned to brands without repeating OAuth. Locations added in Business Profile later appear in `GET /v1/platforms/google/logins/{loginId}/locations`. See [Connect another location to a brand](/platforms/google/locations#connect-another-location-to-a-brand). **2026-08-28** WhatsApp ## WhatsApp replies inside the 24-hour window send Replies to a contact inside the 24-hour service window are no longer rejected by SocialAPI with `whatsapp.window_closed`. WhatsApp decides whether a send is allowed. See [The 24-hour service window](/platforms/whatsapp#the-24-hour-service-window). **2026-08-28** Instagram and Facebook ## React to direct messages `POST /v1/inbox/conversations/{id}/messages/{messageId}/reaction` adds your account's emoji reaction to a message, and `DELETE` on the same path removes it. Send the emoji character itself. Supported on Instagram and Facebook Messenger. See [React to a message](/guides/direct-messages#react-to-a-message). **2026-08-24** YouTube ## Studio tags on published videos `PATCH /v1/platforms/youtube/accounts/{id}/videos/{videoId}` accepts `tags`, which sets the video's tags in YouTube Studio. See [Update post](/platforms/youtube/publishing#4-update-post). **2026-08-23** Inbox ## Refresh an account's inbox on demand `POST /v1/inbox/sync` refreshes one account's conversations, or one conversation when you send `conversation_id`. The call returns right away: poll `GET /v1/inbox/sync` until `state` is no longer `syncing`. Repeating a sync for the same target before its wait is over returns `429`, and `Retry-After` gives the seconds left. See [Refresh the inbox manually](/guides/inbox#refresh-the-inbox-manually). **2026-08-23** Threads ## Threads insights and profile lookup (beta) `GET /v1/platforms/threads/insights` returns engagement for one Threads post or for the account. `GET /v1/platforms/threads/profile-lookup` resolves a public profile by exact username. See [Insights](/platforms/threads#insights). **2026-08-23** Zalo ## Zalo is open to every account Zalo no longer needs a beta flag or an allowlist entry. See [Zalo](/platforms/zalo). **2026-08-23** Publishing ## A publish without an ID fails If Threads or Instagram accepts a publish without returning a media ID, or Google accepts a local post without returning its resource name, the target now fails instead of reporting success. See [Errors on a failed target](/platforms/threads/publishing#errors-on-a-failed-target). **2026-08-23** YouTube ## YouTube daily reply limit Each YouTube channel can post 5 comment replies per day, counted in Pacific time. A reply over the limit returns `validation.youtube_daily_reply_limit`. See [Comments](/platforms/youtube#comments). **2026-08-22** Facebook ## Reel metrics on post metrics `GET /v1/posts/{pid}/metrics` returns metrics for Facebook Reels. Before, Reel targets were dropped from the response without an error. See [Retrieve posts](/platforms/facebook/publishing#6-retrieve-posts). **2026-08-22** Publishing ## One media list on posts Post requests accept a single `media` array that mixes `url`, `media_id`, and `platform_attachment_id` items, in the order you send them. `platform_attachment_id` publishes only on Instagram, Threads, LinkedIn, LinkedIn Page, and X. `media_ids` still works but is ignored when `media` is set. See [Publish a platform attachment ID](/guides/media#publish-a-platform-attachment-id). **2026-08-21** Instagram ## Messaging participant profiles `GET /v1/platforms/instagram/accounts/{id}/participants/{participantId}/profile` returns a messaging participant's profile, with follow status in both directions. Instagram returns it only for users who messaged the account or tapped an icebreaker or persistent menu item. For anyone else the call returns `403 platform.instagram.230`: Instagram did not disclose the relationship, which does not mean the user does not follow the account. See [Participant profile](/platforms/instagram#participant-profile). **2026-08-20** Inbox ## Comment and review endpoints accept SocialAPI IDs Comment and review endpoints accept a SocialAPI interaction ID, such as `sapi_cmt_...` or `sapi_rev_...`, in place of the platform ID. See [What each endpoint accepts](/guides/ids#what-each-endpoint-accepts). **2026-08-19** Instagram ## Instagram account summary `GET /v1/platforms/instagram/accounts/{id}/summary` returns profile counts and trailing 30-day insight totals for an Instagram account, such as reach, views, interactions, engaged accounts, and net follower change. Accounts connected before the insights permission was requested return `403` until they are reconnected. See [Account summary](/platforms/instagram#account-summary). **2026-08-17** TikTok ## TikTok refusals return 4xx Some TikTok refusals now return `403`, and daily post limits return `429` so you know to back off. Before, these returned `502`. The `platform.tiktok.` error code is unchanged. See [Errors on a failed target](/platforms/tiktok/publishing#errors-on-a-failed-target). **2026-08-15** WhatsApp ## WhatsApp Business is open to every account WhatsApp Business no longer needs a beta flag or an allowlist entry. See [WhatsApp](/platforms/whatsapp). **2026-08-15** Accounts ## Redirect URIs keep their query string A redirect URI you register can include a query string. SocialAPI keeps it and adds its own callback parameters. If your URI has a parameter with the same name as a callback parameter, SocialAPI's value replaces yours. See [Connect accounts](/guides/connect-accounts). **2026-08-14** Accounts ## Deleting an account cancels its subscription `DELETE /v1/users/me` cancels the account's paid subscription before the account is deleted. If the cancellation fails, the deletion still goes through. See [Change or cancel your plan](/guides/plans#change-or-cancel-your-plan). **2026-08-13** Instagram ## Instagram collaboration invites Collaborators are invited when you publish, through `collaborators` in the platform data, and `GET /v1/platforms/instagram/accounts/{id}/media/{mediaId}/collaborators` lists them for the publishing account only. The invited account accepts or declines with `POST` on `collab/accept` or `collab/decline`. Invites expire 14 days after they are sent. See [Collaboration invites](/platforms/instagram#collaboration-invites). **2026-08-12** Billing ## Plan catalog and use case `GET /v1/billing/plans` returns the plan catalog: names, taglines, prices, limits, and which plan is marked popular. `GET /v1/users/me` returns `use_case`. See [The plan catalog](/guides/plans#the-plan-catalog). **2026-08-12** Webhooks ## Instagram comment events include the post permalink Instagram `comment.received` events include `permalink`, the public URL of the post. The field is optional and set only for Instagram comments. See [comment.received](/guides/webhook-events#commentreceived). **2026-08-12** Publishing ## Look up a post by its platform ID `GET /v1/posts/{pid}` and `GET /v1/posts/{pid}/metrics` accept a platform post ID as well as a SocialAPI post ID. See [Publishing](/guides/publishing). **2026-08-10** TikTok ## Send a TikTok video to the creator's inbox Set `post_mode` to `inbox` in the TikTok platform data to upload a video to the creator's TikTok inbox as a draft instead of publishing it. The `video.upload` scope is required. See [Draft in the TikTok app](/platforms/tiktok/publishing#draft-in-the-tiktok-app). **2026-08-09** Webhooks ## Replay protection headers Each webhook delivery includes `X-SocialAPI-Timestamp`, `X-SocialAPI-Delivery`, and `X-SocialAPI-Signature-V2`. The V2 signature is an HMAC-SHA256 over the timestamp, a `.` character, and the raw request body. Use them to reject replayed deliveries. See [Replay protection](/guides/webhooks#replay-protection). **2026-08-09** LinkedIn ## LinkedIn posts return a permalink LinkedIn and LinkedIn Page posts return `permalink`, the URL of the published post. It was empty before. See [Retrieve posts](/platforms/linkedin/publishing#6-retrieve-posts). **2026-08-08** Threads ## Threads publishes retry when the media is not ready A Threads publish that fails because its media container is not ready yet is retried before it is reported as failed. See [Errors on a failed target](/platforms/threads/publishing#errors-on-a-failed-target). **2026-08-06** Facebook ## Ineligible private replies return a stable error A private reply that Meta rejects because the comment is not eligible returns `platform.facebook.private_reply_ineligible`. The message explains that the 7-day window has passed or that the comment already received a private reply. See [Private replies](/platforms/facebook#private-replies). **2026-08-06** Facebook ## List every Facebook Page Listing a user's Facebook Pages returns every Page. Accounts with more than 25 Pages were cut off at 25. See [Pages of an account](/platforms/facebook#pages-of-an-account). **2026-08-04** TikTok ## TikTok profile fields in the account summary `GET /v1/platforms/tiktok/accounts/{id}/summary` returns the creator's display name, username, avatar, follower, following, likes, and video counts, and bio. See [Account summary](/platforms/tiktok#account-summary). **2026-07-31** Facebook and Instagram ## Specific error codes for Facebook messaging and Meta throttling Sending a Facebook message after the 24-hour messaging window closes returns `400 platform.facebook.window_closed`. Other refusals with Meta error code 10 return `403 platform.facebook.permission_denied`. See [The messaging window](/guides/direct-messages#the-messaging-window) and [Errors](/guides/errors). **2026-07-30** TikTok ## TikTok account summary and video sync `GET /v1/platforms/tiktok/accounts/{id}/summary` returns the profile and video statistics of a TikTok account. `POST /v1/platforms/tiktok/accounts/{id}/sync` re-syncs the account's videos. See [Account summary](/platforms/tiktok#account-summary) and [Sync videos](/platforms/tiktok#sync-videos). **2026-07-28** Announcements ## Service announcements `GET /v1/announcements` lists current service announcements, newest first, with a `seen` flag for the calling user. `POST /v1/announcements/{id}/seen` marks one as seen, and repeating the call is safe. Both calls are free and work with restricted API keys. See [Announcements](/guides/announcements). **2026-07-27** X / Twitter ## Images and video on X posts Posts to X accept up to 4 images, 1 video, or 1 GIF. Uploads need the `media.write` scope, so an account connected before this change must reconnect before its first upload. A failed upload fails the whole post; the text is not published alone. See [Supported media](/platforms/twitter/publishing#2-supported-media). **2026-07-27** X / Twitter ## Hide X replies X comments report `can_hide: true`, so you can hide them through the inbox moderation calls. Deleting X replies is still not offered, because X only allows deleting posts the account wrote. See [X comments](/platforms/twitter#comments). **2026-07-27** X / Twitter ## X comment lists A post's X comments no longer include the post itself as its first comment. A `limit` below X's minimum no longer returns `400`; the list is trimmed to the number you asked for. See [X comments](/platforms/twitter#comments). **2026-07-27** X / Twitter ## X DM replies go to the other person When the newest message in an X conversation was sent by your account, the conversation now resolves to the other participant. Before this fix, a reply in that case went to your own account. See [Direct messages](/platforms/twitter#direct-messages). **2026-07-27** X / Twitter ## X accounts stay connected after an unexplained 401 A 401 from X that does not name an invalid or expired token no longer marks the account as needing a reconnect. A token that X reports as invalid or expired still does. See [Account status](/guides/connect-accounts#account-status). **2026-07-17** Threads ## Threads error details from Meta A bare error code 10 from Threads returns `422 platform.threads.10` with Meta's message, subcode, and trace ID. Before, it returned "Operation not supported for this platform" and dropped those details. See [Errors](/guides/errors). **2026-07-16** WhatsApp ## WhatsApp Business (beta) WhatsApp Business is available in beta. Request access from the dashboard, then connect a phone number through Meta's signup flow to read and reply to messages, send message templates, and check the phone number status. See [Connecting](/platforms/whatsapp#connecting). **2026-07-14** Google Business Profile ## Google 403 responses do not require a reconnect A 403 from Google, such as posting disabled by a policy, returns `403 platform.google.access_denied` with Google's message. The account no longer needs a reconnect for it. Only a 401 from Google requires one. See [Account status](/guides/connect-accounts#account-status). **2026-07-12** API keys ## Edit and rotate API keys `PATCH /v1/keys/{id}` changes a key's name, scopes, and brand restrictions, and the key value stays the same. `POST /v1/keys/{id}/rotate` issues a new secret and stops the old one working immediately. The new secret is shown once. See [Scoped keys](/guides/scoped-keys). **2026-07-10** API keys ## Scoped and brand-restricted API keys `POST /v1/keys` accepts `scopes` and `brand_ids` to limit a key to the actions and brands you choose. `GET /v1/keys/scopes` lists the scopes you can assign. A call outside a key's scopes returns `403 auth.insufficient_scope`. See [Scoped keys](/guides/scoped-keys). **2026-07-10** Direct messages ## Stable DM conversation IDs A direct message conversation keeps the `conversation_id` first delivered for it. The ID no longer changes partway through a thread. See [Conversations](/guides/direct-messages#conversations). **2026-07-09** X / Twitter ## X accounts stay connected after a duplicate post is refused When X refuses a post as duplicate content, the account keeps working and does not need a reconnect. See [Account status](/guides/connect-accounts#account-status). **2026-07-06** Inbox ## brand\_id filters on posts, comments, and conversations `GET /v1/posts`, `GET /v1/inbox/comments`, and `GET /v1/inbox/conversations` now honor the `brand_id` parameter. Before, it was ignored and the results covered every brand. See [Concepts](/guides/concepts). **2026-07-06** Reviews ## Reviews are listed per account Breaking: `GET /v1/inbox/reviews` now returns your connected Google Business Profile accounts, each with its rating summary, including accounts with no reviews. To list reviews, call `GET /v1/inbox/reviews/{account_id}` for one account. See [The two list endpoints](/guides/reviews#the-two-list-endpoints). **2026-07-04** Google Business Profile ## Choose one Google Business Profile When a Google login manages more than one Business Profile, the connection stays in `selection_required` status until you choose one. `GET /v1/accounts/pending/{connection_id}` lists the candidates, and `POST /v1/accounts/pending/{connection_id}/select` connects the one you pick. The MCP tools `social_api_list_pending_profiles` and `social_api_select_pending_profile` do the same. See [Choose a Google Business Profile](/guides/connect-accounts#choose-a-google-business-profile). **2026-07-04** Google Business Profile ## Every Google account and location is listed Listing Google Business Profile accounts and locations now follows every page of results. Before, a long list was cut short, and the profiles past the cut could not be connected. See [Choose a Google Business Profile](/guides/connect-accounts#choose-a-google-business-profile). **2026-06-28** Zalo ## Zalo Official Account (beta) Zalo Official Account is available in beta. Connect an Official Account to read and send direct messages, and to create, update, list, and delete article posts. See [Zalo](/platforms/zalo). **2026-06-22** Mentions ## Reply to a mention as a public comment `POST /v1/accounts/{id}/mentions/reply` posts a public comment in reply to a mention on Instagram or Facebook, including on a third-party post where a normal comment reply is not allowed. Each reply uses 1 interaction credit. See [Mentions](/guides/mentions). **2026-06-20** Publishing ## Post type on each target Each target in a post response includes `media_type`, one of `feed`, `reel`, `stories`, or `carousel`. See [Publishing](/guides/publishing). **2026-06-18** Publishing ## Platform settings on the post `POST /v1/posts`, `PATCH /v1/posts/{pid}`, and `POST /v1/posts/validate` accept `platform_data` at the top level, keyed by platform name, for example `{"instagram": {"content_type": "feed"}}`. A target's own `platform_data` takes precedence over the top-level value. See [Publishing](/guides/publishing). **2026-06-17** Instagram and Facebook ## Mention events include the mention content `mention.received` events for Instagram and Facebook include the text, media, permalink, and author of the mention when the platform returns them. Any field can be empty. See [Webhook events](/guides/webhook-events). **2026-06-16** LinkedIn ## LinkedIn post options LinkedIn posts accept `platform_data` for `visibility`, `disable_reshare`, alt text for each image (`alt_text` or `alt_texts`), and `media_title`. Invalid values fail validation before the post is published. See [LinkedIn publishing](/platforms/linkedin/publishing). **2026-06-16** LinkedIn ## LinkedIn article and link posts LinkedIn article posts accept `article_url`, `article_title`, `article_description`, and a thumbnail image in `platform_data`. Posts can add a call-to-action button with `cta_label` and `cta_url`. See [LinkedIn publishing](/platforms/linkedin/publishing). **2026-06-16** Instagram ## Quick replies, buttons, and carousels in DMs `POST /v1/inbox/conversations/{id}/messages` accepts `quick_replies`, `buttons`, and `cards` for Instagram conversations. When a person taps a postback button on Instagram, SocialAPI sends a `dm.postback` webhook event. See [Direct messages](/guides/direct-messages). **2026-06-16** Instagram and Facebook ## Multiple attachments and replies in DMs A DM request can send more than one attachment, each as its own message, using `attachments` with a `source_type` of `url`, `media_id`, or `platform_attachment_id`. `reply_to_message_id` sends the first message as a reply to an earlier message. `attachment_url` is deprecated in favor of `attachments`. See [Direct messages](/guides/direct-messages). **2026-06-16** Instagram ## Read Instagram stories `GET /v1/platforms/instagram/accounts/{id}/stories` lists the account's active stories. It supports `limit` and `cursor`. See [Instagram](/platforms/instagram). **2026-06-14** Publishing ## Posts are validated before they are scheduled or published Scheduling or publishing a post runs the same checks as `POST /v1/posts/validate`. If the checks find errors, the request returns `400` with `publishing.validation_failed` and the list of issues. Send `skip_validation: true` to schedule or publish anyway. See [Publishing](/guides/publishing). **2026-06-14** Facebook ## Removed Facebook Pages are reported When a Facebook Page is removed or its access is revoked, SocialAPI sends a `page.removed` webhook event. If the account has no Pages left, its status becomes `reconnect_recommended`. See [Webhook events](/guides/webhook-events). **2026-06-14** LinkedIn ## LinkedIn comment authors include a headline and profile link Comments on LinkedIn Page posts include `author_headline` and `author_profile_url` when LinkedIn returns them. See [Comments](/guides/comments). **2026-06-11** Accounts ## Connect without a registered redirect URI `POST /v1/accounts/connect` accepts any valid `https` `redirect_uri`, or `http` on `localhost`, until you register at least one in the dashboard. After that, only registered URIs are accepted, and others return `400 validation.redirect_uri_not_whitelisted`. See [Connect accounts](/guides/connect-accounts). **2026-06-09** TikTok ## Choose the cover photo for TikTok photo posts Set `platform_data.photo_cover_index` on a TikTok photo post to choose which photo is the cover. The default is the first photo, index `0`. See [TikTok publishing](/platforms/tiktok/publishing). **2026-06-08** Instagram and Facebook ## Message tags on DMs `message_tag` on a DM request sends a message outside the standard 24-hour window on Instagram and Facebook. Instagram accepts only `HUMAN_AGENT`, which requires Meta's Human Agent access, and Facebook also accepts `ACCOUNT_UPDATE`, `CONFIRMED_EVENT_UPDATE`, `CUSTOMER_FEEDBACK`, and `POST_PURCHASE_UPDATE`. See [Direct messages](/guides/direct-messages). **2026-06-08** Threads and X ## Thread chains on Threads and X `segments` on `POST /v1/posts` adds up to 25 follow-up posts that publish as a native thread on Threads and X. Each segment takes `text`, plus `media_ids` on Threads (X publishes segment text only). The response lists the published segment IDs in `thread_post_ids`. See [Threads publishing](/platforms/threads/publishing) and [X publishing](/platforms/twitter/publishing). **2026-06-08** YouTube ## YouTube comments in the inbox list `GET /v1/inbox/comments` includes YouTube posts only when the request includes `account_id`. The YouTube list returns a single page with no cursor. See [Comments](/guides/comments). **2026-06-07** YouTube ## Moderate and edit YouTube comments `POST /v1/platforms/youtube/comments/{commentId}/moderate` sets a comment to `published`, `held_for_review`, or `rejected`. Set `ban_author` to `true` to ban the author when the status is `rejected`. `PATCH /v1/platforms/youtube/comments/{commentId}` changes the comment text. Both calls require `account_id` and `post_id`. See [YouTube](/platforms/youtube). **2026-06-07** Comments ## Filter comments by status, order, and keyword `GET /v1/inbox/comments/{postId}` accepts `moderation_status` (`published`, `held_for_review`, or `likely_spam`), `order` (`recent` or `relevance`), and `q` to search comment text. Other platforms ignore these parameters. See [Comments](/guides/comments). **2026-06-06** Telegram ## Telegram direct messages Telegram is available to all accounts. Connect a Telegram bot to read and send direct messages. See [Telegram](/platforms/telegram). **2026-06-06** YouTube ## YouTube channel summary, re-sync, and video edits `GET /v1/platforms/youtube/accounts/{id}/summary` returns the channel summary. `POST /v1/platforms/youtube/accounts/{id}/sync` re-syncs the channel's videos. `PATCH /v1/platforms/youtube/accounts/{id}/videos/{videoId}` edits a published video. See [YouTube](/platforms/youtube). **2026-06-04** Accounts ## Page IDs accept platform IDs Requests that take a page ID accept either the SocialAPI page ID (`sapi_page_...`) or the platform's own page ID. See [IDs](/guides/ids). **2026-06-03** Facebook ## Facebook Page details Facebook Page responses from `GET /v1/accounts/{id}/pages` include `followers_count`, `category`, `username`, `link`, `about`, `cover_photo_url`, `verification_status`, and `enriched_at`. SocialAPI fills these when a Page is connected and refreshes them over time. See [Connect accounts](/guides/connect-accounts). **2026-06-01** X ## X post settings in platform\_data X posts accept `platform_data` for `reply_settings`, `community_id`, `geo` and `geo_place_id`, `for_super_followers_only`, and `nullcast`. See [X publishing](/platforms/twitter/publishing). **2026-06-01** Facebook ## Publish Facebook Reels Set `content_type` to `reel` in the Facebook `platform_data` to publish a Reel. A Reel takes a single video. A Reel fails with `422` when Facebook reports that processing failed, and with `504` when processing does not finish in time. See [Facebook publishing](/platforms/facebook/publishing). **2026-05-31** LinkedIn ## LinkedIn member accounts Connect a LinkedIn member account to publish text, images, and videos as that member, with multi-image posts limited to images. Comments, reactions, and reading the member's posts are not supported, and the member reconnects every 60 days when LinkedIn access expires. See [LinkedIn publishing](/platforms/linkedin/publishing). **2026-05-30** Errors ## Send your own X-Request-ID Send an `X-Request-ID` header to name a request. A valid value comes back on the response, and SocialAPI generates one when you send none. Log it with each call so you can match a request to a support ticket. See [Debugging](/guides/debugging). **2026-05-26** Webhooks ## Post lifecycle webhook events Subscribe to `post.scheduled`, `post.published`, `post.partial`, `post.failed`, `post.updated`, `post.deleted`, `post.unpublished`, and `post.retried` to follow posts you create. Each event carries the post's SocialAPI ID. See [Webhook events](/guides/webhook-events). **2026-05-26** Direct messages ## Direct message status events `dm.status.sent`, `dm.status.delivered`, `dm.status.read`, and `dm.status.failed` report the delivery state of a message you sent. Which statuses arrive depends on the platform. See [Webhook events](/guides/webhook-events). **2026-05-26** Facebook ## Facebook multi-photo posts A Facebook post with 2 to 10 images publishes as one multi-photo album. Images and videos cannot be mixed in one album: the request fails with `media_mixed`, and more than 10 images fail with `media_too_many`. See [Facebook publishing](/platforms/facebook/publishing). **2026-05-26** Inbox ## Mention list pagination `GET /v1/accounts/{id}/mentions` returns `next_cursor` when more mentions exist. Pass it back to get the next page, and stop when the field is absent. See [Mentions](/guides/mentions). **2026-05-26** Inbox ## Author usernames Authors in interactions and webhook events include a `username` when the platform provides one, next to `name` and `avatar_url`. See [Webhook events](/guides/webhook-events#interaction-payload). **2026-05-25** Threads ## Threads carousel posts A Threads post with more than one media item publishes as a carousel. See [Threads publishing](/platforms/threads/publishing). **2026-05-25** Threads ## Threads like and reply counts Threads posts report like and reply counts taken from the platform. A count the platform does not return reads as 0. See [Threads publishing](/platforms/threads/publishing). **2026-05-25** Threads ## Threads reply deletion A Threads reply can be deleted only when the connected account wrote it, and the reply's capability flags now say so. Replies to your own posts can still be hidden. See [Comments](/guides/comments). **2026-05-25** Webhooks ## account.connected and account.disconnected events Subscribe to `account.connected` to hear when an account finishes connecting, and to `account.disconnected` when one is disconnected. Each event carries the account's details. See [Webhook events](/guides/webhook-events). **2026-05-21** Facebook ## Facebook connect requests business management access Connecting a Facebook account asks for the `business_management` permission, which lets SocialAPI list Pages owned through a Meta Business Portfolio. See [Facebook publishing](/platforms/facebook/publishing). **2026-05-20** Publishing ## Post text can live on each target `text` on `POST /v1/posts` is optional when every entry in `targets` sets its own `text`. Otherwise the top-level `text` is still required. See [Publishing](/guides/publishing). **2026-05-18** Publishing ## Post deletion reports each platform `DELETE /v1/posts/{pid}` returns `deleted` and a `results` list with one outcome per platform: `deleted`, `not_supported`, or `failed`. A platform that cannot delete a published post keeps its copy live. See [Publishing](/guides/publishing). **2026-05-18** TikTok ## TikTok comment, duet, and stitch are off unless you turn them on Set `disable_comment`, `disable_duet`, or `disable_stitch` to `false` in `platform_data` to allow an interaction. An omitted flag disables that interaction, and an interaction the creator's TikTok account has turned off stays off whatever you send. See [TikTok publishing](/platforms/tiktok/publishing). **2026-05-17** Errors ## Threads reconnect\_required error When Threads rejects a write because the connected account lacks a permission, the call returns `403` with `platform.threads.reconnect_required`. Reconnect the Threads account to grant the permission. See [Errors](/guides/errors). **2026-05-17** Instagram ## Instagram DM attachments and shared posts Instagram direct messages now carry their attachments, including shared posts and shared stories, in direct message events and message lists. See [Direct messages](/guides/direct-messages#attachments). **2026-05-17** TikTok ## Breaking: TikTok privacy level is required Breaking: `platform_data.privacy_level` must now be set on every TikTok post. SocialAPI no longer picks a default, so a post without it fails with `validation.privacy_level_required`. Send one of the values TikTok lists for the creator. See [TikTok publishing](/platforms/tiktok/publishing). **2026-05-13** YouTube ## YouTube open to every account YouTube no longer requires beta access, so any account can connect a YouTube channel. See [YouTube publishing](/platforms/youtube/publishing). **2026-05-08** Direct messages ## Direct message events include conversation and recipient DM events include `conversation_id`, the SocialAPI ID of the conversation, and `recipient`, the person who received the message. See [Webhook events](/guides/webhook-events). **2026-05-07** Facebook ## dm.sent and dm.referral events on Facebook `dm.sent` and `dm.referral` now fire for Facebook Page conversations too. See [Webhook events](/guides/webhook-events). **2026-05-06** Instagram ## dm.sent and dm.referral events `dm.sent` fires for messages the connected Instagram account sends. `dm.referral` fires when an Instagram conversation starts from an ad or link click. See [Webhook events](/guides/webhook-events). **2026-05-02** MCP ## Token endpoint accepts HTTP Basic client credentials `POST /oauth/token` accepts the client ID and secret in an `Authorization: Basic` header, as RFC 6749 allows. A `client_secret` in the body still works. See [OAuth](/integrations/oauth). **2026-05-02** MCP ## Token endpoint errors use OAuth error fields Errors from `POST /oauth/token` return `error` and `error_description` with the codes OAuth defines, such as `invalid_request`, `unsupported_grant_type`, and `invalid_client`. See [OAuth](/integrations/oauth). **2026-05-02** MCP ## Client registration returns expiry fields `POST /oauth/register` returns `client_id_issued_at` and `client_secret_expires_at`. A `client_secret_expires_at` of `0` means the secret does not expire. See [OAuth](/integrations/oauth). **2026-04-29** Accounts ## Send users back to your app after connecting Pass `redirect_uri` and `state` on `POST /v1/accounts/connect` to send the user to your own URL when the connection finishes. The URL receives `status`, `platform`, `account_id` or `account_ids`, and your `state` as query parameters, or `status=error` with an `error` code on failure. The `redirect_uri` must be on your allowed list in the dashboard, or the call returns `400 validation.redirect_uri_not_whitelisted`. See [Redirect parameters](/guides/connect-accounts#redirect-parameters). **2026-04-24** X / Twitter ## Connect X with your own developer app X connections can use your own X developer app. Save its client ID and secret in the dashboard, for your account or for one brand. If X rejects the saved credentials, the call returns `byok.credentials_invalid`. See [Use your own X app](/platforms/twitter/own-app). **2026-04-22** Instagram and Facebook ## Connect Instagram and Facebook without an access request Any account can connect Instagram and Facebook now. Neither platform needs an access request first. See [Instagram](/platforms/instagram) and [Facebook](/platforms/facebook). **2026-04-13** MCP ## Webhook tools for AI agents AI agents can read and test webhooks with `social_api_get_webhook` and `social_api_test_webhook`, and inspect deliveries with `social_api_list_webhook_deliveries`, `social_api_get_webhook_delivery`, and `social_api_retry_webhook_delivery`. See [MCP tools reference](/integrations/tools#webhooks). **2026-04-12** TikTok ## TikTok privacy level is optional `privacy_level` is optional on TikTok posts. When you leave it out, SocialAPI uses `SELF_ONLY` if TikTok allows it for the account, and otherwise the first option TikTok returns. Validation reports the default as a warning. Before this change, a missing `privacy_level` returned `400 validation.privacy_level_required`. See [Platform data fields](/platforms/tiktok/publishing#platform-data-fields). **2026-04-11** Webhooks ## Webhook delivery history and retries `GET /v1/webhooks/{id}/deliveries` lists the deliveries of a webhook endpoint, and `GET /v1/webhooks/{id}/deliveries/{did}` returns one. `POST /v1/webhooks/{id}/deliveries/{did}/retry` sends a delivery again, and `POST /v1/webhooks/{id}/test` sends a test event. Delivery history is kept for 3 days on Free, 7 days on Starter, 30 days on Pro, and 90 days on Business and Enterprise. See [Monitor deliveries](/guides/webhooks#monitor-deliveries) and [Test an endpoint](/guides/webhooks#test-an-endpoint). **2026-04-11** Events ## Event summary counts `GET /v1/events/summary` returns event totals, failure counts, and breakdowns by category and error category for a time window. Pass `from` and `to` as RFC 3339 timestamps. The `social_api_events_summary` MCP tool returns the same counts. See [Events, usage, and your SocialAPI account](/integrations/tools#events-usage-and-your-socialapi-account). **2026-04-11** Facebook ## Facebook post targets report their Page A Facebook post target includes `metadata` with the `page_id` and `page_name` of the Page that published it. See [Facebook publishing](/platforms/facebook/publishing). **2026-04-10** Instagram ## Instagram content types and validation Instagram posts take a `content_type` of `feed`, `reel`, `carousel`, or `stories`, and `POST /v1/posts/validate` checks it along with the media, `alt_text`, `collaborators`, and cover settings for each Instagram target. See [Instagram publishing](/platforms/instagram/publishing). **2026-04-08** Facebook ## Pages on Facebook accounts A Facebook account can manage more than one Page: `GET /v1/accounts/{id}/pages` lists them, and `PATCH /v1/accounts/{id}/pages/{pageId}` sets `is_default` or `is_active`. Pass `page_id` to choose the Page on post targets, on `GET /v1/posts`, and on the inbox endpoints. The MCP tools are `social_api_list_account_pages` and `social_api_update_account_page`. See [Facebook Pages](/guides/connect-accounts#facebook-pages). **2026-04-07** Errors ## Breaking: error responses have an error object Error responses now put the details in an `error` object: `{"error": {"code": "...", "message": "...", "meta": {}}}`. Before, `error` was a string and `code` sat at the top level. Read `error.code` for the machine-readable code and `error.message` for the text; `meta` appears only when there are extra details, such as the name of the field that failed. See [Handling errors](/guides/errors#handling-errors). **2026-04-07** Publishing ## Breaking: a post where every target fails returns 422 `POST /v1/posts` returns `422` when every target fails, instead of `502`. Partial failures still return `207`. If your code treats `502` as a failed publish, check for `422` instead. See [Publishing errors](/guides/publishing#errors). **2026-04-06** TikTok ## Creator info for TikTok accounts `GET /v1/accounts/{id}/creator-info` returns the privacy levels, interaction settings, and posting availability for a TikTok account. Read it before you publish. Other platforms return `501`. See [Creator info and validation](/platforms/tiktok/publishing#creator-info-and-validation). **2026-04-06** Publishing ## Validation checks TikTok targets `POST /v1/posts/validate` checks each TikTok target against TikTok's own rules for the account. Errors set `valid` to `false`, and warnings are returned without blocking the post. See [Validate before you publish](/guides/publishing#validate-before-you-publish). **2026-04-06** TikTok ## Choose the TikTok video cover frame TikTok video posts accept `video_cover_timestamp_ms` in `platform_data` to pick the frame used as the cover. See [Platform data fields](/platforms/tiktok/publishing#platform-data-fields). **2026-04-06** YouTube ## YouTube (beta) Connect a YouTube channel to publish, update, and delete videos, and to read, reply to, moderate, hide, and delete comments. Request access from the dashboard. See [YouTube](/platforms/youtube). **2026-04-05** YouTube ## More YouTube publishing options YouTube posts now apply these `platform_data` fields: `made_for_kids`, `license`, `embeddable`, `public_stats_viewable`, `publish_at`, `notify_subscribers`, `recording_date`, `default_language`, `playlist_id`, and `contains_synthetic_media`. The first comment is posted after upload, and `playlist_id` adds the video to that playlist. See [Platform data fields](/platforms/youtube/publishing#platform-data-fields). **2026-04-05** LinkedIn ## LinkedIn comment replies, likes, and deletes Replies, likes, and deletes work on LinkedIn comments, through the same endpoints as the other platforms. See [Comments](/guides/comments#list-comments-and-replies) and [LinkedIn](/platforms/linkedin). **2026-04-04** Comments ## Replies to a comment `GET /v1/inbox/comments/{postId}/{commentId}/replies` returns the replies to a comment, with cursor pagination. The response has the same shape as the top-level comment list. See [List comments and replies](/guides/comments#list-comments-and-replies). **2026-04-02** Analytics ## MCP tools for exports AI agents can create, list, and read analytics exports with `social_api_create_export`, `social_api_list_exports`, `social_api_get_export`, and `social_api_get_export_videos`. See [MCP tools reference](/integrations/tools#analytics-and-exports). **2026-04-01** Analytics ## Videos of an export `GET /v1/exports/{id}/videos` returns the videos of a completed export. See [Exports](/guides/analytics#exports). **2026-03-28** Analytics ## Analytics exports `POST /v1/accounts/{id}/export` starts an Excel export of an account's analytics. Fetch an export with `GET /v1/exports/{id}` or list them with `GET /v1/exports`. The number of exports per billing period depends on your plan. See [Analytics](/guides/analytics). **2026-03-27** Webhooks ## Event history `GET /v1/events` returns events for your account, such as account connections and disconnections. How far back you can query depends on your plan. **2026-03-27** Publishing ## Post logs replaced by events Breaking: `GET /v1/posts/logs` and `GET /v1/posts/{pid}/logs` are removed. Read event history from `GET /v1/events` instead. **2026-03-27** Accounts ## Invite links `POST /v1/invites` creates a single-use link that lets someone connect an account to a brand. List a brand's links with `GET /v1/invites` and revoke one with `DELETE /v1/invites/{id}`. The same actions are available as MCP tools. See [Connect accounts](/guides/connect-accounts). **2026-03-27** Accounts ## Platform access requests Some platforms need access approval before an account can connect to them. A connect call for a platform you have no access to returns `403` with the error code `platform_not_available`. Request access from the dashboard; TikTok is open to every account. See [Platforms](/platforms/overview). **2026-03-27** Authentication ## Authorization header without Bearer The `Authorization` header accepts a raw API key as well as `Bearer `. See [Authentication](/guides/authentication). **2026-03-27** Instagram ## Comment replies include parent\_id Instagram comment replies include `parent_id`, the ID of the comment they answer. See [Comments](/guides/comments). **2026-03-26** Accounts ## Brands A brand groups connected accounts. Manage brands with `GET /v1/brands`, `POST /v1/brands`, `PATCH /v1/brands/{id}`, and `DELETE /v1/brands/{id}`. Pass `brand_id` when you connect an account. See [Concepts](/guides/concepts). **2026-03-26** Billing ## Usage reports brands Breaking: `GET /v1/usage` returns `brands_used` and `brands_limit` in place of `accounts_used` and `accounts_limit`. Update clients that read the old names. See [Plans](/guides/plans). **2026-03-22** Inbox ## Per-account inbox routes removed Breaking: these routes are removed: `GET /v1/accounts/{id}/dms`, `GET /v1/accounts/{id}/reviews`, `GET /v1/accounts/{id}/posts/{pid}/comments`, `POST /v1/accounts/{id}/dms/{tid}/send`, `POST /v1/accounts/{id}/interactions/{iid}/reply`, and the moderation and reply-thread routes beside them. Use the `/v1/inbox` routes instead. `GET /v1/accounts/{id}/mentions` still works. See [Inbox](/guides/inbox). **2026-03-22** Inbox ## Unified inbox `GET /v1/inbox/conversations` lists direct message threads, and `GET /v1/inbox/comments` lists posts that have comments. Reply, hide, delete, like, and send private replies to comments from the same routes. Reviews are at `/v1/inbox/reviews`. See [Inbox](/guides/inbox) and [Direct messages](/guides/direct-messages). **2026-03-22** Publishing ## Post targets, validation, and import `POST /v1/posts` takes a `targets` list with one entry per account. Each entry can set its own `text`, `title`, `media_ids`, `first_comment`, `visibility`, `scheduled_at`, and `platform_data`. Check a draft with `POST /v1/posts/validate`, read platform limits with `GET /v1/posts/validate`, import many posts with `POST /v1/posts/import`, and unpublish a post with `POST /v1/posts/{pid}/unpublish`. See [Publishing](/guides/publishing). **2026-03-22** Publishing ## Platform options replaced by targets Breaking: `platform_options` is removed from post requests. Send `title`, `visibility`, `first_comment`, and `platform_data` on each entry in `targets` instead. See [Publishing](/guides/publishing). **2026-03-22** Facebook ## Facebook Pages inbox (beta) Comments and direct messages on Facebook Pages appear in the inbox, and you can reply to them from the same routes. Facebook is available to beta testers. See [Facebook](/platforms/facebook). **2026-03-20** Publishing ## Media library `GET /v1/media` lists your media, up to 100 items per page. `DELETE /v1/media/{id}` removes an item, and `GET /v1/media/storage` shows your storage use. Upload with `POST /v1/media/upload`. See [Media](/guides/media). **2026-03-20** Billing ## Unlimited posts and interactions on paid plans Paid plans no longer cap posts or interactions per billing period. The Free plan keeps its limits. See [Plans](/guides/plans). **2026-03-19** Publishing ## Publish right away `POST /v1/posts` without `scheduled_at` publishes right away and returns the post: `201` when it is published, `207` when some accounts fail, and `502` when it fails. Publishing that runs longer than 90 seconds reports a timeout error. See [Publishing](/guides/publishing). **2026-03-19** Publishing ## Reschedule and filter scheduled posts `PATCH /v1/posts/{pid}` changes the scheduled time and accounts of a scheduled post. `GET /v1/posts` supports filters and pagination. See [Scheduling](/guides/scheduling). **2026-03-15** Accounts ## Disconnect an account `DELETE /v1/accounts/{id}` disconnects an account. See [Connect accounts](/guides/connect-accounts). **2026-03-14** Publishing ## Retry a failed post `POST /v1/accounts/{id}/posts/{pid}/retry` publishes a post again after its publish failed. See [Publishing](/guides/publishing). **2026-03-14** TikTok ## TikTok (beta) Connect TikTok accounts and publish videos and photo carousels. TikTok is available to beta testers. See [TikTok](/platforms/tiktok). **2026-03-14** LinkedIn ## LinkedIn (beta) Connect LinkedIn accounts, read and moderate comments, and publish text and media posts. LinkedIn is available to beta testers. See [LinkedIn](/platforms/linkedin). **2026-03-14** Threads ## Threads (beta) Connect Threads accounts, read mentions and replies, and publish posts. Threads is available to beta testers. See [Threads](/platforms/threads). **2026-03-14** Google ## Google Business Profile (beta) Connect Google Business Profile locations. Google is available to beta testers. See [Google](/platforms/google). **2026-03-11** Billing ## Usage reports limits instead of credits Breaking: `GET /v1/usage` no longer returns `credits_used` and `credits_limit`. It returns usage and limits for accounts, posts, and interactions in the current billing period. See [Plans](/guides/plans). **2026-03-11** Facebook ## Facebook Pages publishing Create, update, and delete posts on Facebook Pages with `POST /v1/accounts/{id}/posts`, `PATCH /v1/accounts/{id}/posts/{pid}`, and `DELETE /v1/accounts/{id}/posts/{pid}`. See [Facebook publishing](/platforms/facebook/publishing). **2026-03-11** MCP ## Publishing from MCP MCP clients can create, update, delete, and retry posts, and upload media. See [MCP tools](/integrations/tools). **2026-03-10** Instagram ## Instagram publishing Publish to Instagram accounts with `POST /v1/accounts/{id}/posts`, and delete a published post with `DELETE /v1/accounts/{id}/posts/{pid}`. Media for a post starts with `GET /v1/media/upload-url`. See [Instagram publishing](/platforms/instagram/publishing). **2026-03-07** Facebook ## Facebook Pages Facebook Pages are available to connect. See [Facebook](/platforms/facebook). **2026-03-07** MCP ## Hosted MCP server The API hosts an MCP server at `/mcp`. MCP clients register and sign in with OAuth 2.1, and a consent page confirms access. See [OAuth](/integrations/oauth). **2026-03-07** Billing ## Annual billing Paid plans can be billed yearly at a 20% discount. See [Plans](/guides/plans). **2026-03-06** Comments ## Comments are scoped to a post Breaking: `GET /v1/accounts/{id}/comments` is replaced by `GET /v1/accounts/{id}/posts/{pid}/comments`. Use `GET /v1/accounts/{id}/posts` to find post IDs. See [Comments](/guides/comments). **2026-03-01** Initial release ## Initial release SocialAPI is open to public signups, with: * Instagram: connect accounts with `POST /v1/accounts/connect`, read comments, direct messages, and mentions, reply to comments, send direct messages, and moderate comments. * API keys: create, list, and revoke keys with `/v1/keys`. * Webhooks: create, list, update, and delete endpoints with `/v1/webhooks`. * Usage: `GET /v1/usage`, and account deletion with `DELETE /v1/users/me`. * Plans: paid plans with checkout and a billing portal in the dashboard. See [Quickstart](/quickstart). # Analytics Source: https://docs.social-api.ai/guides/analytics Read daily history for accounts, brands, and posts, fetch live post metrics from each platform, and refresh an account's posts with one request. Analytics in SocialAPI has 2 parts: stored daily history that SocialAPI keeps for accounts, brands, and posts, and live reads that fetch current figures from a platform. History never calls a platform, so it answers fast and works when a token has expired. Live reads show the platform's current numbers. Analytics requests cost no credits. An account needs the `analytics.read` [permission](/guides/permissions) for its history, its live post metrics, and its platform insights, and the `posts.read` permission for `POST /v1/accounts/{id}/sync`. Platform-specific insight endpoints, such as Instagram's account summary and Google Business Profile performance metrics, are on the platform pages linked in [Platform insights](#platform-insights). ## Endpoints | Method | Path | Description | | - | - | - | | `GET` | `/v1/accounts/{id}/analytics` | Stored daily history for one connected account | | `GET` | `/v1/brands/{id}/analytics` | Stored daily history for a brand, summed across its accounts | | `GET` | `/v1/posts/{pid}/analytics` | Stored daily history for one published post | | `GET` | `/v1/posts/{pid}/metrics` | Live engagement figures for a published post | | `POST` | `/v1/accounts/{id}/sync` | Refresh an account's posts and their counts now | ## Read stored history All 3 history endpoints take the same query parameters and return the same shape. | Parameter | Description | | - | - | | `from` | First day, `YYYY-MM-DD`, inclusive. Default: 29 days before `to`, so the default window is 30 days | | `to` | Last day, `YYYY-MM-DD`, inclusive. Default: today (UTC) | | `metrics` | Comma-separated metric names. Default: every stored metric | | `page_id` | Account endpoint only. Narrows the result to one Facebook Page of the account | ```bash theme={null} curl "https://api.social-api.ai/v1/posts/7c9e6679-7425-40de-944b-e07fc1f90ae7/analytics?from=2026-10-01&to=2026-10-03" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": { "from": "2026-10-01", "to": "2026-10-03", "series": [ { "metric": "post_likes", "kind": "gauge", "source": "sampled", "day_tz": "UTC", "points": [ { "day": "2026-10-01", "value": 40, "subjects_counted": 2 }, { "day": "2026-10-02", "value": 52, "change": 12, "days_spanned": 1, "subjects_counted": 2 }, { "day": "2026-10-03", "value": 61, "change": 9, "days_spanned": 1, "subjects_counted": 2 } ] } ] } } ``` | Field | Description | | - | - | | `from`, `to` | The range SocialAPI used, including the defaults | | `series[].metric` | The metric name | | `series[].kind` | `gauge` is a level you diff for growth. `flow` is a per-day count you sum for a period total | | `series[].source` | `reported` (the platform attributes the value to that day), `sampled` (SocialAPI read the value on that day), `owned` (SocialAPI counted its own actions), or `mixed` (a sum combines more than one) | | `series[].day_tz` | The time zone the days were cut on, or `mixed` | | `points[].day` | The day, oldest first | | `points[].value` | The figure for the day | | `points[].change` | The difference from the previous point. Present on a `gauge` only, and absent on the first point | | `points[].days_spanned` | How many days `change` covers. Larger than 1 when a day is missing. Divide `change` by it before you chart growth | | `points[].subjects_counted` | How many accounts, Pages, or post targets the value sums. At least 1 | Read `subjects_counted` before you chart a sum. A day on which fewer subjects reported adds up lower, and without the count a gap looks like a decline. A range longer than 400 days returns `400 validation.range_too_wide`. A `from` or `to` that is not `YYYY-MM-DD` returns `400 validation.from_invalid` or `400 validation.to_invalid`, and `to` before `from` returns `400 validation.range_inverted`. A `metrics` name SocialAPI does not store returns no series for it. A range with no stored points returns `"series": []`. ### What SocialAPI stores | Metric | Kind | Source | Endpoints | Meaning | | - | - | - | - | - | | `post_likes`, `post_comments`, `post_shares` | `gauge` | `sampled` | Post | The post's lifetime like, comment, and share counts, as read on that day. Days are UTC. The last reading of a day is kept | | `replies_sent` | `flow` | `owned` | Account, brand | Public replies to comments that SocialAPI sent for the account | | `mentions_answered` | `flow` | `owned` | Account, brand | Public replies to mentions | | `private_replies_sent` | `flow` | `owned` | Account, brand | Private replies to commenters | | `dms_sent` | `flow` | `owned` | Account, brand | DMs | | `reviews_answered` | `flow` | `owned` | Account, brand | Replies to reviews | | `replies_failed` | `flow` | `owned` | Account, brand | Replies, private replies, DMs, review replies, and mention replies that failed | | `comments_moderated` | `flow` | `owned` | Account, brand | Comments you hid, showed again, deleted, liked, or unliked | * **Post history:** SocialAPI adds a point each time it syncs the post's account: when you connect it, and when you call `POST /v1/accounts/{id}/sync`. An account without `analytics.read` adds no points. On every platform except X, LinkedIn member profiles, WhatsApp, and Pinterest, post history also gains points without a sync call. History starts when SocialAPI first tracks the post, so a post that was published long ago has no earlier points. A post with 2 or more targets returns one series per metric, summed across its targets. Pinterest posts are not synced, so they have no history. * **Action counts:** SocialAPI counts the replies, DMs, and moderation actions you send through the API, MCP tools, and Automations, per account and per day (UTC). Today's count can be up to 24 hours behind. An account without `analytics.read` is not counted. * **Platform metrics:** follower, reach, and view history from the platforms is not stored. Read current figures from the platform insight endpoints below. ### Account, brand, and post history * **Account:** `GET /v1/accounts/{id}/analytics` returns the account's series and, for an account with Pages, the series of its Pages summed. Add `page_id` for one Page. An unknown account returns `404 account.not_found`, an unknown Page returns `404 resource.not_found`, and an account without `analytics.read` returns `403 account.permission_missing`. * **Brand:** `GET /v1/brands/{id}/analytics` sums every account and Page in the brand per metric and day. The sum includes accounts you disconnected, so history does not disappear when an account disconnects. An account without `analytics.read` is left out of the sum. A brand-restricted key reads only its own brands. An unknown brand returns `404 resource.not_found`. `day_tz` is `mixed` when platforms cut days differently. * **Post:** `GET /v1/posts/{pid}/analytics` takes the SocialAPI post ID (a UUID), not the platform's post ID. A post that does not exist, or one your key cannot see, returns `404 resource.not_found`. A target whose account lacks `analytics.read` is left out of the total, and a post with no target left returns `403 account.permission_missing`. ## Read live post metrics `GET /v1/posts/{pid}/metrics` asks each platform for the post's current figures. It accepts the SocialAPI post ID or a platform post ID. A platform post ID that matches more than one post returns `409 ambiguous_post_id`: use the SocialAPI post ID. ```bash theme={null} curl https://api.social-api.ai/v1/posts/7c9e6679-7425-40de-944b-e07fc1f90ae7/metrics \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": { "post_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "tiktok", "status": "", "metrics": { "likes": 142, "comments": 23, "shares": 8, "saves": 0, "views": 5120, "extra": { "views": 5120 } }, "metrics_synced_at": "2026-10-13T09:30:00Z" }, { "account_id": "acc_01HZ9X4T6B8C2D5F7G9H1J3K", "platform": "pinterest", "error": { "code": "resource.not_supported", "message": "Pinterest does not report engagement counts on the pin list, so metrics cannot be refreshed", "category": "resource", "caused_by": "platform" } } ] } } ``` Each published target has `metrics` or, when its platform cannot report them, an `error`. The HTTP status stays 200 whatever the targets return. A target whose `status` is not `published` does not appear. The figures are written to the post, so `GET /v1/posts/{pid}` shows the latest values and `metrics_synced_at`. | Platform | Figures | | - | - | | Instagram | `likes`, `comments`, and insights such as `views`, `reach`, `interactions`, `saves`, and `shares`. See [Instagram live metrics](/platforms/instagram/publishing#live-metrics-and-insights) | | Facebook | `likes` (reactions), `comments`, `shares` | | Threads | `likes`, `comments` (replies). Both stay `0` for an account connected before 2026-10-17 until it is reconnected | | TikTok | `likes`, `comments`, `shares`, `saves` (always `0`), `views`, `extra.views` | | YouTube | `likes`, `comments`, `views`, `extra.views` | | X / Twitter | `likes`, `comments` (replies) | | Bluesky | `likes`, `comments` (replies), `shares` (reposts) | | Zalo | `shares`, `views`, `extra.views` | | LinkedIn | Not available: the target returns `501 resource.not_supported`. Account-level figures are in [LinkedIn insights](/platforms/linkedin#insights) | | Pinterest | Not available: the target returns `501 resource.not_supported` | | Google Business Profile | Local posts report no engagement counts, so every count is `0`. Use [performance metrics](/platforms/google#performance-metrics) | A figure a platform does not report is `0`, except Instagram insights, which are absent when Instagram does not report them. A target that cannot be read returns the platform's error, for example `404 resource.not_found` when the platform no longer returns the post. A target whose account lacks `analytics.read` returns `resource.not_found` in its `error`, with a message that says the account could not be loaded. ## Refresh an account's posts `POST /v1/accounts/{id}/sync` pulls the account's posts from the platform now, so new posts and fresh counts appear without waiting for the background sync. It returns when the refresh finishes. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/sync \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "success": true } ``` * **Depth:** Instagram covers up to the 500 most recent posts, Facebook up to the 500 most recent per Page, TikTok and Zalo the 20 most recent, and other platforms the 25 most recent. The TikTok platform route has its own cooldown: see [TikTok](/platforms/tiktok#sync-videos). * **Cooldown:** SocialAPI allows 1 sync per account every 5 minutes. The 5-minute window starts when you connect the account and on every sync request, including one that returns `501`. A request inside the window returns `429 accounts.sync_rate_limited`, with `Retry-After` and `error.meta.retry_after_seconds` set to the seconds left. * **Unsupported platforms:** a platform that cannot list posts, such as a LinkedIn member profile or WhatsApp, returns `501 resource.not_supported`. * **Pinterest:** `POST /v1/accounts/{id}/sync` returns 200 `{"success": true}` and refreshes nothing: Pins are read live. * **Other errors:** `404 account.not_found` for an account your key cannot reach, `403 account.permission_missing` for an account without `posts.read`, and `502` with a `platform.{platform}.*` code when the platform fails. TikTok and YouTube also have their own sync routes. See [TikTok](/platforms/tiktok#sync-videos) and [YouTube](/platforms/youtube). ## Platform insights These endpoints return live figures from one platform: | Platform | What you get | | - | - | | Facebook | Page followers, views, reach, and post engagements, as a 30-day summary and a daily series. See [Facebook Page insights](/platforms/facebook#page-insights) | | Instagram | Profile counts and 30-day insight totals. See [Instagram account summary](/platforms/instagram#account-summary) | | Threads | Post-level and account-level insights. See [Threads insights](/platforms/threads#insights) | | TikTok | Profile and counts. See [TikTok account summary](/platforms/tiktok#account-summary) | | YouTube | Channel counts. See [YouTube channel stats](/platforms/youtube#read-videos-and-channel-stats) | | Pinterest | Profile counts. See [Pinterest account summary](/platforms/pinterest#account-summary) | | Google Business Profile | Daily performance metrics and search keywords. See [Google performance metrics](/platforms/google#performance-metrics) | | LinkedIn | Follower count and per-post counts for a company Page or a member profile. `GET /v1/platforms/linkedin/insights`. See [LinkedIn insights](/platforms/linkedin#insights) | ## Scopes | Scope | Grants | | - | - | | `accounts:read` | `GET /v1/accounts/{id}/analytics` and `GET /v1/brands/{id}/analytics`, and the platform account summaries | | `posts:read` | `GET /v1/posts/{pid}/analytics` and `GET /v1/posts/{pid}/metrics` | | `accounts:manage` | `POST /v1/accounts/{id}/sync` | | `analytics:read` | The events log, exports, Threads insights, Threads profile lookups, and LinkedIn insights, not history. Refused to brand-restricted keys | A brand-restricted key can read history for its own brands only. See [Scoped API keys](/guides/scoped-keys). ## Exports Analytics exports build a report of an account's videos and their metrics. Exports per billing period, videos per export, and the wait between exports depend on your plan. See [History and export allowances](/guides/plans#history-and-export-allowances). ## Use from an AI agent | Tool | What it does | | - | - | | `social_api_get_account_analytics` | Reads an account's daily history | | `social_api_get_brand_analytics` | Reads a brand's daily history | | `social_api_get_post_analytics` | Reads a post's daily history | | `social_api_get_post_metrics` | Reads a post's live figures | `POST /v1/accounts/{id}/sync` has no tool. See [MCP tools](/integrations/tools). ## Related * [Track post delivery](/guides/delivery): post and target fields, including `metrics` * [Publish a post](/guides/publishing): create the posts you measure * [Plans and usage](/guides/plans): export allowances * [Account permissions](/guides/permissions): the `analytics.read` and `posts.read` permissions * [Scoped API keys](/guides/scoped-keys): scopes for analytics * [Errors](/guides/errors): every analytics error code # Announcements Source: https://docs.social-api.ai/guides/announcements Read SocialAPI maintenance windows, incidents, product updates, and breaking changes through the API, and mark each one as seen. Announcements are service notices that SocialAPI publishes: scheduled maintenance, incidents, product updates, and breaking changes. Every SocialAPI account receives the same announcements, and there is nothing to subscribe to. Poll `GET /v1/announcements` from your own app and show new notices to your team, so they hear about a maintenance window before it starts. Both endpoints cost no credits, and any valid key can call them, including a [scoped key](/guides/scoped-keys). There is no API to create, edit, or delete an announcement. ## Endpoints | Method | Path | Description | | - | - | - | | `GET` | `/v1/announcements` | List the announcements on display, newest first | | `POST` | `/v1/announcements/{id}/seen` | Mark an announcement as seen | ## List announcements ```bash theme={null} curl https://api.social-api.ai/v1/announcements \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "id": "9c1e7f2a-3b4d-4e5f-8a9b-0c1d2e3f4a5b", "kind": "maintenance", "title": "Scheduled maintenance", "body": "We are upgrading our servers. Publishing and inbox reads will be unavailable for up to 2 hours.", "link_url": "https://status.social-api.ai", "link_label": "Status page", "window_start": "2026-10-18T02:00:00Z", "window_end": "2026-10-18T04:00:00Z", "seen": false } ], "count": 1 } ``` An empty `data` array means there is nothing to announce. | Field | Description | | - | - | | `id` | The announcement's ID, a UUID | | `kind` | `maintenance`, `incident`, `product_update`, or `breaking_change` | | `title`, `body` | The notice's headline and text | | `link_url`, `link_label` | A page with more detail and the text for the link. Absent when the announcement has no link | | `window_start`, `window_end` | The announced downtime, in UTC. Absent when the announcement has no downtime window. Show them in the reader's time zone | | `seen` | `true` once your SocialAPI account marked the announcement as seen | An announcement is listed during its display period, which the API does not return. The display period is separate from `window_start` and `window_end`, which describe the announced downtime. ## Mark an announcement as seen ```bash theme={null} curl -X POST https://api.social-api.ai/v1/announcements/9c1e7f2a-3b4d-4e5f-8a9b-0c1d2e3f4a5b/seen \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "ok": true } ``` `seen` belongs to your SocialAPI account, not to a key. Marking an announcement as seen with any API key sets `seen` to `true` for every key of the account. Marking it again changes nothing and returns the same response. An ID that is not a UUID, or that matches no announcement, returns `404 resource.not_found`. No MCP tool exposes announcements. ## Related * [Status page](https://status.social-api.ai) for live incident status * [Changelog](/changelog) for every change to the API * [Authentication](/guides/authentication) for the keys that call these endpoints * [Scoped API keys](/guides/scoped-keys) for the endpoints every key can call, whatever its scopes # Authentication Source: https://docs.social-api.ai/guides/authentication Authenticate SocialAPI requests with an API key or an OAuth 2.1 access token, and create, list, rotate, and revoke API keys. Every request to `https://api.social-api.ai/v1` and to the MCP server at `https://api.social-api.ai/mcp` carries a token in the `Authorization` header: `Authorization: Bearer `. SocialAPI accepts two kinds of token: an API key, which you create and keep on your server, and an OAuth 2.1 access token, which an app receives after a SocialAPI user approves it. Most integrations use an API key. Managing keys costs no credits. ## Token types | Token | Format | Who uses it | How to get it | | - | - | - | - | | API key | `sapi_key_` followed by 62 hexadecimal characters | Your server, scripts, and AI agents you configure | Create it in the dashboard or with `POST /v1/keys` | | OAuth 2.1 access token | A signed token that does not start with `sapi_key_`. Valid for 1 hour | MCP clients such as Claude, ChatGPT, and Cursor, and apps that act for a SocialAPI user | The [OAuth flow for MCP clients](/integrations/oauth) | A full-access API key and an OAuth access token can call every public endpoint of your SocialAPI account. A [scoped API key](/guides/scoped-keys) can call only what its scopes and brands allow. The two token types work on the REST API and on the MCP server alike. ```bash theme={null} curl https://api.social-api.ai/v1/accounts \ -H "Authorization: Bearer $SOCAPI_KEY" ``` ## Endpoints | Method | Path | Description | | - | - | - | | `POST` | `/v1/keys` | Create a key | | `GET` | `/v1/keys` | List your keys, newest first, including revoked ones | | `PATCH` | `/v1/keys/{id}` | Change a key's name, scopes, and brands. The secret stays the same | | `POST` | `/v1/keys/{id}/rotate` | Issue a new secret for a key | | `DELETE` | `/v1/keys/{id}` | Revoke a key | | `GET` | `/v1/keys/scopes` | List the scopes a key can hold | Creating, listing, changing, rotating, and revoking keys needs full access: a full-access key, an OAuth access token, or the dashboard. A scoped key, including a key restricted only to brands, that calls these endpoints gets `403 auth.insufficient_scope`. `GET /v1/keys/scopes` is the exception: any valid key can call it. ## Create a key Create a key in the SocialAPI dashboard, or with the API: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/keys \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Production server"}' ``` Response (HTTP 201): ```json theme={null} { "id": "5b0e6f3a-8c2d-4e1f-9a7b-3c4d5e6f7a8b", "name": "Production server", "raw_key": "sapi_key_cd27569b0823266d6431e5431c09fd746edd48faa40aeb43e5e7ccd9a34fee", "message": "Store this key securely. It will not be shown again.", "scopes": [], "brand_ids": [] } ``` `raw_key` is shown once, in this response. Store it in a secret manager or an environment variable such as `SOCAPI_KEY`. `GET /v1/keys` shows only a preview afterwards, and no endpoint returns the secret again: a lost secret needs a [rotation](#rotate-a-key). | Field | Type | Required | Description | | - | - | - | - | | `name` | string | Yes | A label for the key. A missing or empty name returns `400 validation.field_required` | | `scopes` | string\[] | No | Scopes the key holds. Empty or absent: every scope. An unknown scope returns `400 validation.scope_invalid`. See [Scoped API keys](/guides/scoped-keys) | | `brand_ids` | string\[] | No | Brands the key can reach. Empty or absent: every brand. An ID that is not a UUID, or not one of your brands, returns `400 validation.brand_invalid` | ## List keys ```bash theme={null} curl https://api.social-api.ai/v1/keys \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "id": "5b0e6f3a-8c2d-4e1f-9a7b-3c4d5e6f7a8b", "name": "Production server", "preview": "sapi_key_cd275...", "is_active": true, "last_used_at": "2026-10-12T16:04:00Z", "created_at": "2026-09-30T08:15:00Z", "scopes": [], "brand_ids": [] } ], "count": 1 } ``` | Field | Description | | - | - | | `preview` | `sapi_key_` and the first 5 characters after it, followed by `...` | | `is_active` | `false` once the key is revoked. Revoked keys stay in the list | | `last_used_at` | The time of the key's latest request, up to 1 minute out of date. Absent when the key was never used or was rotated since | | `scopes`, `brand_ids` | The key's restrictions. Two empty arrays mean full access | ## Rotate a key Rotation issues a new secret and keeps the key's `id`, `name`, `scopes`, and `brand_ids`: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/keys/5b0e6f3a-8c2d-4e1f-9a7b-3c4d5e6f7a8b/rotate \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): the same fields as when you create a key, with the new `raw_key`. The old secret stops working at once, so update every service that uses it. `last_used_at` is cleared. Rotating a revoked key, or an ID that is not one of your keys, returns `404 resource.not_found`. To replace a secret with no downtime, create a second key with the same scopes and brands, deploy it to your services, then revoke the first key. Rotation cuts over at once and has no overlap. To change a key's name, scopes, or brands without a new secret, use `PATCH /v1/keys/{id}`, described in [Scoped API keys](/guides/scoped-keys#change-the-restrictions-on-a-key). ## Revoke a key ```bash theme={null} curl -X DELETE https://api.social-api.ai/v1/keys/5b0e6f3a-8c2d-4e1f-9a7b-3c4d5e6f7a8b \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "ok": true } ``` > [!WARNING] > > Revoking cannot be undone. The key stops working at once, and every request with it returns `401 auth.invalid_key`. Revoking a key that is already revoked returns `404 resource.not_found`. ## Authentication errors * No `Authorization` header returns `401 auth.missing_header`. * `Authorization: Bearer ` followed by nothing returns `401 auth.empty_token`. * A `sapi_key_` token that is unknown or revoked returns `401 auth.invalid_key`. * Any other token that is invalid or expired returns `401 auth.invalid_token`. * An OAuth access token whose user no longer exists returns `401 auth.user_not_found`. * A scoped key outside its scopes or brands, or calling key management, returns `403 auth.insufficient_scope`. * A failed payment on your SocialAPI account returns `402 billing.past_due` on every endpoint except every method of `/v1/users/me` and every `/v1/billing/*` endpoint. See [Plans](/guides/plans). Match on `error.code`, never on `error.message`. Every code is in [Errors](/guides/errors). The MCP server at `/mcp` answers the same failures with HTTP 401 (or 402 for a past-due account) and a body of the form `{"error": "invalid or inactive API key"}`, without the error envelope. The past-due 402 body also carries `code`: `billing.past_due`. ## Keep keys safe * **Use keys only on a server.** A key in a browser, a mobile app, or a public repository can be copied and used by anyone. * **Load keys from the environment.** Read `SOCAPI_KEY` from an environment variable or a secret manager, never from source code. * **Create one key per service.** Revoke one service's key without stopping the others. * **Give each key only what it needs.** Restrict keys to the [scopes and brands](/guides/scoped-keys) the service uses, so a leaked key cannot reach other clients or actions. * **Guard full-access keys most.** A full-access key can create more keys, including other full-access keys. * **Rotate a key you think leaked.** `POST /v1/keys/{id}/rotate` ends the old secret at once and keeps the key's restrictions. * **Revoke keys you no longer use.** Check `last_used_at` to find them. ## Use from an AI agent | Tool | What it does | | - | - | | `social_api_list_keys` | Lists your API keys | | `social_api_revoke_key` | Revokes an API key | See [MCP tools](/integrations/tools) for every tool. ## Related * [Scoped API keys](/guides/scoped-keys) for keys limited to some scopes and brands * [OAuth for MCP clients](/integrations/oauth) for the OAuth 2.1 flow and its tokens * [The MCP server](/integrations/overview) for connecting AI clients with a key or OAuth * [Errors](/guides/errors) for the full `auth.*` list * [Rate limits](/guides/rate-limits) for how many requests you can send # Automations Source: https://docs.social-api.ai/guides/automations Answer matching comments and DMs on Instagram and Facebook with rules you set once through the API, with no code running on your side. An Automation is a rule on one Instagram or Facebook account. When a comment or a DM on that account matches the rule's filters, SocialAPI carries out its actions itself: a private reply with a link, a public reply, a hidden comment, a DM. You configure it once through the API or an AI agent, and there is no server, webhook handler, or queue to run on your side. The classic use is comment-to-DM: someone comments "link" on a post and gets the link in their DMs. With buttons and steps, the same rule can offer a small menu or ask people to follow the account before they get the link. Each Automation action costs what the same API call costs, and the same platform rules apply: the [messaging window](/guides/direct-messages#the-messaging-window) and one [private reply](/guides/comments#send-a-private-reply) per comment. ## Endpoints | Method | Path | Description | | - | - | - | | `POST` | `/v1/automations` | Create an Automation | | `GET` | `/v1/automations` | List your Automations, newest first. Filter to one account with `account_id` | | `GET` | `/v1/automations/{id}` | Get one Automation with its counters | | `PATCH` | `/v1/automations/{id}` | Update the fields you send, or pause and resume | | `DELETE` | `/v1/automations/{id}` | Delete an Automation and its run history | | `GET` | `/v1/automations/{id}/firings` | List the Automation's runs from the last 30 days | Configuring Automations is free. Only the actions they carry out can cost credits. ## Your first Automation This Automation sends a private reply with a link to anyone who comments "link" on any post of an Instagram account. A private reply is free. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/automations \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "name": "Send the link", "trigger": "comment", "once_per_person": true, "steps": [ { "key": "start", "filters": [ { "type": "keyword", "keyword": { "keywords": ["link"], "match": "contains" } } ], "actions": [ { "type": "private_reply", "private_reply": { "texts": [ "Here is the link: https://example.com/spring", "Thanks for asking! Here you go: https://example.com/spring" ] } } ] } ] }' ``` Response (HTTP 201): ```json theme={null} { "id": "aut_01J9Z8Y7X6W5V4T3S2R1Q0P9N8", "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "name": "Send the link", "status": "active", "trigger": "comment", "once_per_person": true, "steps": [ { "key": "start", "filters": [ { "type": "keyword", "keyword": { "keywords": ["link"], "match": "contains" } } ], "actions": [ { "type": "private_reply", "private_reply": { "texts": [ "Here is the link: https://example.com/spring", "Thanks for asking! Here you go: https://example.com/spring" ] } } ] } ], "fired_count": 0, "sent_count": 0, "failed_count": 0, "created_at": "2026-10-02T09:00:00Z", "updated_at": "2026-10-02T09:00:00Z" } ``` The Automation is `active` when the request returns. A comment such as "Can I get the LINK?" now gets one of the two texts, picked at random, and `once_per_person` makes sure each commenter gets it only once. To stop it, pause it. Send `active` to resume: ```bash theme={null} curl -X PATCH https://api.social-api.ai/v1/automations/aut_01J9Z8Y7X6W5V4T3S2R1Q0P9N8 \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "status": "paused" }' ``` ## How an Automation runs When a comment or DM arrives on the account, every active Automation with a matching trigger is checked on its own: 1. Comments and messages from the account itself are skipped. 2. For a comment trigger with `post_ids`, the comment must be on one of those posts. 3. The `start` step's filters must all pass. If they do not, nothing happens and no run is recorded. 4. Unless `once_per_person` or the [hourly cap on dm Automations](#limits-and-safety) stops it, the run is recorded as a [Firing](#firings) and the `start` step's actions are carried out. If 2 or more Automations match the same event, each one runs. There is no priority between them, so avoid two Automations that send a private reply to the same comments: Meta allows one private reply per comment, and the second one fails. ## Triggers | Trigger | Fires on | Actions allowed on the start step | | - | - | - | | `comment` | A new comment on any post of the account, or on the posts listed in `post_ids` | `hide_comment`, `reply_comment`, `private_reply` | | `dm` | A new DM to the account, the same messages your webhooks receive as [`dm.received`](/guides/webhook-events#dm-received) | `send_dm` | Fields that shape the trigger: | Field | Description | | - | - | | `trigger` | `comment` or `dm`. Fixed at creation | | `post_ids` | Comment trigger only. Up to 100 platform post IDs, each 1 to 100 characters, the same value as `platform_post_id` on a [`comment.received`](/guides/webhook-events#comment-received) event. Omit it to cover every post. On update, an empty list means every post | | `once_per_person` | Reach each person at most once. Defaults to `false`. A run in which every action failed does not count, so that person can trigger it again. Turning it on later also covers the people the Automation already reached | A `dm` trigger fires on every `dm.received`, which can include story replies and story mentions on Instagram. Add a keyword filter to the `start` step when you only want to answer certain messages. Facebook sends an edited comment again as a new event. Automations answer new comments only, so an edit never triggers one. ## Steps An Automation is a list of 1 to 10 steps. Every Automation has one step keyed `start`, which runs when the trigger fires. Other steps run only when the person taps a button, or when a step's filters fail and it falls through to another step. | Field | Description | | - | - | | `key` | Unique within the Automation, 1 to 32 characters of `a-z`, `0-9` and `_`. The entry step is `start` | | `filters` | Up to 10 [filters](#filters). All must pass. No filters means every event passes | | `actions` | At least one [action](#actions), at most one of each type | | `buttons` | Up to 3 [buttons](#buttons-and-multi-step-flows), shown under the step's `private_reply` or `send_dm` message | | `fallthrough_step` | Key of the step to run when this step's filters fail. Not allowed on `start`. Fallthrough links must not form a cycle | > [!WARNING] > > A `start` step with no filters answers every comment, or every DM, on the account. Add a keyword filter unless that is what you want. ## Filters ### keyword Matches the comment or message text against a list of keywords. ```json theme={null} { "type": "keyword", "keyword": { "keywords": ["link", "price list"], "match": "contains" } } ``` | Field | Description | | - | - | | `keywords` | 1 to 50 keywords, each 1 to 100 characters. The filter passes when any keyword matches | | `match` | `contains`: a keyword appears in the text as a whole word or phrase. `exact`: the whole text equals a keyword | How matching works: * **Case, accents, and spacing are ignored.** "LINK", "Link" and "link" all match `link`, and "cafe" matches "Café". Runs of spaces count as one space. * **`contains` matches whole words.** `link` matches "send me the link!" but not "linked". A keyword of 2 or more words matches that phrase. An apostrophe separates words, so `info` matches "l'info". * **`exact` compares the whole text.** `link` matches "Link" but not "link please" or "link!". * **Symbols and emoji need no boundary.** On a side where a keyword starts or ends with a symbol or emoji, it matches with no space there: `#promo` matches "join#promo", and 🔥 matches wherever it appears. * **Unspaced scripts match anywhere.** Chinese, Japanese, Korean, Thai, Lao, Khmer and Burmese are written without spaces between words, so a keyword in those scripts matches wherever it appears in the text. * **Accents are ignored only where they are optional to a reader**: in Latin, Greek, Cyrillic, Arabic and Hebrew text. Elsewhere a mark changes the word (Japanese voicing marks, Thai tone marks), so it must match. ### follows\_account Passes when the person follows the account. Instagram only. ```json theme={null} { "type": "follows_account" } ``` * It cannot be used on `start`. Instagram shares whether someone follows the account only after they tap a button or message it, and a comment does not count. Put it on a step reached by a button. * It is checked live when the step runs. A person whose follow status is unknown is treated as following. Only a definite "does not follow" fails the filter, because denying a real follower costs you more than letting one non-follower through, and Instagram can be slow to report a fresh follow. * On a Facebook account it is refused with `400 validation.filter_unsupported`: Facebook does not tell apps whether a person follows a Page. ## Actions Actions run in a fixed order, whatever order you write them in: `hide_comment`, `reply_comment`, `private_reply`, `send_dm`. A failed action does not stop the next one, and a failed action is not retried. | Action | What it does | Allowed on | Cost | | - | - | - | - | | `hide_comment` | Hides the comment | `start` of a comment Automation | Free | | `reply_comment` | Posts a public reply to the comment | `start` of a comment Automation | 1 interaction credit | | `private_reply` | Sends a DM to the commenter | `start` of a comment Automation | Free | | `send_dm` | Sends a DM in the conversation | `start` of a dm Automation, and every step after `start` | 1 interaction credit | Each cost is the cost of the same API call. See [what counts as an interaction](/guides/plans#what-counts-as-a-post-or-interaction). Set the field named by `type` with the texts to send. `hide_comment` takes no field: ```json theme={null} [ { "type": "hide_comment" }, { "type": "reply_comment", "reply_comment": { "texts": ["Sent you a DM!"] } }, { "type": "private_reply", "private_reply": { "texts": ["Here is the link: https://example.com"] } } ] ``` `texts` holds 1 to 10 variants, each 1 to 1,000 characters, and one is picked at random each time the action runs. On Instagram, a private reply or DM text above 1,000 bytes of UTF-8 is refused by Instagram, and the failure is recorded on the Firing. When the step has buttons, each `private_reply` and `send_dm` text is at most 640 characters. Steps after `start` run after a button tap, which opens the messaging window but has no comment to act on, so they can use only `send_dm`. ## Buttons and multi-step flows Buttons appear under the step's `private_reply` or `send_dm` message. A step with buttons must have one of those two actions. On Facebook, the buttons of a `send_dm` action are not delivered, because a Facebook DM send delivers only the text. The buttons on the first `private_reply` are delivered. | Field | Description | | - | - | | `type` | `url` opens a link. `postback` runs another step when tapped | | `title` | Up to 20 characters | | `url` | An `http` or `https` URL. Required for a `url` button | | `next_step` | Key of the step to run when the button is tapped. Required for a `postback` button | This comment Automation answers "menu" with a private reply carrying three buttons. Two of them lead to steps that send a DM: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/automations \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "name": "Menu", "trigger": "comment", "steps": [ { "key": "start", "filters": [ { "type": "keyword", "keyword": { "keywords": ["menu"], "match": "contains" } } ], "actions": [ { "type": "private_reply", "private_reply": { "texts": ["Hi! What would you like to know?"] } } ], "buttons": [ { "type": "postback", "title": "Prices", "next_step": "prices" }, { "type": "postback", "title": "Opening hours", "next_step": "hours" }, { "type": "url", "title": "Website", "url": "https://example.com" } ] }, { "key": "prices", "actions": [ { "type": "send_dm", "send_dm": { "texts": ["Our price list: https://example.com/prices"] } } ] }, { "key": "hours", "actions": [ { "type": "send_dm", "send_dm": { "texts": ["We are open Monday to Saturday, 9am to 7pm."] } } ], "buttons": [ { "type": "postback", "title": "Prices", "next_step": "prices" } ] } ] }' ``` A tap on **Prices** runs the `prices` step and sends 1 DM (1 interaction credit). The **Opening hours** answer offers **Prices** again, since buttons can loop: each loop needs a person to tap. Rules for the graph: * Every `next_step` and `fallthrough_step` must name a step that exists. * On a comment Automation, a button cannot lead back to `start`. * A step reached by a tap checks its own filters when the tap arrives. A tap carries no text, so a keyword filter on a step after `start` never passes. Use `follows_account` there, and `fallthrough_step` for what to do when it fails. A tap has no expiry: it is honored whenever it arrives, weeks later included, and runs the step's current content, so an edit applies to buttons already sent. A tap on a paused or deleted Automation, or on a step that no longer exists, does nothing. Taps have no limit. Each costs what its actions cost, and taps are exempt from `once_per_person` and from the hourly cap on dm Automations: they continue the run the trigger started. ## The Follow gate The Follow gate sends a link only to people who follow the account. It is a shortcut for Instagram comment Automations: send `follow_gate` instead of `steps`, and SocialAPI builds the steps for you. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/automations \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "name": "Link for followers", "trigger": "comment", "once_per_person": true, "follow_gate": { "keywords": ["link"], "opening_texts": ["Tap below to get the link"], "button_title": "Get the link", "link_texts": ["Here you go: https://example.com"], "not_following_texts": ["Follow us first, then tap again"], "retry_button_title": "I followed" } }' ``` | Field | Description | | - | - | | `keywords` | Comment keywords that open the gate, matched with `contains` | | `opening_texts` | Private reply sent to the commenter, under a button | | `button_title` | The button under the private reply, up to 20 characters | | `link_texts` | Sent when the person follows the account | | `not_following_texts` | Sent when the person does not follow the account yet, under a retry button | | `retry_button_title` | The retry button, up to 20 characters | The gate is stored as three ordinary steps: | Step | Filters | Action | Next | | - | - | - | - | | `start` | keyword, on `keywords` | `private_reply` with `opening_texts` | Button `button_title` leads to `check` | | `check` | `follows_account` | `send_dm` with `link_texts` | Falls through to `not_following` when the person does not follow | | `not_following` | none | `send_dm` with `not_following_texts` | Button `retry_button_title` leads back to `check` | `GET` returns these steps, with no trace of `follow_gate`, so you edit a gate like any other Automation. Sending `follow_gate` together with `steps`, or with the `dm` trigger, returns `400 validation.follow_gate_invalid`. On a Facebook account it returns `400 validation.filter_unsupported`, since the gate relies on `follows_account`. ## Limits and safety | Limit | Value | | - | - | | Automations per account | 50 (`409 automation.limit_reached`) | | Platforms | Instagram and Facebook accounts (`422 automation.platform_unsupported` otherwise) | | Name | 1 to 100 characters | | Steps per Automation | 1 to 10 | | Filters per step | 10 | | Keywords per keyword filter | 1 to 50, each 1 to 100 characters | | Text variants per action | 1 to 10, each 1 to 1,000 characters, or 640 for a `private_reply` or `send_dm` on a step with buttons | | Buttons per step | 3, each title up to 20 characters | | `post_ids` | Up to 100 | | Runs of one dm Automation for one person | 5 per hour, button taps not counted | | Firings history | 30 days | What keeps an Automation from misbehaving: * **Your own activity never triggers it.** Comments and messages sent by the account itself are skipped, so an Automation's replies cannot trigger it again. On Facebook the account is the Page: a Page admin commenting from their personal profile does trigger it. * **A dm Automation answers one person at most 5 times an hour.** This stops two auto-responders from replying to each other forever. Button taps are not counted. * **Each event runs at most once per Automation**, even when the platform delivers it twice. * **The account needs the permissions the Automation uses.** Creating an Automation, changing its `steps`, or setting it to `active` returns `400 validation.permission_missing` when its trigger or an action needs a [permission](/guides/permissions#automations) outside the account's `effective_permissions`. `error.meta.permission` names it. An Automation is not paused when the account later loses a permission: its actions fail on the Firing with `account.permission_missing`. * **A past-due account gets no actions.** While your subscription is [past due](/guides/plans#past-due-accounts), Automations do nothing and record no Firings, the same way the API returns `402`. * **Platform rules still apply.** An action the platform or SocialAPI refuses (a private reply on a comment older than 7 days, a DM outside the messaging window, an exhausted interaction allowance) fails with the same error code the API call returns, and the failure is recorded on the Firing. ## Firings A Firing is one run of an Automation against one event: a comment, a DM, or a button tap. It records the step that ran and the outcome of each action. Firings are kept for 30 days. A `once_per_person` Automation keeps its record of who it reached after the Firings expire, so it never answers the same person twice. ```bash theme={null} curl "https://api.social-api.ai/v1/automations/aut_01J9Z8Y7X6W5V4T3S2R1Q0P9N8/firings?limit=20" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` ```json theme={null} { "data": [ { "id": "afr_01J9Z9A2B3C4D5E6F7G8H9J0K1", "event_id": "17858893269577012", "person_id": "17841400000000000", "step_key": "start", "outcomes": [ { "action": "reply_comment", "status": "sent" }, { "action": "private_reply", "status": "failed", "error_code": "platform.instagram.private_reply_ineligible" } ], "created_at": "2026-10-02T09:14:03Z" } ], "next_cursor": "" } ``` | Field | Description | | - | - | | `event_id` | The platform's own ID for the comment, message, or button tap | | `person_id` | The platform ID of the person who commented, messaged, or tapped | | `step_key` | The step whose actions ran. When no step's filters passed (for example a failed `follows_account` with no fallthrough), the step the event entered at, with no outcomes | | `outcomes` | One entry per action: `status` is `sent` or `failed`, and a failure carries `error_code` | Firings are listed newest first. `limit` is 1 to 100 (default 20); pass `next_cursor` as `cursor` to fetch the next page, and stop when it is empty. The Automation itself carries running totals: `fired_count` (runs, taps included), `sent_count` (actions that succeeded) and `failed_count` (actions that failed). ### Debug a failed action 1. `GET /v1/automations/{id}` and check `failed_count`. 2. List the Firings and find an outcome with `"status": "failed"`. 3. Look up its `error_code` in [Errors](/guides/errors). It is the code the same API call would have returned, so it means the same thing. For example, `platform.instagram.private_reply_ineligible` means the comment is older than 7 days or already had a private reply, perhaps from another Automation or from your own API call. `platform.instagram.window_closed` means a `send_dm` went outside the messaging window. 4. Fix the cause, for example by removing an overlapping Automation. The failed action is not retried. ### When nothing fired A comment or DM that records no Firing at all was skipped before any action ran. Check, in order: * The Automation is `active`. * The account has the permission for the trigger: `comments.read` for a comment Automation, `messages.read` for a dm Automation. Without it, the platform's events are dropped before they reach the Automation. * For a comment trigger with `post_ids`, the comment is on one of those posts. * The text passes the `start` step's filters. A non-matching event records nothing. * With `once_per_person`, the person has not been reached already. * For a dm Automation, it has not already run 5 times for that person in the last hour. * The comment or message is not from the account itself (on Facebook, the Page). * The event is not an edited Facebook comment. * Your subscription is not past due. Automations run on the same inbound events as your `comment.received` and `dm.received` webhooks. If those stop arriving for an account, for example because it needs reconnecting, its Automations stop too. ## Scopes Two [scopes](/guides/scoped-keys#scopes) cover Automations themselves: | Scope | Grants | | - | - | | `automations:read` | List and get Automations, list their Firings | | `automations:write` | Create, edit, pause, resume, and delete Automations | An Automation sends on your behalf, so a scoped key also needs the scope each of its actions' own API call needs. Without it, the request returns `403 auth.insufficient_scope` naming the missing scope. | Action | Also needs | | - | - | | `hide_comment`, `reply_comment`, `private_reply` | `inbox:write` | | `send_dm` | `dms:send` | On `PATCH`, renaming or pausing needs only `automations:write`, so a key can always stop an Automation it can see. Resuming, or changing `steps`, `post_ids` or `once_per_person`, also needs each action's scope. Deleting needs only `automations:write`. A [brand-restricted key](/guides/scoped-keys#brand-restrictions) sees only the Automations on its brands' accounts. Creating one on another brand's account returns `404 account.not_found`. ## Automations and your webhooks Automations do not change what your [webhooks](/guides/webhooks) receive. Every event goes to your webhook endpoints and to your Automations alike: * **Triggers are still delivered.** A comment or DM an Automation answers still arrives as [`comment.received`](/guides/webhook-events#comment-received) or [`dm.received`](/guides/webhook-events#dm-received). If your own handler also replies to those events, the person gets both replies, so let one or the other answer each kind of event. * **Button taps arrive as [`dm.postback`](/guides/webhook-events#dm-postback).** A tap on an Automation's postback button reaches your endpoint like any other tap, with an opaque `metadata.payload` that starts with `sa1.`. The Automation handles the tap, so ignore those payloads, and do not start your own payloads with `sa1.`. * **Sent DMs echo as [`dm.sent`](/guides/webhook-events#dm-sent).** DMs an Automation sends are messages the account sent through SocialAPI, so on Instagram and Facebook they produce the same `dm.sent` echo as a DM you send through the API. Nothing in the event tells the two apart. * **There are no Automation events.** To see what an Automation did, read its [Firings](#firings). ## Use from an AI agent An agent connected to the [SocialAPI MCP server](/integrations/overview) can create and manage Automations: see the [MCP tools reference](/integrations/tools). The tool descriptions carry the main rules on this page, so an agent can build a valid definition from a plain request such as "When someone comments 'menu' on my Instagram posts, send them a private reply with a link to our menu." | Tool | What it does | | - | - | | `social_api_list_automations` | List Automations, optionally for one account | | `social_api_get_automation` | Get one Automation with its steps and counters | | `social_api_create_automation` | Create an Automation, with `steps` or `follow_gate` | | `social_api_update_automation` | Change fields, or pause and resume | | `social_api_delete_automation` | Delete an Automation and its run history | | `social_api_list_automation_firings` | List runs from the last 30 days with each action's outcome | ## Related * [Comments](/guides/comments) for replying, private replies, and hiding through the API * [Direct messages](/guides/direct-messages) for the messaging window and sending DMs through the API * [Webhook events](/guides/webhook-events) for the events your own code receives * [Account permissions](/guides/permissions) for the permissions an account needs * [Scoped API keys](/guides/scoped-keys) for least-privilege keys * [Errors](/guides/errors) for every error code an action can record * [Unified inbox](/guides/inbox) for the inbox areas an Automation answers # Comments Source: https://docs.social-api.ai/guides/comments List posts with comments, read comments and replies, reply publicly or privately, hide, delete, and like comments across platforms through one API. SocialAPI's comments API reads the comments on the posts of your connected accounts and acts on them: reply publicly, send a private reply, hide, unhide, delete, and like. The endpoints are the same on every platform that has comments, and each comment tells you which actions the platform allows for it in its `capabilities` object. A public reply costs 1 interaction credit. Reading, private replies, hiding, deleting, and liking cost none. [Platform pages](/platforms/overview) hold the rules that differ per platform. ## Endpoints | Method | Path | Description | | - | - | - | | `GET` | `/v1/inbox/comments` | List posts that received comments | | `GET` | `/v1/inbox/comments/{postId}` | List the comments on one post | | `GET` | `/v1/inbox/comments/{postId}/{commentId}/replies` | List the replies to one comment | | `POST` | `/v1/inbox/comments/{postId}` | Reply to a comment, or comment on the post | | `POST` | `/v1/inbox/comments/{postId}/{commentId}/private-reply` | Send a private reply to the person who commented | | `POST` | `/v1/inbox/comments/{postId}/{commentId}/hide` | Hide a comment | | `DELETE` | `/v1/inbox/comments/{postId}/{commentId}/hide` | Unhide a comment | | `DELETE` | `/v1/inbox/comments/{postId}/{commentId}` | Delete a comment | | `POST` | `/v1/inbox/comments/{postId}/{commentId}/like` | Like a comment as the connected account | | `DELETE` | `/v1/inbox/comments/{postId}/{commentId}/like` | Remove that like | `{postId}` is the platform's post ID (`targets[].platform_post_id` on a post you published through SocialAPI, or `id` from `GET /v1/inbox/comments`). `{commentId}` and `comment_id` accept the comment's `sapi_cmt_` ID or the platform's comment ID. See [IDs](/guides/ids#what-each-endpoint-accepts). ## Your first reply List the posts that have comments on one account: ```bash theme={null} curl "https://api.social-api.ai/v1/inbox/comments?account_id=acc_01HZ9X3Q4R5M6N7P8V2K0W1J&min_comments=1" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "user_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "instagram", "id": "17895695668004550", "content": "Spring collection is live", "thumbnail": "https://scontent.cdninstagram.com/v/t51/spring.jpg", "permalink": "https://www.instagram.com/p/C8xY2AbC/", "comment_count": 4, "like_count": 87, "published_at": "2026-10-12T09:00:00Z", "created_at": "2026-10-12T09:00:05Z", "updated_at": "2026-10-13T14:30:00Z" } ], "pagination": { "has_more": false } } ``` Read the comments on that post. `account_id` is required: ```bash theme={null} curl "https://api.social-api.ai/v1/inbox/comments/17895695668004550?account_id=acc_01HZ9X3Q4R5M6N7P8V2K0W1J" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "id": "sapi_cmt_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "platform_id": "17858893269577012", "platform": "instagram", "text": "Love this product!", "author_id": "17841400008460056", "author_name": "Jane Smith", "author_username": "janesmith", "author_picture": "", "is_owner": false, "like_count": 2, "reply_count": 1, "has_replies": true, "liked_by_viewer": false, "is_hidden": false, "parent_id": null, "created_at": "2026-10-13T14:30:00Z", "capabilities": { "can_reply": true, "can_delete": true, "can_hide": true, "can_like": false, "can_private_reply": true } } ], "pagination": { "has_more": false, "next_cursor": "" } } ``` Reply to that comment: ```bash theme={null} curl -X POST "https://api.social-api.ai/v1/inbox/comments/17895695668004550" \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "comment_id": "sapi_cmt_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "text": "Thank you, Jane!" }' ``` Response (HTTP 200): ```json theme={null} { "success": true, "comment_id": "sapi_cmt_TT974YVQH4RAHNN2HY2DJSNY9111D7LMTP1X" } ``` The reply is public when the platform accepts it. `comment_id` in the response is the new reply's `sapi_cmt_` ID. ## List posts with comments `GET /v1/inbox/comments` returns the posts that received comments, newest published first. | Parameter | Description | | - | - | | `account_id` | Only posts of this connected account | | `brand_id` | Only posts of accounts in this brand | | `platform` | Only posts of this platform, for example `instagram` | | `page_id` | Facebook only: only posts of this Facebook Page, by its SocialAPI Page ID | | `min_comments` | Only posts with at least this many comments. A non-integer or negative value returns `400 validation.field_invalid` | | `since` | Only posts published at or after this time, in RFC 3339 format. A malformed value returns `400 validation.field_invalid` | | `sort_by` | `date` (default): the publish date of the post | | `sort_order` | `desc` (default) or `asc` | | `limit` | Posts per page. The default is 25. A value above 100 is treated as 100. A value of 0 or less, or one that is not an integer, returns `400 validation.field_invalid` | | `cursor` | The `next_cursor` of the previous page | Each post has `id` (the platform's post ID, the value for `{postId}`), `user_id` (the ID of your SocialAPI account), `account_id`, `page_id` (Facebook only), `platform`, `content`, `thumbnail`, `permalink`, `comment_count`, `like_count`, `published_at`, `created_at`, and `updated_at`. `pagination` holds `has_more`, and `next_cursor` when `has_more` is `true`. Paging is in [Pagination](/guides/pagination#paginated-endpoints). YouTube videos with comments appear only when you pass `account_id`, and that list is 1 page with no cursor. ## List comments and replies `GET /v1/inbox/comments/{postId}` lists the comments on a post. `GET /v1/inbox/comments/{postId}/{commentId}/replies` lists the replies to one comment in the same shape. Both need `account_id`, the connected account that owns the post. A missing `account_id` returns `400 validation.field_required`. | Parameter | Description | | - | - | | `account_id` | Required. The connected account that owns the post | | `limit` | Items per page. The default is 25 for comments and 10 for replies. A value above 100 is treated as 100. A value of 0 or less, or one that is not an integer, returns `400 validation.field_invalid` | | `cursor` | The `next_cursor` of the previous page | A page can hold fewer comments than `limit`, because the platform sets its own page size. Follow `next_cursor` until `has_more` is `false`: see [Pagination](/guides/pagination#fetch-every-page). | Field | Description | | - | - | | `id` | The comment's `sapi_cmt_` ID. Use it as `{commentId}` | | `platform_id` | The platform's own comment ID | | `platform` | The platform slug | | `text` | The comment text | | `author_id`, `author_name`, `author_username`, `author_picture` | Who wrote it. Each is empty when the platform gives none | | `is_owner` | `true` when the connected account wrote the comment | | `like_count` | Likes on the comment | | `reply_count` | Deprecated: not every platform reports an exact count. Read `has_replies` instead | | `has_replies` | `true` when the comment has replies | | `liked_by_viewer` | `true` when the connected account liked the comment. On Facebook, and on Instagram connected through [Facebook Login](/platforms/instagram#connect-through-facebook-login), it reflects only likes made through SocialAPI. On Instagram Login it is always `false` | | `is_hidden` | `true` when the comment is hidden | | `moderation_status` | YouTube only: `published`, `held_for_review`, `likely_spam`, or `rejected`. Absent elsewhere | | `parent_id` | The `platform_id` of the comment this one replies to, or `null` for a top-level comment | | `created_at` | When the comment was posted, in ISO 8601 UTC | | `capabilities` | What you can do with this comment, in the next table | Comments in webhook events use the nested [Interaction](/guides/webhook-events#interaction-payload) shape instead of this flat one. | `capabilities` key | `true` when | | - | - | | `can_reply` | You can reply publicly | | `can_delete` | You can delete the comment. On YouTube and Threads it is `true` for every comment, but deleting another account's comment fails: see [YouTube comments](/platforms/youtube#comments) and [Threads](/platforms/threads) | | `can_hide` | You can hide and unhide the comment | | `can_like` | You can like the comment | | `can_private_reply` | You can send a private reply to its author | Read `capabilities` instead of assuming. A call the platform does not support returns `501 resource.not_supported`. ## Reply to a comment `POST /v1/inbox/comments/{postId}` posts a public reply. | Field | Description | | - | - | | `account_id` | Required. The connected account that replies | | `text` | Required. The reply text. A missing value returns `400 validation.field_required` | | `comment_id` | The comment to reply to. Omit it to comment on the post itself | A reply costs 1 interaction credit. When the plan's interaction allowance is used up, the call returns `429 validation.interaction_limit_exhausted`. A credit is returned when the request fails with a `validation.*` error: see [When a credit comes back](/guides/plans#when-a-credit-comes-back). ## Send a private reply A private reply is a DM to the person who wrote a comment. It has its own endpoint: `POST /v1/inbox/comments/{postId}/{commentId}/private-reply`. The reply endpoint above never sends one. ```bash theme={null} curl -X POST "https://api.social-api.ai/v1/inbox/comments/17895695668004550/17858893269577012/private-reply" \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "text": "Thanks for asking! Here is the full range.", "buttons": [ { "type": "url", "title": "See the range", "url": "https://shop.example.com/spring" } ] }' ``` Response (HTTP 200): ```json theme={null} { "success": true } ``` | Field | Description | | - | - | | `account_id` | Required. The connected account that sends it | | `text` | Required unless you send `attachment`. The message. With `buttons`, it is the body of the button template (640 characters at most). On Instagram, the text holds 1,000 bytes of UTF-8 at most | | `buttons` | 1 to 3 buttons, sent as a button template. Fields and limits are in [Buttons](/guides/direct-messages#buttons). A tap on a `postback` button arrives as a `dm.postback` event | | `attachment` | One image, video, audio file, or document sent as the private reply instead of text. `source_type` is `url`, `media_id` (a file from your [media library](/guides/media)), or `platform_attachment_id`, `source` holds the reference, and `type` (`image`, `video`, `audio`, or `file`) is required only with `platform_attachment_id` | A private reply is a single message, so it carries either `text` (with optional `buttons`) or one `attachment`. Sending `attachment` with `text` or `buttons` returns `400 validation.attachment_conflict`. If the platform cannot fetch or accept the file, the call returns `422 platform.instagram.attachment_failed` or `422 platform.facebook.attachment_failed`, and the comment can still receive its private reply. To send a PDF as the first message: ```bash theme={null} curl -X POST "https://api.social-api.ai/v1/inbox/comments/17895695668004550/17858893269577012/private-reply" \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "attachment": { "source_type": "url", "source": "https://cdn.example.com/guide.pdf", "type": "file" } }' ``` Private replies exist on Instagram and Facebook. The platform allows 1 private reply per comment, within 7 days of the comment. A second reply, or a comment older than 7 days, returns `400 platform.instagram.private_reply_ineligible` or `400 platform.facebook.private_reply_ineligible`. `error.meta.reason` says why: `already_replied`, `platform_refused`, or `window_expired`. This 400 is final, so retrying never succeeds. Every other platform returns `501 resource.not_supported`. A private reply does not open the [messaging window](/guides/direct-messages#the-messaging-window). You cannot message the person again until they answer, and their answer opens the window. To answer comments by keyword with no handler of your own, use an [Automation](/guides/automations): it sends the private reply and handles each button tap, including a [Follow gate](/guides/automations#the-follow-gate) on Instagram. ## Hide and unhide a comment Hiding removes a comment from public view. Unhiding restores it. ```bash theme={null} curl -X POST "https://api.social-api.ai/v1/inbox/comments/17895695668004550/17858893269577012/hide" \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }' ``` Response (HTTP 200): ```json theme={null} { "success": true } ``` Unhide with `DELETE` on the same path and `account_id` as a query parameter: ```bash theme={null} curl -X DELETE "https://api.social-api.ai/v1/inbox/comments/17895695668004550/17858893269577012/hide?account_id=acc_01HZ9X3Q4R5M6N7P8V2K0W1J" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Hiding is free. A platform without it returns `501 resource.not_supported`. Threads hides replies only on an account connected on or after 2026-10-17: reconnect an older account first. See [Threads](/platforms/threads). ## Delete a comment `DELETE /v1/inbox/comments/{postId}/{commentId}?account_id={account_id}` deletes the comment on the platform. It returns `{ "success": true }` and cannot be undone. On Threads, TikTok, X, Bluesky, and YouTube, you can delete only comments the connected account wrote. Each other platform's page states its rule. ## Like and unlike a comment `POST /v1/inbox/comments/{postId}/{commentId}/like` likes a comment as the connected account. `DELETE` on the same path removes the like. Both take `account_id`: in the body for `POST`, as a query parameter for `DELETE`. They return `{ "success": true }`. A platform without comment likes returns `501 resource.not_supported`, and `can_like` is `false` on its comments. ## Filter YouTube comments `GET /v1/inbox/comments/{postId}` accepts 3 more parameters. Only YouTube uses them to filter or sort. On TikTok, a `q` or a non-default `order` or `moderation_status` stops hidden comments from being added to the list: see [TikTok comments](/platforms/tiktok#comments). | Parameter | Description | | - | - | | `moderation_status` | `published` (default), `held_for_review`, or `likely_spam`. The last two list comments waiting for your review | | `order` | `recent` (default) or `relevance` | | `q` | A keyword searched in the comment text, up to 256 characters | A value outside these lists, or a longer `q`, returns `400 validation.field_invalid`. See [YouTube comments](/platforms/youtube#comments). ## Platform support Every platform has its own comment rules: whether replies nest, who can hide or delete what, and which account permissions and platform scopes the connection needs. The [capability matrix](/platforms/overview) compares platforms, and each platform page has the details: [Instagram](/platforms/instagram#comments), [Facebook](/platforms/facebook), [Threads](/platforms/threads#replies), [TikTok](/platforms/tiktok#comments), [YouTube](/platforms/youtube#comments), [X](/platforms/twitter), and [Bluesky](/platforms/bluesky#comments-and-replies). Google Business Profile has no comments or Q\&A: its calls return `501 resource.not_supported`. Read reviews on [Reviews](/guides/reviews). ## Limits | Limit | Value | | - | - | | Private replies per comment | 1, within 7 days of the comment | | Buttons on a private reply | 1 to 3, with a body of 640 characters at most | | Private reply text on Instagram | 1,000 bytes of UTF-8 | | `q` filter length | 256 characters | | Posts per page | Default 25, up to 100 | | Comments per page | Default 25, up to 100. On TikTok, up to 30 comments and 30 replies, plus any hidden comments added to the first page of results (see [TikTok comments](/platforms/tiktok#comments)) | | Replies per page | Default 10, up to 100 | ## Errors and debugging Check these in order when a comment call fails or returns nothing: 1. **`401`, `403 auth.insufficient_scope`.** The key is invalid, or a scoped key lacks the scope in [Scopes](#scopes). 2. **`404 account.not_found`, `403 account.reconnection_required`.** The `acc_` ID is not yours, or the account needs [reconnecting](/guides/connect-accounts#reconnect-an-account). 3. **`403 account.permission_missing`.** The account lacks the [permission](/guides/permissions) the call needs: `comments.read` to list the comments on a post and the replies to a comment, `comments.write` to reply, hide, unhide, delete, or like, and `messages.write` for a private reply. `error.meta.permission` names it. `GET /v1/inbox/comments` lists the posts SocialAPI stored and never returns this error: an account without `comments.read` stores no new posts. 4. **`400 validation.field_required`, `400 validation.field_invalid`.** A required field is missing, or a parameter has a wrong value. `error.meta.field` names it. 5. **`400 validation.id_invalid`, `400 validation.id_platform_mismatch`.** The comment ID is the wrong type or belongs to another platform: see [IDs](/guides/ids#what-each-endpoint-accepts). 6. **`501 resource.not_supported`.** The platform lacks the action. Read `capabilities` on the comment. 7. **`404 resource.not_found`.** The post was deleted on the platform. The same post returns the same 404 afterwards. 8. **`400 platform.instagram.private_reply_ineligible`, `400 platform.facebook.private_reply_ineligible`.** The comment already received its private reply, or is older than 7 days. 9. **`429 validation.interaction_limit_exhausted`, `429 platform.{platform}.rate_limit`.** Your allowance, or the platform's own limit. Read `Retry-After`: see [Rate limits](/guides/rate-limits). 10. **`422 platform.youtube.processing_failure`.** YouTube refused to delete a comment that your channel did not write. Hide it instead. When the list is empty, check that the post has comments, that `account_id` owns the post, and, on a platform that sends events, that the comment arrived: see [Webhook events](/guides/webhook-events#comments). The full catalog is in [Errors](/guides/errors#inbox-comments-messages-and-reviews). ## Scopes | Scope | Operations | | - | - | | `inbox:read` | List posts with comments, list comments, list replies | | `inbox:write` | Reply, send a private reply, hide, unhide, delete, like, unlike | ## Events SocialAPI sends `comment.received` when a person comments on a post of a connected account, on the platforms that report comments to SocialAPI. See [`comment.received`](/guides/webhook-events#comments) for the platforms and the payload. Platforms that report nothing, such as X, YouTube, and Bluesky, need the list endpoints above. An account without the `comments.read` [permission](/guides/permissions) receives no `comment.received` events. ## Use from an AI agent | Tool | What it does | | - | - | | `social_api_list_inbox_posts` | List your posts with their comment counts | | `social_api_list_post_comments` | List the comments on one post | | `social_api_get_comment_replies` | List the replies to one comment | | `social_api_reply_to_comment` | Reply publicly to a post or a comment. The text is 2,200 bytes at most | | `social_api_private_reply_to_comment` | Send a private reply. The text is 1,000 bytes at most | | `social_api_hide_comment`, `social_api_unhide_comment` | Hide or unhide a comment | | `social_api_delete_comment` | Delete a comment | | `social_api_like_comment`, `social_api_unlike_comment` | Like or unlike a comment | | `social_api_moderate_comment` | Hide, unhide, or delete a comment by its `sapi_cmt_` ID, with `action` set to `hide`, `unhide`, or `delete` | The tool list and error shape are in [MCP tools](/integrations/tools#comments). ## Related * [Direct messages](/guides/direct-messages): the messages and buttons a private reply starts. * [Automations](/guides/automations): answer comments by keyword with no handler of your own. * [Webhook events](/guides/webhook-events#comments): the `comment.received` payload per platform. * [Platform support](/platforms/overview): which platform supports which comment action. * [IDs](/guides/ids): the `sapi_cmt_` ID and the platform IDs these endpoints accept. * [Account permissions](/guides/permissions): the permissions an account needs for each comment action. * [Plans and usage](/guides/plans#what-counts-as-a-post-or-interaction): what costs an interaction credit. # Core concepts Source: https://docs.social-api.ai/guides/concepts Learn the five objects of the SocialAPI REST API (brand, account, post, interaction, capability) and how they relate before you build. SocialAPI is a REST API and MCP server that reads and answers comments, direct messages (DMs), mentions, and reviews on connected social accounts, and publishes posts to them. Five objects shape every endpoint: brand, account, post, interaction, and capability. A brand groups accounts. An account is one connected social account. You publish a post to one or more accounts. Interactions arrive on an account. Capabilities tell you which actions a platform allows on an interaction. ## Brand A brand is the top-level group of connected accounts: one business, client, or location. Every account belongs to exactly 1 brand. Every interaction belongs to the account that received it. A post goes to 1 or more accounts as `targets[]`. ```json theme={null} { "id": "1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f", "name": "Acme Corp", "created_at": "2026-04-12T09:30:00Z" } ``` * **A new SocialAPI account starts with 1 brand** named "Default". Rename or delete it like any other brand. * **Your plan sets how many brands you can create.** Pricing pages call a brand a profile. See [Plans and usage](/guides/plans), and read your current count with `GET /v1/usage` ([Check your usage](/guides/plans#check-your-usage)). * **A brand-restricted API key sees only its brands.** Use brands to keep agency clients apart. See [Brand restrictions](/guides/scoped-keys#brand-restrictions). * **Creating a brand is not idempotent.** Brand names are not unique, so a retried `POST /v1/brands` creates a duplicate. See [Create requests are not idempotent](/guides/ids#create-requests-are-not-idempotent). ## Account An account is one connected social account: an Instagram account, a Facebook login with its Pages, a YouTube channel, or a Google Business Profile location. Its ID starts with `acc_`, and you pass it to every endpoint that acts on the account. ```json theme={null} { "id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "instagram", "name": "Acme Corp", "username": "acmecorp", "brand_id": "1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f", "status": "active", "permissions": null, "effective_permissions": ["posts.read", "posts.write", "comments.read", "comments.write", "messages.read", "messages.write", "analytics.read"] } ``` You connect an account with `POST /v1/accounts/connect` and send the person through the platform's consent screen. SocialAPI's own app connects most platforms, so you register nothing. X, Telegram, Bluesky, and App Store Connect use your own app or credentials. See [Connect accounts](/guides/connect-accounts) and [Platform apps](/guides/platform-credentials). An account's `status` is `active`, `reconnect_recommended`, or `token_expired`. Reconnecting keeps the account's ID. See [Account status](/guides/connect-accounts#account-status). An account's `permissions` limit what it can do on its platform. `null` means every permission, and `effective_permissions` lists the ones the account can use now. See [Account permissions](/guides/permissions). ## Post A post is one piece of outbound content. One `POST /v1/posts` call can send it to 1 or more accounts. Each account it goes to is a target in `targets[]`, with its own status, `platform_post_id`, `permalink`, error, and metrics. That split lets one post report "published on Instagram, failed on Facebook". ```json theme={null} { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "text": "Spring collection is live.", "status": "published", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "instagram", "status": "published", "platform_post_id": "17895695668004550", "permalink": "https://www.instagram.com/p/ABC123/", "metrics": { "likes": 42, "comments": 5, "shares": 0, "saves": 0 } } ], "created_at": "2026-10-13T11:00:00Z" } ``` * **A request publishes only with `publish_now` or `scheduled_at`.** With neither, `POST /v1/posts` saves a draft. * **A post is `draft`, `scheduled`, `publishing`, `published`, `partial`, `failed`, or `cancelled`.** A target is `pending`, `publishing`, `published`, `failed`, or `unpublished`. See [Post status](/guides/delivery#post-status). * **An HTTP success means SocialAPI accepted the post.** Delivery to each platform finishes afterwards: read the post again or wait for a `post.*` webhook event. See [Track post delivery](/guides/delivery). * **Metric names are the same on every platform.** Where a platform reports a count, it uses the keys `likes`, `comments`, `shares`, and `saves`. A key the platform does not report is `0`. A Google Business Profile target has no `metrics` object, or one whose four keys are all `0`. LinkedIn and Pinterest targets return `501 resource.not_supported`. Instagram, TikTok, YouTube, and Zalo add keys such as `views`: see [Analytics](/guides/analytics#read-live-post-metrics). ## Interaction An interaction is anything inbound on an account: a comment on one of your posts, a DM, a mention, or a review. You read interactions with the [inbox endpoints](/guides/inbox), and SocialAPI also sends them to your server as [webhook events](/guides/webhook-events). Comments, reviews, DMs, and mentions have different IDs and different item shapes: | Interaction | ID prefix | Guide | | - | - | - | | Comment | `sapi_cmt_` | [Comments](/guides/comments) | | DM | `sapi_dm_` | [Direct messages](/guides/direct-messages) | | Review | `sapi_rev_` | [Reviews](/guides/reviews) | | Mention | `sapi_mnt_` | [Mentions](/guides/mentions) | Treat every ID as an opaque string. See [IDs](/guides/ids). A comment returned by the REST API is a flat object: ```json theme={null} { "id": "sapi_cmt_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "platform_id": "17858893269577012", "platform": "instagram", "text": "Love this product!", "author_id": "17841400008460056", "author_name": "Jane Smith", "author_username": "janesmith", "is_owner": false, "is_hidden": false, "parent_id": null, "created_at": "2026-10-13T14:30:00Z", "capabilities": { "can_reply": true, "can_delete": true, "can_hide": true, "can_like": false, "can_private_reply": true } } ``` The same comment in a `comment.received` webhook event is nested: `author` holds the person and `content` holds the text. Webhook events and the mentions list share that nested shape. See [Interaction payload](/guides/webhook-events#interaction-payload). ## Capability A capability is what one platform lets you do with one kind of feature: reply to a comment, hide it, like it, send a private reply, send a DM, reply to a review. Platforms differ, so SocialAPI reports capabilities instead of making you branch on platform names. Each comment carries a `capabilities` object with `can_reply`, `can_delete`, `can_hide`, `can_like`, and `can_private_reply`. Read it before you show a button or call an action. An action the platform does not support returns `501` with `error.code: "resource.not_supported"`. Retrying does not change that answer. For the whole picture, [Supported platforms](/platforms/overview) compares every platform, and each platform page lists its own Capabilities table. ## Related * [Quickstart](/quickstart) to use all five objects in one walkthrough * [Connect accounts](/guides/connect-accounts) to create accounts and read their status * [Unified inbox](/guides/inbox) to read and answer interactions * [Publishing](/guides/publishing) to send posts to accounts * [IDs](/guides/ids) for the ID format of every object * [Supported platforms](/platforms/overview) for what each platform allows # Connect a social account Source: https://docs.social-api.ai/guides/connect-accounts Connect Instagram, Facebook, TikTok, YouTube, LinkedIn, X, and more to SocialAPI, choose a location, reconnect, and disconnect accounts. A connected account is one social account (an Instagram account, a Facebook login with its Pages, a YouTube channel, a Telegram bot) that SocialAPI can act on for you. You start a connection with `POST /v1/accounts/connect`, send the person to the platform's consent screen, and SocialAPI redirects them back to your app with the new account's ID. SocialAPI keeps the platform's access tokens, so you never receive or refresh them. The same flow serves every platform. Telegram, Bluesky, and App Store Connect skip the consent screen and connect in one request. Google Business Profile can pause so you choose which location to connect, and LinkedIn so you choose a member profile or a company Page. Facebook can pause so you choose which Pages join the brand (see [Facebook Pages](#facebook-pages)), and you can move Pages between brands afterwards. Connecting, reconnecting, and disconnecting cost no credits. ## Endpoints | Method | Path | Description | | - | - | - | | `POST` | `/v1/accounts/connect` | Start a connection, or reconnect an existing account | | `GET` | `/v1/accounts` | List your connected accounts and their status. Filter to one brand with `brand_id` | | `GET` | `/v1/accounts/pending/{connection_id}` | Read a connection that waits for you to choose a location, a LinkedIn profile or Page, or Facebook Pages | | `POST` | `/v1/accounts/pending/{connection_id}/select` | Make the choice and finish the connection | | `PATCH` | `/v1/accounts/{id}` | Change the account's permissions: see [Account permissions](/guides/permissions) | | `DELETE` | `/v1/accounts/{id}` | Disconnect an account | | `POST` | `/v1/invites` | Create a single-use link that lets someone else connect an account to a brand | | `GET` | `/v1/invites` | List a brand's invite links. Requires `brand_id` | | `DELETE` | `/v1/invites/{id}` | Revoke an invite link | ## Your first connection This request starts connecting an Instagram account into an existing brand and asks SocialAPI to send the person back to your app afterwards: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/connect \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "instagram", "brand_id": "1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f", "redirect_uri": "https://app.example.com/oauth/callback", "state": "session_abc123" }' ``` Response (HTTP 202): ```json theme={null} { "auth_url": "https://www.instagram.com/oauth/authorize/?client_id=1234567890&redirect_uri=https%3A%2F%2Fapi.social-api.ai%2Foauth%2Fcallback%2Finstagram&response_type=code&state=4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2#weblink", "state": "4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2", "message": "Open this link to connect your instagram account. Once you've authorized, the account will be connected automatically." } ``` Send the person to `auth_url`. After they approve, their browser lands on your `redirect_uri`: ```text theme={null} https://app.example.com/oauth/callback?account_id=acc_01HZ9X3Q4R5M6N7P8V2K0W1J&platform=instagram&state=session_abc123&status=success ``` Store `account_id` against your user. The account is usable at once. ### Request fields | Field | Type | Required | Description | | - | - | - | - | | `platform` | string | Yes | The platform slug. See [Platform slugs](#platform-slugs) | | `brand_id` | string | No | The brand to connect the account into. Without it, SocialAPI creates a new brand named after the account, and the new brand counts against your plan's brand allowance | | `redirect_uri` | string | No | Where SocialAPI sends the person after the consent screen. See [Redirect URI rules](#redirect-uri-rules). Without it, the person sees a SocialAPI page that says the account is connected (or why it failed). Platforms that connect without a consent screen ignore it | | `state` | string | No | Your own value, up to 512 characters, returned unchanged on the redirect. Use it to match the redirect to the session that started it | | `permissions` | array of strings | No | The [permissions](/guides/permissions#the-permissions) to request: `posts.read`, `posts.write`, `comments.read`, `comments.write`, `messages.read`, `messages.write`, `reviews.read`, `reviews.write`, `analytics.read`. Omit it to keep the brand's current selection for this platform, or to request every permission when the brand has none. An empty list returns `400 validation.permissions_empty`, and an unknown name returns `400 validation.permission_unknown` | | `metadata` | object | No | Platform-specific connect options, documented on each platform's page: `bot_token` for Telegram, `identifier`, `app_password`, and `auth_factor_token` for Bluesky, `key_id`, `issuer_id`, `private_key`, and `app_id` for [App Store Connect](/platforms/app-store-connect#connect-an-account), `connection_type` for [TikTok](/platforms/tiktok#creator-connection) and [LinkedIn](/platforms/linkedin#connect-an-account), and `connection_type: "facebook_login"` for [Instagram](/platforms/instagram#connect-through-facebook-login) | ### Response fields `POST /v1/accounts/connect` answers in one of two ways: * **HTTP 202** for platforms with a consent screen: `auth_url` (send the person there), `state` (SocialAPI's own value for this connection), and `message`. WhatsApp works the same way: `auth_url` opens Meta's signup. See [WhatsApp](/platforms/whatsapp). * **HTTP 201** for Telegram, Bluesky, and App Store Connect, which connect without a consent screen: `account_id`, `brand_id`, `platform`, `username`, and `display_name`. `brand_id` is the brand the account joined, or the brand created for it when you sent no `brand_id`. The person has 1 hour from your request to finish on the platform. After 1 hour, the platform's redirect reaches a SocialAPI error page instead of your `redirect_uri`, and you start a new connection. ## How a connection works A connection either completes at once, pauses for a choice (a Google location, a LinkedIn member profile or company Page, Facebook Pages, or a WhatsApp number), or fails. Every outcome reaches your `redirect_uri` as query parameters. ```mermaid theme={null} flowchart TD A["POST /v1/accounts/connect"] -->|"201: Telegram, Bluesky, App Store Connect"| Z["Account connected"] A -->|"202: auth_url"| B["Person approves on the platform"] B --> C{"Redirect to your redirect_uri"} C -->|"status=success"| Z C -->|"status=selection_required"| D["GET /v1/accounts/pending/{connection_id}"] D --> E["POST /v1/accounts/pending/{connection_id}/select"] E --> Z C -->|"status=error"| F["Start a new connection"] ``` **1. Start the connection** Call `POST /v1/accounts/connect` from your server with your API key. Pass the `brand_id` the account belongs in. **2. Send the person to the consent screen** Redirect the person's browser to `auth_url`. They sign in to the platform and approve the scopes SocialAPI asks for. Each platform's page lists those scopes. **3. Read the result on your redirect URI** SocialAPI completes the authorization with the platform and redirects the person to your `redirect_uri` with `status` and the parameters below. Check `status` first, then compare `state` with the value you sent. **4. Choose a Google Business Profile, a LinkedIn profile or Page, Facebook Pages, or a WhatsApp number, if asked** When `status` is `selection_required`, read the candidates and choose within 30 minutes, as described in [Choose a Google Business Profile](#choose-a-google-business-profile), [Choose a LinkedIn profile or Page](#choose-a-linkedin-profile-or-page), [Choose Facebook Pages](#choose-facebook-pages), and [Choose a WhatsApp number](/platforms/whatsapp#choose-a-number). ### Redirect parameters | Parameter | When it is present | Description | | - | - | - | | `status` | Always | `success`, `selection_required`, or `error` | | `platform` | Always | The platform slug, for example `instagram` | | `state` | When you sent `state` | Your value, unchanged | | `account_id` | `success` with one account | The connected account's SocialAPI ID | | `account_ids` | `success` with more than one account | Comma-separated SocialAPI IDs, when one authorization connects more than one account, for example a Google login with more than one YouTube channel | | `brand_id` | `success` with one account, and `selection_required` when you sent `brand_id` | The brand the account joins. Without a `brand_id` on the connect, a `selection_required` redirect has none: the select response returns it | | `brand_ids` | `success` with more than one account | Comma-separated brand IDs, in the same order as `account_ids` | | `connection_id` | `selection_required` | The pending connection to finish with `POST /v1/accounts/pending/{connection_id}/select` | | `error` | `error` | The reason, see below | | `error_description` | `error`, when there is one | A sentence you can show the person | `error` holds one of these values: * **The platform's own code** when the person declines or the platform refuses the request, for example `access_denied`. * **A `platform.*` or other SocialAPI error code** when the platform or SocialAPI refuses the connection, for example `billing.brands_limit` (your plan's brand allowance is used up), `account.already_linked` (the brand already holds a different account on this platform, or `lock_accounts_to_brand` blocked a move between brands), `account.different_login` (the brand's Facebook is connected through a different login), `byok.credentials_missing` (no X app credentials saved), `byok.credentials_invalid` (X rejected the saved credentials), or `byok.app_access_denied` (X refused your app access to its API). Each code is listed in [Errors](/guides/errors). * **`missing_code`** when the platform returned neither a code nor an error. * **`exchange_failed`** when the failure has no code. * **`internal_error`** when the connection failed on our side. Start a new connection. A `platform.*.insufficient_scope` code means the person left a permission unticked on the consent screen. Ask them to connect again and allow every permission. After any error, start a new connection: the old `auth_url` is spent. Without a `redirect_uri`, a connection that needs a choice sends the person to the SocialAPI dashboard, which asks them to choose. ## Platform slugs The `platform` field takes one of these values: | Slug | Platform | How it connects | | - | - | - | | `instagram` | Instagram | Consent screen | | `facebook` | Facebook Page | Consent screen. Every granted Page that none of your other brands holds is connected (see [Facebook Pages](#facebook-pages)) | | `threads` | Threads | Consent screen | | `google` | Google Business Profile | Consent screen, then a choice of location when the login manages more than one | | `tiktok` | TikTok | Consent screen | | `linkedin` | LinkedIn member profile or company Page | Consent screen, then a choice of member profile or company Page when the login offers more than one. See [LinkedIn](/platforms/linkedin#connect-an-account) | | `twitter` | X / Twitter | Consent screen through your own X app. `x` is accepted as the same slug | | `youtube` | YouTube | Consent screen | | `pinterest` | Pinterest | Consent screen | | `zalo` | Zalo | Consent screen | | `whatsapp` | WhatsApp | Meta's signup through `auth_url`, then a number choice when the account holds several | | `telegram` | Telegram | One request with a bot token, no consent screen | | `bluesky` | Bluesky | One request with an app password, no consent screen | | `app_store_connect` | App Store Connect | One request with your App Store Connect API key, no consent screen. One app per account | A slug not in this table, including `trustpilot`, returns `403 billing.platform_not_available`. `linkedin_page` returns `400 validation.platform_invalid`: connect LinkedIn company Pages with `linkedin`. Which app each platform connects through (SocialAPI's or your own) is listed in [Platform apps](/guides/platform-credentials). ## Redirect URI rules Your `redirect_uri` must use `http` or `https` and must not contain a user name, a password, or a `#fragment`. A malformed value returns `400 validation.redirect_uri_invalid_format`. Until you register a redirect URI, SocialAPI accepts any well-formed `redirect_uri`. Once you register one or more, every `redirect_uri` must match one of them, and any other value returns `400 validation.redirect_uri_not_whitelisted` before a connection starts. Register up to 20 redirect URIs in the dashboard at [app.social-api.ai/api-configuration](https://app.social-api.ai/api-configuration?tab=redirect-uris). Deleting every registered URI accepts any well-formed value again. SocialAPI compares redirect URIs after normalizing both sides: the scheme and host are lowercased, a default port (`:443` for `https`, `:80` for `http`) is dropped, a trailing `/` is removed, and query parameters are sorted. `https://App.example.com:443/oauth/callback/` matches a registered `https://app.example.com/oauth/callback`. Your `redirect_uri` can carry its own query parameters. SocialAPI adds its parameters to them. When one of yours has the same name as a parameter SocialAPI sends (`status`, `platform`, `state`, `account_id`, `account_ids`, `connection_id`, `error`, `error_description`), SocialAPI's value replaces yours. ## Choose a Google Business Profile A Google connection pauses with `status=selection_required` when the Google login manages more than one Business Profile. You choose exactly one. A login with a single Profile connects at once. The redirect carries only the `connection_id`. Read the candidates with `GET /v1/accounts/pending/{connection_id}`: ```bash theme={null} curl https://api.social-api.ai/v1/accounts/pending/pc_4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2 \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "connection_id": "pc_4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2", "platform": "google", "expires_at": "2026-10-13T09:30:00Z", "profiles": [ { "platform_account_id": "accounts/123/locations/456", "display_name": "Joe's Pizza (Downtown)", "username": "", "profile_picture_url": "", "bio": "Wood-fired pizza since 1998" }, { "platform_account_id": "accounts/123/locations/789", "display_name": "Joe's Pizza (Uptown)", "username": "", "profile_picture_url": "", "bio": "" } ], "login_id": "cred_01J8X3Q4R5M6N7P8V2K0W1JA" } ``` Finish the connection by sending the chosen `platform_account_id`: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/pending/pc_4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2/select \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{"platform_account_id": "accounts/123/locations/456"}' ``` Response (HTTP 201): ```json theme={null} { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f", "platform": "google", "username": "", "display_name": "Joe's Pizza (Downtown)" } ``` ### Pending connection fields | Field | Description | | - | - | | `connection_id` | The pending connection's ID | | `platform` | `google`, `linkedin`, `whatsapp`, `facebook`, or `instagram` | | `expires_at` | When the pending connection expires, 30 minutes after it was created | | `profiles[]` | Google: the Business Profiles the login manages. LinkedIn: the member profile and the company Pages the login offers (see [Choose a LinkedIn profile or Page](#choose-a-linkedin-profile-or-page)). Each has `platform_account_id`, `display_name`, `username`, `profile_picture_url`, and `bio`. Facebook and Instagram: an empty array | | `pages[]` | Facebook and Instagram only: the candidates to choose from. See [Choose Facebook Pages](#choose-facebook-pages). Absent for Google and LinkedIn | | `lost_access[]` | Facebook and Instagram only: Pages this login no longer grants. See [When reconnecting Facebook removes Pages](#when-reconnecting-facebook-removes-pages). Absent when empty | | `login_id` | The login behind the connection. For Google, use it to connect more locations later without a new consent screen: see [Google Business Profile locations](/platforms/google/locations). For LinkedIn, see [Connect more profiles and Pages from a login](/platforms/linkedin#connect-more-profiles-and-pages-from-a-login). For Facebook, see [One login, many brands](/platforms/facebook#one-login-many-brands) | The response has no `data` wrapper: every field is at the top level. ### Selection rules * **A pending connection expires 30 minutes after it is created** and works once. The first `select` that succeeds consumes it. On a Facebook or Instagram connection, some failed `select` calls consume it too: see [Choose Facebook Pages](#choose-facebook-pages). A second `select`, or a call after expiry, returns `404 resource.not_found`. * **A `400` keeps the pending connection.** On a Facebook or Instagram connection, only the three `400` codes listed in [Choose Facebook Pages](#choose-facebook-pages) keep it. Fix the request and send it again within the 30 minutes. * **A `platform_account_id` that is not in `profiles[]`** returns `400 validation.profile_not_in_set`. * **A body without `platform_account_id`** returns `400 validation.field_required`. ## Choose a LinkedIn profile or Page A LinkedIn connection pauses with `status=selection_required` when the authorization offers more than one account: the member profile of the person who signed in, and each company Page that person administers. You choose exactly one. An authorization that offers a single account connects at once. `metadata.connection_type` on the connect narrows the offer: see [LinkedIn](/platforms/linkedin#connect-an-account). Read the candidates with `GET /v1/accounts/pending/{connection_id}`: ```bash theme={null} curl https://api.social-api.ai/v1/accounts/pending/pc_9b3e7a1d5c2f8e40b6a1d7c3f9e2b5a8 \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "connection_id": "pc_9b3e7a1d5c2f8e40b6a1d7c3f9e2b5a8", "platform": "linkedin", "expires_at": "2026-11-05T09:30:00Z", "profiles": [ { "platform_account_id": "urn:li:person:8fA3kLm2Qx", "display_name": "Jane Doe", "username": "janedoe", "profile_picture_url": "https://media.licdn.com/dms/image/v2/jane-doe.jpg", "bio": "" }, { "platform_account_id": "urn:li:organization:12345678", "display_name": "Acme Corp", "username": "acme-corp", "profile_picture_url": "", "bio": "Industrial tools since 1998" } ], "login_id": "cred_01J9K4R5M6N7P8V2K0W1J3QX" } ``` A `platform_account_id` that starts with `urn:li:person:` is the member profile, and one that starts with `urn:li:organization:` is a company Page. Finish the connection by sending the chosen `platform_account_id`: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/pending/pc_9b3e7a1d5c2f8e40b6a1d7c3f9e2b5a8/select \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{"platform_account_id": "urn:li:organization:12345678"}' ``` Response (HTTP 201): ```json theme={null} { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1K", "brand_id": "1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f", "platform": "linkedin", "username": "acme-corp", "display_name": "Acme Corp" } ``` The [selection rules](#selection-rules) apply. To connect the candidates you did not choose, use the `login_id`, when the response has one, with [the LinkedIn login endpoints](/platforms/linkedin#connect-more-profiles-and-pages-from-a-login): they need no new consent screen. ## Facebook Pages A Facebook connection pauses with `status=selection_required` so you choose which granted Pages join the brand. The same applies to Instagram connected with `metadata.connection_type: "facebook_login"`, where each candidate is an Instagram account. Instagram connected through Instagram Login has no selection step. `GET /v1/users/me` returns `facebook_page_selection`, which is `true` on new SocialAPI accounts and which the API cannot change. When it is `false` on your SocialAPI account, a Facebook connection has no selection step: SocialAPI adds every granted Page that none of your other brands holds to the brand's account, and the redirect carries `status=success`. A connection started from an [invite link](#let-someone-else-connect-an-account) never has a selection step, whatever the setting. * A login that grants no Page ends with `status=error` and `error=platform.facebook.no_pages` on your `redirect_uri`. * Without a selection step, a login whose every Page belongs to another of your brands ends with `status=error` and `error=platform.facebook.no_assignable_pages` (`platform.instagram.no_assignable_pages` for Instagram). With a selection step, those Pages come back with `assignable: false` instead. To move a Page to another brand after connecting, see [One login, many brands](/platforms/facebook#one-login-many-brands). ### Choose Facebook Pages Read the candidates with `GET /v1/accounts/pending/{connection_id}`: ```bash theme={null} curl https://api.social-api.ai/v1/accounts/pending/pc_7d2e9a4c1f8b3e60a5d2c9f4b1e8a3d7 \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "connection_id": "pc_7d2e9a4c1f8b3e60a5d2c9f4b1e8a3d7", "platform": "facebook", "expires_at": "2026-10-13T09:30:00Z", "profiles": [], "login_id": "cred_01J8X3Q4R5M6N7P8V2K0W1J", "pages": [ { "platform_page_id": "104857392011223", "name": "Acme Bakery", "category": "Bakery", "assignable": true }, { "platform_page_id": "104857392011987", "name": "Acme Catering", "assigned_brand_id": "4d8a2c7e-91b3-4f6a-a2d5-7e0c3b9f1a64", "assigned_brand_name": "Acme Events", "assigned_account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W3C", "assignable": false } ] } ``` | Field | Description | | - | - | | `pages[].platform_page_id`, `name` | Facebook's Page ID and the Page's name. For Instagram, the Instagram account's ID and name | | `pages[].picture_url`, `category` | Each is omitted when Facebook returns none | | `pages[].assignable` | `true` when none of your brands holds the Page. `false` in three cases: another of your brands holds it, the connecting brand already holds it (you can still select it), or, for Instagram, you hold the account through Instagram Login. In the first two cases, `assigned_brand_id`, `assigned_brand_name`, and `assigned_account_id` name the brand and its account | Finish the connection by sending the chosen Page IDs: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/pending/pc_7d2e9a4c1f8b3e60a5d2c9f4b1e8a3d7/select \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{"page_ids": ["104857392011223"]}' ``` Response (HTTP 201): ```json theme={null} { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f", "platform": "facebook", "username": "Maria Lopez", "display_name": "Maria Lopez" } ``` `username` and `display_name` are the name on the Facebook login, not a Page name. The selected Pages are on the account: list them with `GET /v1/accounts/{id}/pages`. The [selection rules](#selection-rules) on expiry, single use, and `400` apply. Also: * **`page_ids` empty or missing** returns `400 validation.field_required`. * **A Page ID that is not in `pages[]`** returns `400 validation.page_not_in_set`, with the ID in `meta.platform_page_id`. * **A Page that another of your brands holds** returns `400 validation.page_already_assigned`, with `meta.brand_id` naming that brand. A Page the connecting brand already holds can be selected. To take a Page from another brand, move it with the assign call in [One login, many brands](/platforms/facebook#one-login-many-brands). * **More Pages than your plan allows per brand** returns `403 platform.facebook.page_cap`, with the allowance in `meta.limit`. On a Facebook connection, the count includes the Pages the brand already holds. The allowance per plan is in [Plans and usage](/guides/plans#facebook-pages-per-brand). * **An Instagram account you already connected through Instagram Login** returns `409 platform.instagram.flow_conflict`. `meta.accounts` is a comma-separated string of the Instagram account IDs concerned. Disconnect that account first. * **On a Facebook or Instagram connection, only `400 validation.field_required`, `400 validation.page_not_in_set`, and `400 validation.page_already_assigned` keep the pending connection.** Any other error, including `400 validation.page_not_granted`, `403 platform.facebook.page_cap`, and `409 platform.instagram.flow_conflict`, consumes it. Start a new connection to choose again. * **No call cancels a pending connection.** An unused one expires after 30 minutes. ### When reconnecting Facebook removes Pages Facebook grants Page access per app, not per brand. When a person reconnects a Facebook login and unchecks a Page on Facebook's consent screen, every brand that used that Page through this login loses it. The affected account gets `status: reconnect_recommended` with `reconnect_reason: page_access_lost`. The Page stays assigned to its brand. When the login grants the Page again (the person reconnects and keeps the Page checked), the Page works again on the same brand with no further step. ## Account status `GET /v1/accounts` returns every connected account with its `status`: ```bash theme={null} curl "https://api.social-api.ai/v1/accounts?brand_id=1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "instagram", "name": "Acme Corp", "username": "acmecorp", "profile_picture_url": "https://scontent.cdninstagram.com/v/t51.2885-19/acmecorp.jpg", "bio": "Wood-fired bread since 1998", "brand_id": "1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f", "status": "reconnect_recommended", "reconnect_reason": "platform_unauthorized", "permissions": null, "effective_permissions": ["posts.read", "posts.write", "comments.read", "comments.write", "messages.read", "messages.write", "analytics.read"] } ], "count": 1 } ``` | Field | Description | | - | - | | `id` | The account's SocialAPI ID, prefixed `acc_` | | `platform` | The platform slug | | `name`, `username` | The account's display name and handle. On platforms without a handle, `username` equals `name` | | `profile_picture_url`, `bio` | The account's picture and biography. Absent when the platform gives none | | `brand_id` | The brand that holds the account | | `status` | `active`, `reconnect_recommended`, or `token_expired`, see below | | `reconnect_reason` | Why the account needs reconnecting. Absent when `status` is `active` or when SocialAPI has no reason to give | | `login_id` | The platform login shared by every account connected in the same authorization. Present for Facebook, Google, and LinkedIn accounts. For the LinkedIn exception, see [Connect more profiles and Pages from a login](/platforms/linkedin#connect-more-profiles-and-pages-from-a-login) | | `connection_type` | LinkedIn only: `profile` for a member profile, `organization` for a company Page. Absent on every other platform | | `permissions` | The [permissions](/guides/permissions) requested for this account. `null` means every permission, including ones added later. Always present | | `effective_permissions` | The permissions the account can use now: the requested ones that the platform supports and the person connecting granted. An empty list when none apply. Always present | | `status` | What it means | What to do | | - | - | - | | `active` | The account works | Nothing | | `reconnect_recommended` | The platform refused a request, or the account lost access to something it used. Requests on the account still go through | Ask the person to reconnect. Check `reconnect_reason` first: some reasons are not fixed by a reconnect | | `token_expired` | The platform said the stored authorization is no longer valid. Every request that acts on the account returns `403 account.reconnection_required` | Reconnect the account | Treat a `reconnect_reason` you do not recognize as a request to reconnect. | `reconnect_reason` | Cause | | - | - | | `platform_unauthorized` | The platform refused a request as unauthorized without saying why. The account returns to `active` by itself after its next successful request | | `access_revoked` | The person removed SocialAPI's access on the platform, or removed the scopes it needs | | `all_pages_removed` | The login no longer grants any of the Pages this account used | | `page_access_lost` | A reconnect of the same Facebook login unchecked a Page this account used | | `posts_sync_auth_failed` | The platform refused a read of the account's posts as unauthorized | | `youtube_account_suspended` | YouTube suspended the channel. Reconnecting does not lift a suspension: the channel owner resolves it with YouTube | | `whatsapp_number_unavailable` | Meta reports that the WhatsApp number can no longer be used through this connection | | `whatsapp_number_banned` | Meta banned the WhatsApp number. Reconnecting does not lift a ban: a connection to that number returns `403 whatsapp.number_banned` until Meta lifts it | | `password_changed`, `session_expired`, `invalid_access_token`, `app_not_installed`, `user_checkpointed`, `checkpoint_required`, `user_not_confirmed` | Instagram, Facebook, or Threads ended the authorization, for the reason named. The person signs in to the platform, resolves any security check, then reconnects | ## Reconnect an account To reconnect an account, call `POST /v1/accounts/connect` again with the same `platform` and the account's current `brand_id` (the `brand_id` that `GET /v1/accounts` returns), and send the person through the consent screen with the same platform login. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/connect \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "instagram", "brand_id": "1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f", "redirect_uri": "https://app.example.com/oauth/callback" }' ``` SocialAPI updates the existing account in place: * The account keeps its `id`, its brand, its posts, conversations, and Automations. * `status` returns to `active` and `reconnect_reason` is cleared. * The account keeps its `permissions` unless you send `permissions` on the reconnect. A narrower list deletes the data the removed permissions covered: see [Removing a permission deletes data](/guides/permissions#removing-a-permission-deletes-data). * SocialAPI sends `account.connected` again. A Facebook reconnect, or an Instagram reconnect through Facebook Login, can end with `status=selection_required` like a first connection: see [Facebook Pages](#facebook-pages). A LinkedIn reconnect can too: choose the same member profile or company Page, as described in [Choose a LinkedIn profile or Page](#choose-a-linkedin-profile-or-page). Always pass `brand_id` when you reconnect. Without it, SocialAPI creates a new brand for the connection and moves the account into it, which leaves the old brand without the account. Connecting a platform account that another of your brands already holds moves it to the brand you name. These cases do not move it: * **Facebook, and Instagram connected through Facebook Login.** A Page or Instagram account that another of your brands holds is not taken. With a selection step, it comes back with `assignable: false`. Without a selection step, a login whose every Page or Instagram account belongs to another brand ends with `error=platform.facebook.no_assignable_pages` or `error=platform.instagram.no_assignable_pages`. You assign Pages to brands as described in [Facebook](/platforms/facebook). * **Google Business Profile locations assigned with the assign call.** `POST /v1/platforms/google/logins/{loginId}/locations/{locationId}/assign` returns `400 validation.location_already_assigned` for a location that another brand holds. See [Google locations](/platforms/google/locations). * **LinkedIn member profiles and company Pages assigned with the assign call.** `POST /v1/platforms/linkedin/logins/{loginId}/profiles/{profileId}/assign` returns `400 validation.profile_already_assigned` for an account that another brand holds. See [LinkedIn](/platforms/linkedin#connect-more-profiles-and-pages-from-a-login). * **`lock_accounts_to_brand` set to `true` on your SocialAPI account** (`PATCH /v1/users/me`). A connect that would move an account to a different brand ends with `error=account.already_linked`, or returns `409 account.already_linked`. The default is `false`. The lock does not apply to Facebook. Each of those errors leaves the account in its current brand. A brand holds one connected account per platform, except LinkedIn, where a brand can hold any number of LinkedIn member profiles and company Pages. Connecting a different account of the same platform into a brand that already has one ends with `status=error` and `error=account.already_linked` on your `redirect_uri`. Telegram, Bluesky, and App Store Connect, which connect in one request, return `409 account.already_linked` from `POST /v1/accounts/connect`, and so does `POST /v1/accounts/pending/{connection_id}/select` for a Google location. On LinkedIn, the select call returns `409 account.already_linked` only when `lock_accounts_to_brand` is `true` and another of your brands holds the chosen member profile or company Page. On Facebook, connecting a brand whose account came from a different Facebook login ends with `error=account.different_login` on your `redirect_uri`. A Google login with more than one YouTube channel connects every channel in one authorization, and the redirect carries every account in `account_ids`. Without `brand_id`, each channel connects into its own new brand named after the channel. With a `brand_id`, the brand takes one channel, and each further channel ends the connection with `status=error` and `error=account.already_linked` on your `redirect_uri`. Channels connected before the error stay connected. ## Let someone else connect an account An invite link lets a person who has no access to your SocialAPI account connect their own account to one of your brands. Agencies send it to clients. The link works once. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/invites \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "brand_id": "1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f", "platform": "facebook", "expires_in_days": 7 }' ``` Response (HTTP 201): ```json theme={null} { "id": "3f6c1a52-9d4e-4b7a-8c21-5e0f7a9b1d34", "platform": "facebook", "token": "a55755cf75f73b56deb3dfe0c993fd36fc4c2bfa7e1d94c60b8a3f52d17e9c40", "url": "https://api.social-api.ai/invite/a55755cf75f73b56deb3dfe0c993fd36fc4c2bfa7e1d94c60b8a3f52d17e9c40", "expires_at": "2026-10-20T09:00:00Z", "permissions": null } ``` Send `url` to the person. They approve on the platform's consent screen, SocialAPI connects the account to the brand, and `account.connected` is sent to your webhook endpoints. | Field | Type | Required | Description | | - | - | - | - | | `brand_id` | string | Yes | The brand that receives the account | | `platform` | string | Yes | A platform slug that connects through a consent screen, or `whatsapp` | | `expires_in_days` | integer | Yes | `1`, `3`, `7`, `14`, or `30`. Any other value returns `400 validation.field_invalid` | | `permissions` | array of strings | No | The [permissions](/guides/permissions#the-permissions) the connected account gets. Omit it to keep the brand's current selection for this platform, or to give every permission when the brand has none. An empty list returns `400 validation.permissions_empty`, and an unknown name returns `400 validation.permission_unknown`. The response and the invite list return it as you set it, `null` when you omitted it | * A `brand_id` that is not one of your brands returns `404 resource.not_found`. * A brand that already holds an account on that platform, or that already has an unused, unexpired invite for that platform, returns `409 resource.conflict`. A LinkedIn account in the brand does not block a LinkedIn invite: only an unused, unexpired LinkedIn invite does. * `platform: "linkedin_page"` returns `400 validation.platform_invalid`. Use `linkedin`. * A Facebook invite connects every Page of the person's login that none of your other brands holds. The person cannot choose Pages. When `facebook_page_selection` is `true`, more Pages than [your plan allows per brand](/guides/plans#facebook-pages-per-brand) end the invite with `platform.facebook.page_cap`. * An expired, used, or revoked link shows the person a SocialAPI error page. * `GET /v1/invites?brand_id={brand_id}` lists the brand's invites, newest first, each with `id`, `platform`, `token`, `url`, `is_active`, `expires_at`, `created_at`, and `permissions`. A missing `brand_id` returns `400 validation.field_required`. * `DELETE /v1/invites/{id}` revokes an invite and returns HTTP 204 with no body. An unknown or already used invite returns `404 resource.not_found`. Creating, listing, and revoking invites needs a full-access key or the dashboard, and costs no credits. ## Disconnect an account `DELETE /v1/accounts/{id}` disconnects an account: ```bash theme={null} curl -X DELETE https://api.social-api.ai/v1/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response: HTTP 204 with no body. An ID that is not one of your accounts returns `404 account.not_found`. > [!WARNING] > > Disconnecting cannot be undone. Connecting the same platform account again creates a new account with a new `id`, and nothing below comes back. Disconnecting an account: * **Removes the account.** It no longer appears in `GET /v1/accounts`, and every request with its ID returns `404 account.not_found`. Its brand stays. * **Deletes what SocialAPI stored for it:** its Pages and board settings, its Automations and their run history, its DM conversations and messages, its comments and review replies, and the attachments received in its messages. * **Removes the account from your posts.** A post that targeted only this account is deleted. A post that also targeted other accounts keeps those targets. * **Asks the platform to end SocialAPI's access** on Instagram, Facebook, Threads, Google Business Profile, YouTube, TikTok, X, and WhatsApp. The account is disconnected even when the platform does not answer. SocialAPI keeps the authorization while another connected account still uses the same platform login, so disconnecting one brand's Facebook or Google account never cuts off another brand. * **Leaves the authorization in place on LinkedIn, Pinterest, Zalo, Bluesky, and App Store Connect**, which offer SocialAPI no way to end it. The person removes SocialAPI from their platform settings (on Bluesky, deletes the app password; on App Store Connect, revokes the API key). For Telegram, SocialAPI stops receiving the bot's messages, and the bot token stays valid until you revoke it in [@BotFather](https://t.me/BotFather). `DELETE /v1/accounts/{id}` does not send a webhook event. ## Errors `POST /v1/accounts/connect` refuses a request before the consent screen with these codes. Every code is described in [Errors](/guides/errors). * A missing `platform` returns `400 validation.field_required`. * A `state` longer than 512 characters returns `400 validation.customer_state_too_long`. * A slug not in the platform slugs table, including `trustpilot`, returns `403 billing.platform_not_available`. * `linkedin_page` returns `400 validation.platform_invalid`, with `meta.field` set to `platform`. * A `metadata.connection_type` that the platform does not accept returns `400 validation.connection_type_invalid`: on LinkedIn, anything but `profile` or `organization`; on TikTok, anything but `business` or `creator`. * A request without `brand_id` when your plan's brand allowance is used up returns `403 billing.brands_limit`. * A `brand_id` that is not one of your brands returns `404 resource.not_found`, with `meta.resource` set to `brand`. A `brand_id` that is one of your brands but outside your [brand-restricted key](/guides/scoped-keys)'s brands returns `404 brand.not_found`. * A malformed or unregistered `redirect_uri` returns `400 validation.redirect_uri_invalid_format` or `400 validation.redirect_uri_not_whitelisted`. * An empty `permissions` list returns `400 validation.permissions_empty`, and an unknown permission name returns `400 validation.permission_unknown`, with `meta.permission` naming it. `POST /v1/invites` returns both codes too. * X with no app credentials saved returns `412 byok.credentials_missing`. * Telegram, Bluesky, or App Store Connect with a missing `metadata` field returns `400 validation.field_required`, with `meta.field` naming it. On App Store Connect, a Team key sent without `issuer_id` returns `401 platform.app_store_connect.auth`. Errors after the consent screen reach your `redirect_uri` as `status=error` (see [Redirect parameters](#redirect-parameters)). ## Scopes | Scope | Grants | | - | - | | `accounts:read` | `GET /v1/accounts` | | `accounts:manage` | Connect, exchange, read and finish pending connections, change an account's permissions, and disconnect | A brand-restricted key must pass a `brand_id` from its own list. A connection started without `brand_id` creates a new brand that the key cannot see. Invite endpoints need a full-access key. See [Scoped API keys](/guides/scoped-keys). ## Events * `account.connected` is sent each time an account connects or reconnects. * `account.updated` with `change: "permissions_reduced"` is sent when an account loses a permission you did not remove yourself: see [Account permissions](/guides/permissions#what-an-account-can-do-right-now). * `account.disconnected` is sent in two cases. When the person removes SocialAPI's app from Instagram, Facebook, or Threads, SocialAPI disconnects and deletes the account as `DELETE /v1/accounts/{id}` does, and the event carries `account_id`, `platform`, and `platform_user_id`. When an account needs reconnecting, the account stays connected and the event carries `reconnect_required: true` and a `reason`. The `reason` of a reconnect event names the cause. It equals `reconnect_reason` on `GET /v1/accounts`, with these exceptions: * `access_revoked` is sent for `access_revoked` itself, for `password_changed`, `session_expired`, `invalid_access_token`, `app_not_installed`, `user_checkpointed`, `checkpoint_required`, and `user_not_confirmed`, and for `whatsapp_number_unavailable`. * `number_banned` is sent for `whatsapp_number_banned`. * No event is sent for `platform_unauthorized`. Payloads are in [Webhook events](/guides/webhook-events). ## Use from an AI agent | Tool | What it does | | - | - | | `social_api_connect_account` | Starts a connection and returns the link to open. Accepts `permissions` | | `social_api_list_accounts` | Lists connected accounts with their status | | `social_api_list_pending_profiles` | Reads a pending connection's candidates | | `social_api_select_pending_profile` | Finishes a pending connection. Takes `page_ids` for Facebook and for Instagram connected through Facebook Login | | `social_api_update_account_permissions` | Replaces an account's permissions | | `social_api_disconnect_account` | Disconnects an account | | `social_api_create_invite`, `social_api_list_invites`, `social_api_revoke_invite` | Create, list, and revoke invite links. Creating accepts `permissions` | See [MCP tools](/integrations/tools) for every tool. ## Related * [Account permissions](/guides/permissions) for connecting an account with only the permissions you need * [Platform apps](/guides/platform-credentials) for which app each platform connects through * [Supported platforms](/platforms/overview) for what each platform can do once connected * [Scoped API keys](/guides/scoped-keys) for keys limited to some brands * [Plans](/guides/plans) for the brand allowance * [Webhook events](/guides/webhook-events) for the `account.connected` and `account.disconnected` payloads * [Errors](/guides/errors) for every error code above # Request IDs Source: https://docs.social-api.ai/guides/debugging Read the X-Request-ID header and the request_id error field to identify a SocialAPI call, send your own request ID, and report a failed call. Every response from the SocialAPI REST API carries an `X-Request-ID` header, and every error response repeats the same value as `request_id` in its body. Log it with each call, and include it when you contact support about a failed request. ## Read the request ID ```bash theme={null} curl -i https://api.social-api.ai/v1/accounts \ -H "Authorization: Bearer $SOCAPI_KEY" ``` The response headers include: ```text theme={null} HTTP 200 X-Request-ID: 9f3c1a7e ``` SocialAPI generates the ID when you do not send one: 8 lowercase hexadecimal characters, such as `9f3c1a7e`. An error response has the ID in the body too: ```bash theme={null} curl -i https://api.social-api.ai/v1/accounts \ -H "Authorization: Bearer sapi_key_invalid" ``` Response (HTTP 401): ```json theme={null} { "error": { "code": "auth.invalid_key", "message": "invalid or inactive API key" }, "request_id": "4d82b0c6" } ``` The `X-Request-ID` response header has the value `4d82b0c6`, the same as `request_id`. ## Send your own request ID Send `X-Request-ID` on the request to use your own correlation ID. SocialAPI echoes your `X-Request-ID` in the response header and in `request_id` when it matches `^[a-zA-Z0-9_-]{1,64}$`. Otherwise SocialAPI generates an 8-character lowercase hexadecimal ID. ```bash theme={null} curl -i https://api.social-api.ai/v1/accounts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "X-Request-ID: order-4821-attempt-2" ``` Response headers: ```text theme={null} HTTP 200 X-Request-ID: order-4821-attempt-2 ``` | Rule | Value | | - | - | | Allowed characters | Letters `A-Z` and `a-z`, digits, hyphen, and underscore | | Length | 1 to 64 characters | | When the value does not match | SocialAPI ignores it, returns a generated 8-character ID instead, and does not return an error | SocialAPI does not check that your IDs are unique. Send a different value on each request, or you cannot tell the requests apart when you report one. ## Log the request ID in your application Log the ID of every response, not only of errors, so you can look up any call later: ```typescript theme={null} const resp = await fetch("https://api.social-api.ai/v1/accounts", { headers: { Authorization: `Bearer ${process.env.SOCAPI_KEY}`, "X-Request-ID": myCorrelationId, }, }); const requestId = resp.headers.get("X-Request-ID"); if (!resp.ok) { const body = await resp.json(); console.error("SocialAPI error", { code: body.error.code, message: body.error.message, request_id: body.request_id, }); } else { console.info("SocialAPI call", { request_id: requestId }); } ``` ## Report a failed request Send support the `request_id`, the HTTP method and path, and the time of the call. The `request_id` identifies the single request that failed. Two other IDs identify different things: * **Delivery ID:** `X-SocialAPI-Delivery` on a webhook your endpoint receives identifies one webhook delivery, not an API request. See [Webhooks](/guides/webhooks). * **Target errors:** a failed post target shows its reason in `targets[].error`, with no `request_id`. Report the post ID and the target's `account_id`. See [Track post delivery](/guides/delivery). ## Related * [Errors](/guides/errors): the error envelope and every error code * [Authentication](/guides/authentication): `auth.*` errors for a missing or invalid API key * [Rate limits](/guides/rate-limits): handle HTTP 429 and `Retry-After` * [Webhooks](/guides/webhooks): `X-SocialAPI-Delivery` on webhook deliveries * [IDs](/guides/ids): formats of SocialAPI object IDs # Track post delivery Source: https://docs.social-api.ai/guides/delivery Read the HTTP status, post status, and per-target results of a published post to see what reached each platform, and learn how deleting a post works. Publishing through SocialAPI is asynchronous: an HTTP success means SocialAPI accepted the post, not that every platform has published it. The post has a `status`, and each platform it goes to is a target in `targets[]` with its own `status`, `platform_post_id`, `permalink`, and `error`. Read them with `GET /v1/posts/{pid}` or receive them in the `post.published`, `post.partial`, and `post.failed` webhook events. ## Endpoints | Method | Path | Description | | - | - | - | | `GET` | `/v1/posts/{pid}` | Read one post with its `status` and `targets[]` | | `GET` | `/v1/posts` | List posts with their targets | | `POST` | `/v1/posts/{pid}/publish` | Publish a `draft` or `scheduled` post now | | `POST` | `/v1/posts/{pid}/retry` | Re-send the failed targets of a `failed` or `partial` post | | `POST` | `/v1/posts/{pid}/unpublish` | Remove the published copies from the platforms and keep the post | | `DELETE` | `/v1/posts/{pid}` | Delete a post | | `POST` | `/v1/media/{id}/verify` | Verify uploaded media before it publishes | ## Publish and read the result Publish to 2 targets at once: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Spring sale starts today: https://example.com/spring", "publish_now": true, "targets": [ {"account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J"}, {"account_id": "acc_01HZ9X4T6B8C2D5F7G9H1J3K", "page_id": "sapi_page_01HZ9X5V7C9D3E6G8H1J4K2M"} ] }' ``` Response (HTTP 201): ```json theme={null} { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "text": "Spring sale starts today: https://example.com/spring", "status": "publishing", "hidden": false, "retry_count": 0, "created_at": "2026-10-13T09:00:00Z", "updated_at": "2026-10-13T09:00:00Z", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "threads", "status": "publishing" }, { "account_id": "acc_01HZ9X4T6B8C2D5F7G9H1J3K", "page_id": "sapi_page_01HZ9X5V7C9D3E6G8H1J4K2M", "platform": "facebook", "status": "publishing" } ] } ``` The post is `publishing` and each target is `pending` or `publishing`, because delivery runs after the response. Read the post again until its status is `published`, `partial`, or `failed`, or wait for the webhook: ```bash theme={null} curl https://api.social-api.ai/v1/posts/7c9e6679-7425-40de-944b-e07fc1f90ae7 \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200) after the Threads target published and the Facebook target failed: ```json theme={null} { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "text": "Spring sale starts today: https://example.com/spring", "status": "partial", "hidden": false, "retry_count": 0, "published_at": "2026-10-13T09:00:04Z", "created_at": "2026-10-13T09:00:00Z", "updated_at": "2026-10-13T09:00:05Z", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "threads", "status": "published", "platform_post_id": "18012345678901234", "permalink": "https://www.threads.com/@acme/post/DAbCdEf123", "published_at": "2026-10-13T09:00:04Z" }, { "account_id": "acc_01HZ9X4T6B8C2D5F7G9H1J3K", "page_id": "sapi_page_01HZ9X5V7C9D3E6G8H1J4K2M", "platform": "facebook", "status": "failed", "error": { "code": "platform.facebook.rate_limit", "message": "Platform rate limit exceeded", "category": "rate_limit", "caused_by": "platform" } } ] } ``` The post is `partial`: 1 target published and 1 failed. `POST /v1/posts/{pid}/retry` re-sends only the failed targets. ## Response codes `POST /v1/posts` and `POST /v1/posts/{pid}/publish` return the post object with one of these HTTP statuses: | HTTP status | Post `status` | Meaning | | - | - | - | | 201 | `draft`, `scheduled`, or `publishing` | SocialAPI created the post. With `publish_now` or `POST /v1/posts/{pid}/publish`, delivery continues in the background | | 207 | `partial` | Some targets published and others failed before the response was built | | 422 | `failed` | Every target failed before the response was built. The body is the post object, not an error envelope | 207 and 422 appear only when delivery finishes before SocialAPI builds the response. Do not rely on them: read `status` and each target's `status` on every response, whatever the HTTP status. `POST /v1/posts/{pid}/retry` returns `200` with `{"success": true}`, and the post goes back to `publishing`. Other statuses on these endpoints are errors in the standard envelope: `400 publishing.validation_failed` when the post fails validation (the issues are in `error.meta.issues`), `409 resource.conflict` for duplicate content, `409 post.state_invalid` when the post is not in a state the endpoint accepts, `400 publishing.no_targets` when the post has no publishable targets, `400 post.no_retryable_deliveries` when a retry finds no failed target, and `429 publishing.velocity_limit` or `429 validation.post_limit_exhausted`. See [Errors](/guides/errors). SocialAPI ignores request fields it does not recognize, so a misspelled field is not an error: the post is created without it. Send only the documented field names, and read the post back with `GET /v1/posts/{pid}` when a missing field would be costly. ## Post status A post has one of these statuses. SocialAPI derives it from the statuses of the post's targets. | Status | Meaning | | - | - | | `draft` | Saved without `publish_now` or `scheduled_at`. Nothing is sent until you publish or schedule it | | `scheduled` | The post is waiting for its scheduled time, or SocialAPI is sending it, until every target settles. While it waits, every target is `pending` | | `publishing` | A post published now (`publish_now` or `POST /v1/posts/{pid}/publish`) has at least one target `publishing`, or some targets finished while others are still `pending` | | `published` | Every target is `published` | | `partial` | At least one target is `published`, and at least one other target is `failed` or `unpublished` | | `failed` | No target is `published`, and at least one target is `failed` | | `cancelled` | Every target was `unpublished` | ```mermaid theme={null} flowchart LR A[draft] -->|POST /posts/pid/publish| D[publishing] B[scheduled] -->|POST /posts/pid/publish| D B -->|scheduled time reached| E[published, partial, or failed] C[POST /posts with publish_now] --> D D --> E E -->|failed or partial: POST /posts/pid/retry| D ``` A draft or scheduled post moves to `publishing` when you publish it. A post that is `published`, `partial`, or `failed` stays in that status until you retry, unpublish, or delete it. SocialAPI sends one `post.published`, `post.partial`, or `post.failed` event when the post reaches that status. See [Webhook events](/guides/webhook-events). ## Target fields Each item of `targets[]` is the delivery of the post to one connected account, and one Facebook Page or Pinterest board when the account has more than one. | Field | Description | | - | - | | `account_id` | The connected account | | `page_id` | The `sapi_page_` ID of the Facebook Page or other page-level destination. Absent when the account has none | | `board_id` | The board for a Pinterest target. Absent on other platforms | | `platform` | The platform slug, for example `instagram` or `threads` | | `media_type` | The content type: `feed`, `reel`, `stories`, or `carousel` | | `status` | `pending`, `publishing`, `published`, `failed`, or `unpublished` | | `text`, `title`, `media`, `first_comment`, `visibility`, `scheduled_at` | The values for this target when they differ from the post's own | | `platform_post_id` | The platform's ID of the published post. Absent until the target is `published` | | `permalink` | The URL of the published post. Absent until the target is `published` | | `published_at` | When the target published (ISO 8601, UTC) | | `error` | Why the target failed. Present only when `status` is `failed`. See [Target errors](#target-errors) | | `metrics`, `metrics_synced_at` | Engagement numbers and when they were last read. See [Analytics](/guides/analytics) | | `settings`, `platform_media` | What Instagram or Facebook reports for the published post. See the platform's publishing page | | `metadata` | Platform-specific key-value data for the target | Target `status` means: | Target status | Meaning | | - | - | | `pending` | Waiting to be sent: scheduled for later, or queued | | `publishing` | Being sent, or sent and waiting for the platform to confirm | | `published` | The platform confirmed the post | | `failed` | The platform or SocialAPI refused or could not send it. `error` says why | | `unpublished` | You deleted it from the platform with `DELETE /v1/posts/{pid}` or `POST /v1/posts/{pid}/unpublish` | On TikTok, the platform can confirm a post before it reports the post's ID. The target is then `published` with no `platform_post_id` and no `permalink`, and both appear after SocialAPI checks again. A TikTok draft sent with `post_mode: "inbox"` stays `published` with no `platform_post_id` or `permalink`: the post exists only in the creator's TikTok app. SocialAPI sets a target to `failed` with `system.internal` when it stays in `publishing` for more than 1 hour without a final answer from the platform, or for more than 2 hours when the platform keeps reporting it as in progress. ## Target errors A failed target carries an `error` object with 4 fields: | Field | Description | | - | - | | `code` | The error code. Match on this, never on `message` | | `message` | A description of what failed | | `category` | `validation`, `auth`, `account`, `billing`, `resource`, `rate_limit`, `platform`, or `system` | | `caused_by` | `user` (the request or your connection caused it), `platform` (the platform refused it), or `system` (SocialAPI failed) | Surface `error.message` to your users instead of a generic retry prompt. Target errors appear in 2 places: * **`targets[].error`** on every post response: `POST /v1/posts`, `POST /v1/posts/{pid}/publish`, `GET /v1/posts`, and `GET /v1/posts/{pid}`. * **`results[].error`** in the `post.partial` and `post.failed` webhook events. The payload is `{post_id, status, results}`, and each item of `results` has `account_id`, `brand_id`, `page_id`, `platform`, `status`, `platform_post_id`, `permalink`, and `error`. The webhook `error` holds `code` and `message` only. Target errors do not appear in `GET /v1/events`: an event has `error_message` and `error_category`, with no `error_code`. These codes describe delivery itself. A failure that comes from the platform has a `platform.{platform}.*` code described on the platform's page. * `publishing.delivery.account_load_failed`: SocialAPI could not load the connected account when it started sending. Reconnect the account. * `account.permission_missing`: the account did not have the `posts.write` [permission](/guides/permissions) when the delivery ran. Add the permission, then retry. * `publishing.delivery.page_resolve_failed`: SocialAPI could not resolve the Facebook Page for the target. * `publishing.delivery.timeout`: sending did not finish before SocialAPI's time limit for one delivery (15 minutes). * `validation.publish`: a scheduled post failed validation when its time arrived. `error.message` names the problem. SocialAPI does not retry it. * `platform.{platform}.publish_failed`: the platform failed the request without a more specific code. * `system.internal`: SocialAPI stopped waiting for a stuck target (see above). Retry a failed or partial post with `POST /v1/posts/{pid}/retry`. It re-sends only the failed targets, resets them to `pending`, clears their errors, and costs 1 post credit. See [Plans and usage](/guides/plans#what-counts-as-a-post-or-interaction). ## Verify media before you publish Media uploaded with the presigned flow has a status, and only `ready` media publishes. A media file is `pending` until you call `POST /v1/media/{id}/verify`, `ready` after verification, and `failed` when the file is missing or the upload would exceed your [storage quota](/guides/plans#plan-allowances). When you publish or schedule a post that references media that is not `ready`, `POST /v1/posts` returns `400 publishing.validation_failed`, and `error.meta.issues` contains an issue for field `media` with the message `media {id} is not ready or not found`. Draft posts skip this check and run it when you publish. With `skip_validation: true`, the post is accepted and the target fails at delivery. See [Media](/guides/media) for the upload flow. ## Delete a post `DELETE /v1/posts/{pid}` returns HTTP 200, not 204. What it does depends on the post's status: | Post status | What happens | | - | - | | `draft`, `failed`, or `cancelled` | SocialAPI deletes the post. Nothing was published | | `scheduled` | SocialAPI cancels the scheduled sends and deletes the post | | `published` or `partial` | SocialAPI deletes each published target from its platform. SocialAPI deletes the post only when no target is still `published`. Otherwise it keeps the post, and its `status` is recalculated | | `publishing` | Returns `409 post.state_invalid`. Wait for a final status | Response (HTTP 200) for a post published to Threads and TikTok, when Threads removed its copy and TikTok cannot delete posts: ```json theme={null} { "success": true, "deleted": false, "results": [ { "platform": "threads", "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "outcome": "deleted" }, { "platform": "tiktok", "account_id": "acc_01HZ9X6W8D1E4F7H9J2K5L3N", "outcome": "not_supported", "message": "Operation not supported for this platform" } ] } ``` | Field | Description | | - | - | | `success` | `true` whenever the request was processed, even when a platform kept its copy. Read `deleted` and `results` | | `deleted` | `true` when SocialAPI removed the post. `false` when at least one platform copy is still live and SocialAPI kept the post | | `results[].outcome` | `deleted`, `not_supported` (the platform cannot delete posts through its API), or `failed` | | `results[].message` | The reason for `not_supported` or `failed` | A target the platform did not delete stays `published`, so SocialAPI shows what is still live. Deleting a post costs no credits and sends `post.deleted` when SocialAPI removes the post. ## Unpublish a post `POST /v1/posts/{pid}/unpublish` removes the published copies from the platforms and keeps the post. Send `{"account_id": "..."}` to remove one target only. Targets removed from the platform become `unpublished`. The post is `partial` while another target is still `published`, `failed` when none is published and one target failed, and `cancelled` when every target was removed. See [Platform support](/platforms/overview) for what each platform can delete. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts/7c9e6679-7425-40de-944b-e07fc1f90ae7/unpublish \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{"account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J"}' ``` Response (HTTP 200), when every targeted copy was removed: ```json theme={null} { "success": true } ``` | HTTP | Code | When it occurs | | - | - | - | | 400 | `validation.field_invalid` | No target is `published` with a `platform_post_id`, or the `account_id` you sent has no such target | | 501 | `resource.not_supported` | At least one remaining target is on a platform that cannot delete posts, such as TikTok. Every target that was removed is `unpublished`, `post.unpublished` is sent for those targets, and the post status is recalculated. The copies on platforms that cannot delete stay live | | 500 | `system.internal` | A platform failed to delete a copy. Every target that was removed is `unpublished`, and the post status is recalculated. This code takes precedence over `501` when both happen in one request | SocialAPI sends `post.unpublished` when at least one target is removed, including on a `501` or `500` response. ## Scopes | Scope | Grants | | - | - | | `posts:read` | List and read posts and their targets | | `posts:write` | Publish, retry, unpublish, and delete posts | | `media:write` | Verify uploaded media | See [Scoped API keys](/guides/scoped-keys). ## Use from an AI agent | Tool | What it does | | - | - | | `social_api_create_post` | Creates, schedules, or publishes a post | | `social_api_get_post` | Reads a post with its targets | | `social_api_publish_post` | Publishes a draft or scheduled post now | | `social_api_retry_post` | Re-sends the failed targets | | `social_api_delete_post` | Deletes or cancels a post | | `social_api_unpublish_post` | Removes the published copies and keeps the post | | `social_api_validate_post` | Checks a post without sending it | ## Related * [Publish a post](/guides/publishing): fields every platform shares and how to create a post * [Schedule posts](/guides/scheduling): schedule a post and cancel it * [Upload media](/guides/media): upload, verify, and attach media * [Webhook events](/guides/webhook-events): `post.published`, `post.partial`, and `post.failed` payloads * [Errors](/guides/errors): every error code with its HTTP status * [Rate limits](/guides/rate-limits): posting velocity caps that hold or refuse a publish # Direct messages Source: https://docs.social-api.ai/guides/direct-messages List DM conversations, read messages, send text, attachments, and buttons, react to messages, and respect each platform's messaging window through one API. SocialAPI's direct message (DM) API lists the conversations of your connected accounts, reads their messages, and sends replies. A conversation is one person talking to one connected account. The endpoints are the same on every platform that supports DMs: Instagram, Facebook, X, WhatsApp, Telegram, Zalo, and Bluesky. A send on any other platform returns `501 resource.not_supported`. A DM request costs 1 interaction credit, however many messages it sends. Reads and reactions cost nothing. Each platform decides when you can message a person: see [The messaging window](#the-messaging-window). ## Endpoints | Method | Path | Description | | - | - | - | | `GET` | `/v1/inbox/conversations` | List conversations | | `GET` | `/v1/inbox/conversations/{id}` | Get one conversation | | `PATCH` | `/v1/inbox/conversations/{id}` | Archive a conversation, or set it back to `active` | | `POST` | `/v1/inbox/conversations/{id}/read` | Mark a conversation as read | | `GET` | `/v1/inbox/conversations/{id}/messages` | List the messages of a conversation | | `POST` | `/v1/inbox/conversations/{id}/messages` | Send a message | | `POST` | `/v1/inbox/conversations/{id}/messages/{messageId}/reaction` | React to a message | | `DELETE` | `/v1/inbox/conversations/{id}/messages/{messageId}/reaction` | Remove your reaction | `{id}` is the conversation `id` that `GET /v1/inbox/conversations` returns. It is a UUID, not a `sapi_dm_` ID. `{messageId}` is a message's `platform_id`. See [IDs](/guides/ids#what-each-endpoint-accepts). ## Your first message List the conversations of one connected account: ```bash theme={null} curl "https://api.social-api.ai/v1/inbox/conversations?account_id=acc_01HZ9X3Q4R5M6N7P8V2K0W1J" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "id": "7f1c4e2b-2841-41a8-85d7-27b0f37ac067", "user_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "instagram", "platform_id": "aGVsbG8gd29ybGQ", "participant_id": "17841405793187218", "participant_name": "Jane Smith", "participant_picture": "https://scontent.cdninstagram.com/v/t51/jane.jpg", "last_message": "Is the blue one in stock?", "last_message_at": "2026-10-13T14:30:00Z", "status": "active", "unread_count": 1, "created_at": "2026-10-13T14:30:00Z", "updated_at": "2026-10-13T14:30:00Z" } ], "pagination": { "has_more": false, "next_cursor": "" }, "sync_state": "idle", "last_synced_at": "2026-10-13T14:31:00Z" } ``` Send a reply into that conversation: ```bash theme={null} curl -X POST "https://api.social-api.ai/v1/inbox/conversations/7f1c4e2b-2841-41a8-85d7-27b0f37ac067/messages" \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "text": "Yes, the blue one ships today." }' ``` Response (HTTP 200): ```json theme={null} { "success": true, "message_id": "sapi_dm_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "message_ids": ["sapi_dm_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I"] } ``` `message_ids` lists the SocialAPI ID (`sapi_dm_`) of every message the request sent, in send order. `message_id` is the first one and is deprecated. On Zalo, `message_id` is empty and each entry of `message_ids` is an empty string. To get the platform ID of a sent message, read it from [the message list](#read-messages) (`platform_id`). A conversation that belongs to another connected account than `account_id` returns `404 conversation.not_found`. ## Conversations `GET /v1/inbox/conversations` lists conversations, most recent activity first. | Parameter | Description | | - | - | | `account_id` | Only conversations of this connected account | | `brand_id` | Only conversations of accounts in this brand | | `page_id` | Only conversations of this Facebook Page, for an account with more than 1 Page | | `platform` | Only this platform, for example `instagram` | | `status` | `active` (default) or `archived` | | `limit` | Conversations per page. The default is 25. A value above 100 is treated as 100, and a value of 0 or less, or one that is not an integer, is treated as 25 | | `cursor` | The `next_cursor` of the previous page | | Field | Description | | - | - | | `id` | The conversation ID | | `user_id` | The ID of your SocialAPI account | | `account_id` | The connected account that owns the conversation | | `page_id` | Facebook only: the SocialAPI ID of the Page. Absent elsewhere | | `platform` | The platform slug | | `platform_id` | The platform's ID of the conversation | | `participant_id`, `participant_name`, `participant_picture` | The other person. The name and picture are empty when the platform gives none | | `last_message`, `last_message_at` | The latest message text and its time | | `status` | `active` or `archived` | | `unread_count` | Messages received since you last marked the conversation as read | | `created_at`, `updated_at` | When SocialAPI created and last updated the conversation, in ISO 8601 UTC | | `sync_state`, `last_synced_at` | `idle`, `syncing`, or `failed`, and when the list last completed a refresh. `sync_state` is set only when you pass `account_id`: see [Refresh the inbox manually](/guides/inbox#refresh-the-inbox-manually) | `PATCH /v1/inbox/conversations/{id}` with `{ "status": "archived" }` or `{ "status": "active" }` changes the status and returns `{ "success": true }`. Any other value returns `400 validation.field_invalid`. `POST /v1/inbox/conversations/{id}/read` sets `unread_count` to 0 and returns `{ "success": true }`. It does not send a read receipt to the person. ## Read messages `GET /v1/inbox/conversations/{id}/messages` lists the messages of a conversation, newest first. ```bash theme={null} curl "https://api.social-api.ai/v1/inbox/conversations/7f1c4e2b-2841-41a8-85d7-27b0f37ac067/messages?limit=25" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "id": "0e4d2f6a-5b1c-4c7e-9a3d-8f2b6c1d4e7a", "conversation_id": "7f1c4e2b-2841-41a8-85d7-27b0f37ac067", "platform_id": "m_AbCdEf123", "direction": "outgoing", "text": "Yes, the blue one ships today.", "sender_id": "17841400000000001", "sender_name": "acmecorp", "attachment_type": null, "attachment_url": null, "reply_to": null, "status": "sent", "status_updated_at": "2026-10-13T14:32:00Z", "created_at": "2026-10-13T14:32:00Z" } ], "pagination": { "has_more": false, "next_cursor": "" }, "sync_state": "idle", "last_synced_at": "2026-10-13T14:32:05Z" } ``` | Field | Description | | - | - | | `id` | The message ID in SocialAPI | | `platform_id` | The platform's message ID. It is the `{messageId}` of the reaction endpoints | | `direction` | `incoming` (the person wrote it) or `outgoing` (a connected account sent it) | | `text` | The message text. An attachment-only outgoing message holds `[Attachment]` | | `sender_id`, `sender_name` | Who sent it | | `attachment_type`, `attachment_url` | The attachment, or `null`. For WhatsApp and Telegram the URL is a SocialAPI URL: see [Fetch an inbound attachment](/guides/inbox#fetch-an-inbound-attachment) | | `reply_to` | `{ "platform_id": "..." }` when the message replies to an earlier message, otherwise `null` | | `status` | Outgoing messages only: `pending`, `sent`, `delivered`, `read`, or `failed`. It advances as the platform reports delivery | | `status_updated_at` | When `status` last changed. Outgoing messages only | | `created_at` | When the message was sent or received | `limit` is 25 by default. A value above 200 is treated as 200, and a value of 0 or less, or one that is not an integer, is treated as 25. See [Pagination](/guides/pagination#paginated-endpoints). ## Send a message `POST /v1/inbox/conversations/{id}/messages` sends a message into a conversation. A request must hold `text`, an attachment, or interactive content, and can hold all three. A request with none returns `400 validation.field_required`. | Field | Description | | - | - | | `account_id` | Required. The connected account that sends the message | | `text` | The message text. Required with `quick_replies` and `buttons` | | `attachments` | A list of attachments, described below | | `attachment_url` | Deprecated shorthand for one attachment, ignored when `attachments` is set. A public media URL | | `reply_to_message_id` | Facebook and Zalo: make the first message a reply to the message with this `platform_id`. On Instagram connected through Instagram Login, the send fails with `422 platform.instagram.invalid_parameter`. Other platforms ignore the field | | `message_tag` | Instagram Login: `HUMAN_AGENT`, see [The messaging window](#the-messaging-window). Not sent on Facebook or on Instagram connected through [Facebook Login](/platforms/instagram#connect-through-facebook-login) | | `quick_replies`, `buttons`, `cards` | Interactive content, described in [Interactive messages](#interactive-messages). Set 1 of the 3. A Facebook send delivers only `text` and ignores these fields | | `list_sections`, `list_button` | WhatsApp only: a list menu. See [WhatsApp](/platforms/whatsapp) | SocialAPI sends text first, then each attachment in order, as separate messages. Text plus 2 images arrives as 3 messages, and the request still costs 1 credit. ### Attachments Each item of `attachments` names a media source: | Field | Description | | - | - | | `source_type` | Required. `url` (a public media URL), `media_id` (a file of your [media library](/guides/media)), or `platform_attachment_id` (media already uploaded to the platform). Another value, or none, returns `400 validation.attachment_invalid` | | `source` | Required. The URL, media ID, or platform attachment ID | | `type` | `image`, `video`, `audio`, or `file`. SocialAPI infers it from the file extension for `url` and from the stored file for `media_id`, and treats an unknown extension as `file`. It is required for `platform_attachment_id`: without it the call returns `400 validation.attachment_type_required` | | `caption` | Text shown with the attachment instead of as a separate message. WhatsApp (not on audio) and Telegram. Every other platform ignores it | | `voice` | `true` sends audio as a voice note. WhatsApp only, for Ogg audio with the OPUS codec | ```bash theme={null} curl -X POST "https://api.social-api.ai/v1/inbox/conversations/7f1c4e2b-2841-41a8-85d7-27b0f37ac067/messages" \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "text": "Here is your receipt", "attachments": [ { "source_type": "url", "source": "https://cdn.example.com/receipt.jpg" }, { "source_type": "media_id", "source": "550e8400-e29b-41d4-a716-446655440000" } ] }' ``` The platform fetches a `url` attachment from your URL, so it must be reachable without a login. Facebook accepts files up to 25 MB. Instagram accepts images up to 8 MB and video, audio, and PDF files up to 25 MB. SocialAPI resolves every `media_id` before it sends anything, so an unknown or unfinished `media_id` returns `400 validation.media_unresolved` and sends no message. If a send fails part way, the earlier messages stay delivered. The error carries `error.meta.sent_count` (messages sent) and `error.meta.failed_index` (the item that failed, counting text as item 0 when present), so you can retry from there. Some platforms accept less. X sends text only and rejects an attachment with `400 validation.attachment_unsupported`. Bluesky sends only `text`. Zalo sends a `url` or `media_id` attachment as an image unless its `type` is `gif` or `sticker`. Telegram accepts attachments with captions. See [X](/platforms/twitter#direct-messages), [Bluesky](/platforms/bluesky#direct-messages), [Zalo](/platforms/zalo#direct-messages), and [Telegram](/platforms/telegram). ### Interactive messages Instagram sends structured, tappable content. A Facebook DM send delivers only `text` and ignores `quick_replies`, `buttons`, and `cards`, so the rules in this section apply to Instagram. Buttons do work on a Facebook [private reply](/guides/comments#send-a-private-reply). Set at most 1 of `quick_replies`, `buttons`, or `cards` per message. More than 1 returns `400 validation.interactive_conflict`. When the person taps a quick reply or a `postback` button, SocialAPI sends a [`dm.postback`](/guides/webhook-events#dm-postback) event with the `payload` you set. To answer taps without your own handler, use an [Automation](/guides/automations#buttons-and-multi-step-flows). Interactive content follows the messaging window like any message. #### Quick replies Quick replies are up to 13 tappable chips above the keyboard. They need `text`. | Field | Description | | - | - | | `quick_replies[].title` | Required. The chip label, 20 characters at most | | `quick_replies[].payload` | Optional. The value returned in the `dm.postback` event | More than 13 returns `400 validation.too_many_quick_replies`. A title over 20 characters returns `400 validation.quick_reply_title_too_long`. #### Buttons Buttons are a text bubble with 1 to 3 buttons. `text` is the body and holds 640 characters at most (`400 validation.text_too_long`). | Field | Description | | - | - | | `buttons[].type` | Required. `url` or `postback`. Another value returns `400 validation.invalid_button_type` | | `buttons[].title` | Required. The label, 20 characters at most. An empty or longer title returns `400 validation.invalid_button_title` | | `buttons[].url` | Required when `type` is `url`. The destination | | `buttons[].payload` | Required when `type` is `postback`. The value returned in the `dm.postback` event | More than 3 buttons returns `400 validation.invalid_button_count`. #### Cards Cards are a carousel of up to 10 cards, each with an image, a title, a subtitle, and up to 3 buttons. | Field | Description | | - | - | | `cards[].title` | Required. 80 characters at most | | `cards[].subtitle` | Optional. 80 characters at most | | `cards[].image_url` | Optional. The image URL | | `cards[].buttons` | Optional. Up to 3 buttons, with the fields of `buttons` above | More than 10 cards returns `400 validation.too_many_cards`. More than 3 buttons on a card returns `400 validation.too_many_buttons`. A title or subtitle over 80 characters returns `400 validation.card_text_too_long`. ```bash theme={null} curl -X POST "https://api.social-api.ai/v1/inbox/conversations/7f1c4e2b-2841-41a8-85d7-27b0f37ac067/messages" \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "text": "Your order has shipped.", "buttons": [ { "type": "url", "title": "Track order", "url": "https://shop.example.com/orders/123" }, { "type": "postback", "title": "Talk to support", "payload": "SUPPORT" } ] }' ``` WhatsApp has buttons with its own rules and list menus, and Zalo takes buttons and 1 card. Their pages describe both: [WhatsApp](/platforms/whatsapp), [Zalo](/platforms/zalo#direct-messages). Telegram, X, and Bluesky do not support interactive content. ## React to a message `POST /v1/inbox/conversations/{id}/messages/{messageId}/reaction` adds your emoji reaction to a message, and replaces your earlier one. It works on Instagram and Facebook. Other platforms return `501 resource.not_supported`. ```bash theme={null} curl -X POST "https://api.social-api.ai/v1/inbox/conversations/7f1c4e2b-2841-41a8-85d7-27b0f37ac067/messages/m_AbCdEf123/reaction" \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "reaction": "🎉" }' ``` Response (HTTP 200): ```json theme={null} { "success": true } ``` `reaction` is the emoji character itself, a single emoji. Anything else returns `400 validation.invalid_reaction`. `DELETE` on the same path, with `account_id` as a query parameter, removes the reaction. A reaction costs no credit. ## The messaging window Most platforms let you message a person only for a limited time after their last message. A send outside the window fails, and SocialAPI sends nothing. * **Instagram and Facebook:** 24 hours from the person's last message. Outside it, the send returns `400 platform.instagram.window_closed` or `400 platform.facebook.window_closed`. On Instagram Login, `message_tag: "HUMAN_AGENT"` extends the window to 7 days. Instagram Login accepts no other tag: any other value returns `400 validation.invalid_message_tag`. A Facebook Page, and an Instagram account connected through Facebook Login, send no `message_tag`: the DM goes out untagged. * **WhatsApp:** a free-form message needs the 24-hour service window, and outside it the send returns `400 whatsapp.window_closed`. An approved template is the way to reach the person: see [WhatsApp](/platforms/whatsapp#the-24-hour-service-window). * **Zalo:** the person must have interacted with the Official Account in the last 7 days. Outside it, the send returns `422 platform.zalo.not_deliverable`: see [Zalo](/platforms/zalo#the-7-day-messaging-window). * **Telegram:** the person must have messaged the bot first. Before that, the send returns `403 telegram.cannot_message`: see [Telegram](/platforms/telegram). After Instagram or Facebook refuses a conversation for a closed window, SocialAPI refuses further sends on it with the same code until the person messages you again or 24 hours pass. These refusals cost no credit. An Instagram Login send with `message_tag` is not refused this way. A send on Instagram connected through Facebook Login goes out untagged, so it is refused. A [private reply](/guides/comments#send-a-private-reply) does not clear the refusal. The person's next message does. ## Limits | Limit | Value | | - | - | | Conversations per page | Default 25, up to 100 | | Messages per page | Default 25, up to 200 | | Quick replies | 13 per message, 20 characters per title | | Buttons | 3 per message or card, 20 characters per title, 640 characters of body text | | Cards | 10 per message, 80 characters per title and subtitle | | Text on Facebook | 2,000 characters | | Text on Instagram | 1,000 bytes of UTF-8 | | Text through the MCP tool | 1,000 bytes | Each DM request costs 1 interaction credit. When the allowance is used up, the call returns `429 validation.interaction_limit_exhausted`. [Plans and usage](/guides/plans#what-counts-as-a-post-or-interaction) lists what costs a credit. ## Platform support Which platform supports DMs, attachments, interactive content, and reactions is in the [capability matrix](/platforms/overview). Each platform page lists its own rules: [Instagram](/platforms/instagram#direct-messages-and-the-messaging-window), [Facebook](/platforms/facebook#direct-messages-and-the-messaging-window), [X](/platforms/twitter#direct-messages), [WhatsApp](/platforms/whatsapp), [Telegram](/platforms/telegram), [Zalo](/platforms/zalo#direct-messages), and [Bluesky](/platforms/bluesky#direct-messages). Threads, YouTube, Pinterest, and Google Business Profile have no DMs. ## Errors and debugging Check these in order when a send fails: 1. **`401`, `403 auth.insufficient_scope`.** The key is invalid, or a scoped key lacks `dms:send`. 2. **`404 conversation.not_found`, `404 account.not_found`, `403 account.reconnection_required`.** The conversation is not yours or belongs to another connected account than `account_id`, or the account needs [reconnecting](/guides/connect-accounts#reconnect-an-account). 3. **`403 account.permission_missing`.** The account lacks the [permission](/guides/permissions) the call needs: `messages.write` to send a message or a reaction, and `messages.read` to start or read an inbox sync. `error.meta.permission` names it. The conversation and message lists return what SocialAPI stored and never return this error: an account without `messages.read` stores no new messages. 4. **`400 validation.*`.** The body is wrong. `error.meta.field` names the field. The codes are in [Send a message](#send-a-message) and [Interactive messages](#interactive-messages). 5. **`400 platform.instagram.window_closed`, `400 platform.facebook.window_closed`, `400 whatsapp.window_closed`, `422 platform.zalo.not_deliverable`, `403 telegram.cannot_message`.** The messaging window is closed: see [The messaging window](#the-messaging-window). 6. **`422 platform.instagram.thread_unavailable`, `422 platform.facebook.thread_unavailable`.** The platform refused to deliver to this conversation, which can be restricted or unavailable. 7. **`403 platform.instagram.permission_denied`, `403 platform.facebook.permission_denied`.** The platform scopes granted at connect do not cover the action. Reconnect the account and approve every scope. 8. **`429 validation.interaction_limit_exhausted`, `429 platform.{platform}.rate_limit`.** Your allowance, or the platform's own limit. Read `Retry-After`: see [Rate limits](/guides/rate-limits). 9. **`501 resource.not_supported`.** The platform has no DMs, or no reactions. When a conversation or a message is missing from a list, force a refresh with [`POST /v1/inbox/sync`](/guides/inbox#refresh-the-inbox-manually). The full catalog is in [Errors](/guides/errors#inbox-comments-messages-and-reviews). ## Scopes | Scope | Operations | | - | - | | `dms:read` | List and get conversations, list messages, start or read an inbox sync | | `dms:send` | Send a message, add or remove a reaction | ## Events SocialAPI sends DM events to your [webhook endpoint](/guides/webhooks): [`dm.received`](/guides/webhook-events#dm-received), `dm.sent`, `dm.postback`, `dm.referral`, `dm.deleted`, and `dm.status.*`. Each is described, with the platforms that emit it, on [Webhook events](/guides/webhook-events#direct-messages). An account without the `messages.read` [permission](/guides/permissions) receives none of the `dm.*` events, except that, with `messages.write` alone, the account still receives `dm.status.read` on Instagram, `dm.status.delivered` and `dm.status.read` on Facebook, and every `dm.status.*` event on WhatsApp. It receives no `dm.sent` and no `dm.status.sent` on Instagram or Facebook. ## Use from an AI agent | Tool | What it does | | - | - | | `social_api_list_conversations` | List conversations, most recent activity first | | `social_api_get_conversation` | Get one conversation | | `social_api_list_messages` | List the messages of a conversation, newest first | | `social_api_send_dm` | Send a text message with optional quick replies or buttons. On Facebook the tool delivers only the text. The text is 1,000 bytes at most. The tool sends no attachments and no cards: use the REST endpoint for them | | `social_api_update_conversation` | Archive a conversation, or set it back to `active` | | `social_api_mark_conversation_read` | Mark a conversation as read | There is no tool for reactions or for inbox sync. See [MCP tools](/integrations/tools#direct-messages). ## Related * [Comments](/guides/comments): private replies start a DM conversation from a comment. * [Automations](/guides/automations): send DMs and answer button taps without your own handler. * [Webhook events](/guides/webhook-events#direct-messages): the DM event payloads. * [Rate limits](/guides/rate-limits): platform limits on sending. * [Platform support](/platforms/overview): which platform supports which DM feature. * [Account permissions](/guides/permissions): the permissions an account needs to read and send messages. * [Unified inbox](/guides/inbox): inbound attachments and manual sync. # Errors Source: https://docs.social-api.ai/guides/errors Look up every error code the SocialAPI REST API and MCP server return, with its HTTP status, what triggers it, and how to handle it. Every failed request to the SocialAPI REST API returns a JSON error with an HTTP status, a stable `error.code`, a human-readable `error.message`, and a `request_id`. Branch on `error.code`, never on `error.message`: messages change, codes do not. This page lists every code a client using an API key or the MCP server can receive. ```json theme={null} { "error": { "code": "validation.field_required", "message": "Required field is missing: platform", "meta": { "field": "platform" } }, "request_id": "a1b2c3d4" } ``` | Field | Description | | - | - | | `error.code` | A dot-separated code, stable across releases. Match the whole code, or a prefix such as `platform.` or `billing.` | | `error.message` | A description for logs and display | | `error.meta` | Structured context, present on some errors. `meta.field` names the invalid input, `meta.max` is set on `automation.limit_reached` and `meta.plan` on `whatsapp.paid_plan_required`, `meta.retry_after_seconds` is how long to wait | | `request_id` | Identifies the request. It equals the `X-Request-ID` response header | `request_id` is the `X-Request-ID` request header you sent when it matches `^[a-zA-Z0-9_-]{1,64}$`. Otherwise it is 8 lowercase hexadecimal characters that SocialAPI generates. Quote it in support requests: see [Debugging](/guides/debugging). A `429` response carries `Retry-After` (in seconds) whenever `error.meta.retry_after_seconds` is present. **MCP server.** A failed tool call returns `isError: true` and a text result holding JSON: `{"error": "", "code": ""}`, with the `error.meta` fields added at the top level. The `code` values are the ones on this page. A request refused before the tool runs, such as an expired token, returns an HTTP error: see [Authentication](/guides/authentication). ## Code namespaces The first segment of a code tells you which part of the system refused the request. | Namespace | Meaning | | - | - | | `auth.*` | Authentication and authorization | | `account.*`, `accounts.*` | Connected accounts: lookup, state, Pages, sync | | `brand.*` | Brand lookup | | `platform..*` | The platform (Instagram, TikTok, and the rest) refused the request, or SocialAPI refused it for a platform-specific reason | | `validation.*` | A request field, parameter, or media item is invalid | | `resource.*` | Generic lookup, conflict, and capability errors | | `billing.*` | A plan allowance or plan feature | | `byok.*` | Your own platform app credentials | | `publishing.*` | Publishing a post | | `post.*`, `conversation.*`, `media.*`, `board.*` | Errors on those objects | | `automation.*` | Automations | | `whatsapp.*`, `telegram.*`, `google.*` | Errors specific to those platforms | | `messaging.*`, `inbox.*` | Messaging and sync rate limits | | `ratelimit.*` | Your SocialAPI account's request budget per minute | | `system.internal` | The request failed on SocialAPI's side | | `http.` | A routing or transport error, such as an unknown path | One code has no namespace: `ambiguous_post_id`. ## Authentication and access Every `/v1` request needs `Authorization: Bearer `. See [Authentication](/guides/authentication) and [Scoped API keys](/guides/scoped-keys). | HTTP | Code | When it occurs | | - | - | - | | 401 | `auth.missing_header` | The `Authorization` header is absent | | 401 | `auth.empty_token` | The header is `Authorization: Bearer` with no token | | 401 | `auth.invalid_key` | The API key does not exist, was revoked, or was rotated away | | 401 | `auth.invalid_token` | The bearer token is neither a valid API key nor a valid OAuth 2.1 access token, or it has expired | | 401 | `auth.user_not_found` | The token belongs to a SocialAPI account that no longer exists | | 403 | `auth.insufficient_scope` | A [scoped API key](/guides/scoped-keys) lacks the scope the request needs, or a scoped key called an admin operation (managing keys, brands, invites, or `PATCH /v1/users/me`). `meta.required_scope` names the scope when one applies | | 402 | `billing.past_due` | Your subscription payment failed twice. Every endpoint returns it except every method of `/v1/users/me` and every `/v1/billing/*` endpoint. See [Past-due accounts](/guides/plans#past-due-accounts) for how access returns | | 403 | `auth.state_mismatch` | The `state` you passed to a connect step belongs to a different API key | | 403 | `account.permission_missing` | The connected account does not have the [permission](/guides/permissions) the call needs. `meta.permission` names it. Add it with `PATCH /v1/accounts/{id}`, or reconnect the account when the platform has not granted it. Publishing, post removal, live post metrics, and the stored inbox lists answer differently: see [Account permissions](/guides/permissions#what-an-account-can-do-right-now) | | 400 | `auth.oauth.invalid_client` | The MCP client's `client_id` is unknown. See [OAuth for MCP clients](/integrations/oauth) | | 400 | `auth.oauth.invalid_redirect` | The MCP client's `redirect_uri` is not registered for its `client_id` | The `/oauth/token` endpoint answers errors in the OAuth format (`error`, `error_description`) instead of this envelope: see [OAuth for MCP clients](/integrations/oauth#token-endpoint-errors). ## Request validation These codes apply to any endpoint. `error.meta.field` names the offending input when the input is a single field. | HTTP | Code | When it occurs | | - | - | - | | 400 | `validation.body_invalid` | The request body is not valid JSON, or has the wrong shape | | 400 | `validation.field_required` | A required field is missing. `meta.field` names it | | 400 | `validation.field_invalid` | A field or parameter has the wrong type, format, or value. `meta.field` names it. Examples: `from` is not RFC 3339, `cursor` is malformed, a webhook `url` is not an `https` URL, a webhook event type is unknown, a media `url` is not a public `http` or `https` address, `scheduled_at` is in the past on a post with targets, `stagger` is sent on a draft, or `stagger` is outside `0 <= min_seconds <= max_seconds <= 86400` | | 400 | `validation.id_invalid` | A comment or review ID in the path is neither a platform ID nor a SocialAPI ID | | 400 | `validation.id_platform_mismatch` | A comment or review ID belongs to a different platform than the `account_id` | ## Not found, conflicts, and unsupported operations | HTTP | Code | When it occurs | | - | - | - | | 404 | `resource.not_found` | The object does not exist or is not visible to your key. `meta.resource` names the type (for example `post`, `automation`, `brand`) and `meta.id` the ID | | 404 | `account.not_found` | The `acc_` ID is not a connected account of your SocialAPI account | | 404 | `brand.not_found` | The brand is outside the brands your [brand-restricted key](/guides/scoped-keys) can reach. The Facebook, Google, and LinkedIn assign calls also return it for a brand that does not exist or is not yours | | 404 | `conversation.not_found` | The conversation does not exist or is not yours | | 404 | `media.not_found` | The media item does not exist or is not yours | | 409 | `resource.conflict` | The request conflicts with current state. Duplicate post content and invite creation use it: see below | | 409 | `ambiguous_post_id` | The platform post ID you passed matches more than one post. Use the SocialAPI post ID | | 501 | `resource.not_supported` | The operation does not exist on this account's platform. `meta.platform` names it. See the [capability matrix](/platforms/overview) | | 422 | `account.platform_unsupported` | The account's platform has no reviews, and the request was for reviews | | 4xx, 5xx | `http.` | A routing or transport error. `http.404` is an unknown path. `GET /v1/inbox/media/{id}` also returns `http.404` (unknown media), `http.410` (media expired), `http.503` (too many waiting requests, retry after 1 second), and `http.504` (media still being fetched, send the request again until you get 200 or 410) | ## Rate limits Rate-limit codes are `429` and are covered in [Rate limits](/guides/rate-limits), which says what each limit counts and how to retry. | HTTP | Code | When it occurs | | - | - | - | | 429 | `ratelimit.exceeded` | Your SocialAPI account used its request budget for the current minute. `meta.limit` is the budget and `meta.retry_after_seconds` the wait. See [Request rate limit](/guides/rate-limits#request-rate-limit) | | 429 | `publishing.velocity_limit` | A publish target reached its post cap per hour or per 24 hours. `meta.window` is `hour` or `day` | | 429 | `platform..rate_limit` | The platform's own limit. Wait, then retry with backoff. Credit rules: see [when a credit comes back](/guides/plans#when-a-credit-comes-back) | | 429 | `inbox.sync_rate_limited` | `POST /v1/inbox/sync` ran again before the wait for the same target ended. `meta.retry_after_seconds` is set | | 429 | `accounts.sync_rate_limited` | `POST /v1/accounts/{id}/sync` ran again before the wait for the same account ended. `meta.retry_after_seconds` is set | ## Billing and plan allowances Allowances and billing periods are defined in [Plans and usage](/guides/plans). | HTTP | Code | When it occurs | | - | - | - | | 429 | `validation.post_limit_exhausted` | The post allowance for the billing period is used up. It resets at the start of the next period | | 429 | `validation.interaction_limit_exhausted` | The interaction allowance for the billing period is used up | | 429 | `billing.post_limit` | `POST /v1/posts/import` holds more posts than the allowance has credits left. `error.message` gives the credits needed and remaining | | 403 | `billing.brands_limit` | Creating the brand would exceed your plan's brand allowance | | 403 | `billing.platform_not_available` | The platform slug is not one of the connectable platform slugs, such as `trustpilot`, or the platform is in beta and your account has no beta access | | 413 | `billing.storage_quota` | The upload would exceed your plan's media storage | | 429 | `billing.export_limit` | You reached your plan's number of exports per billing period | | 429 | `billing.export_cooldown` | The account was exported within your plan's cooldown period | | 429 | `billing.export_concurrent` | Another export is still pending or processing | | 403 | `whatsapp.paid_plan_required` | Sending a WhatsApp template needs a paid plan. Replies to people who messaged you do not | | 402 | `billing.past_due` | See [Authentication and access](#authentication-and-access) | ## Connect an account These codes come from `POST /v1/accounts/connect`, the steps that follow the consent screen, and the Page and location pickers. See [Connect accounts](/guides/connect-accounts) for the flow. A code marked **Redirect** has no HTTP status: it reaches your `redirect_uri` as `status=error` and `error=`, with `error_description`. | HTTP | Code | When it occurs | | - | - | - | | 400 | `validation.connection_type_invalid` | `metadata.connection_type` is present but is not one of the platform's values: `business` or `creator` on TikTok, `profile` or `organization` on LinkedIn. `meta.field` is `connection_type` | | 400 | `validation.platform_invalid` | `platform` is `linkedin_page` on `POST /v1/accounts/connect` or `POST /v1/invites`. Connect LinkedIn member profiles and company Pages with `linkedin`. `meta.field` is `platform` | | 400 | `validation.customer_state_too_long` | `state` is longer than 512 characters | | 400 | `validation.permission_unknown` | A value in `permissions` is not a [permission](/guides/permissions#the-permissions). `meta.permission` names it. `POST /v1/accounts/connect`, `POST /v1/invites`, and `PATCH /v1/accounts/{id}` return it | | 400 | `validation.permissions_empty` | `permissions` is an empty list. Send at least one permission, or omit the field. `PATCH /v1/accounts/{id}` also returns it, where `null` gives the account every permission | | 400 | `validation.redirect_uri_invalid_format` | `redirect_uri` is malformed | | 400 | `validation.redirect_uri_not_whitelisted` | `redirect_uri` is not registered. Register it in the dashboard first | | 400 | `validation.state_invalid` | A connection `state` is unknown or expired | | 400 | `validation.profile_not_in_set` | The `platform_account_id` you selected is not one of the candidates for this connection | | 400 | `validation.page_not_in_set` | A Page ID in `page_ids` is not one of the candidates for this connection. `meta.platform_page_id` names it | | 400 | `validation.page_already_assigned` | The Facebook Page already belongs to another brand. `meta.brand_id` names that brand | | 403 | `platform.facebook.page_cap` | The selection or assignment would give the brand more Facebook Pages than your plan allows per brand. `meta.limit` is the allowance. See [Facebook Pages per brand](/guides/plans#facebook-pages-per-brand) | | 409 | `platform.instagram.flow_conflict` | You hold the Instagram account through the other login path: you selected it with Facebook Login while you hold it through Instagram Login, or the reverse. `meta.accounts` is a comma-separated string of the Instagram account IDs. Disconnect the account first. On Instagram Login it reaches your `redirect_uri` as `error=platform.instagram.flow_conflict` | | 400 | `validation.page_not_granted` | The Facebook Page is not accessible with this login | | 400 | `validation.location_not_granted` | The Google location is not accessible with this login | | 400 | `validation.location_already_assigned` | The Google location already belongs to another brand | | 400 | `validation.invalid_account_id` | The Google account ID has an invalid format | | 400 | `validation.invalid_location_id` | The Google location ID is not valid | | 400 | `validation.profile_not_granted` | The LinkedIn member profile or company Page is not accessible with this login. `meta.platform_account_id` names it | | 400 | `validation.profile_already_assigned` | The LinkedIn member profile or company Page already belongs to another brand. `meta.brand_id` names that brand | | 400 | `validation.invalid_profile_id` | The LinkedIn `{profileId}` in the path is not a valid URL-encoded value | | 400 | `validation.auth_code_expired` | The TikTok authorization code expired or was already used. Start the connection again | | 409 | `account.already_linked` | The brand already holds a different account on this platform (never on LinkedIn, where a brand holds any number of accounts), or `lock_accounts_to_brand` is `true` and the account belongs to another of your brands (see [Reconnect an account](/guides/connect-accounts#reconnect-an-account)). In the 409 response, `meta.brand_id` is the brand you named, or with the lock, the brand that holds the account. Telegram, Bluesky, App Store Connect, and choosing a Google location or a LinkedIn member profile or company Page with `POST /v1/accounts/pending/{connection_id}/select` return the 409. On a platform with a consent screen it reaches your `redirect_uri` as `error=account.already_linked` | | 409 | `account.different_login` | The brand's Facebook, or its Instagram connected through Facebook Login, is connected through a different login. Disconnect it first. During a connection it reaches your `redirect_uri` as `error=account.different_login`; assigning a Page returns the 409 | | 403 | `account.reconnection_required` | The platform token is no longer usable. Reconnect with `POST /v1/accounts/connect`, passing the existing connected account's brand and platform: see [Reconnect an account](/guides/connect-accounts#reconnect-an-account) | | 400 | `account.unsupported_platform` | The platform value is not recognized | | Redirect | `platform.facebook.no_pages` | The Facebook login has no Pages | | Redirect | `platform.linkedin.no_pages` | A LinkedIn connect with `metadata.connection_type: "organization"` by a member who administers no company Page | | Redirect | `platform.instagram.no_pages` | The Facebook login has no Instagram account: see [Connect through Facebook Login](/platforms/instagram#connect-through-facebook-login) | | Redirect | `platform.google.no_locations` | The Google login has no Business Profile locations | | Redirect | `platform.google.insufficient_scope` | The person left the Business Profile permission unticked on Google's consent screen. Start a new connection and allow every permission | | Redirect | `platform.youtube.no_channels` | The Google login has no YouTube channels | | 404 | `platform.youtube.no_uploads_playlist` | The YouTube channel has no uploads playlist | | Redirect | `platform.facebook.no_assignable_pages` | On a connection without a [selection step](/guides/connect-accounts#facebook-pages), every Page on the login already belongs to another brand | | Redirect | `platform.instagram.no_assignable_pages` | On a connection without a selection step, every Instagram account on the Facebook login already belongs to another brand, or you hold it through Instagram Login | | 409 | `platform.google.brand_has_account` | Assigning a location when the brand already has a Google Business Profile connected. Disconnect it first, or choose another brand | | 502 | `platform.instagram.api_error` | Instagram's token exchange failed during connection | | 400 | `account.page_not_found` | The `page_id` matches no Page of the account | | 400 | `account.no_active_page` | The account has no active default Page | | 400 | `account.page_inactive` | The Page is deactivated | | 400 | `account.pages_not_supported` | The platform has no Pages | | 400 | `account.use_boards_endpoint` | A Pinterest account has boards, not Pages. Use `/v1/accounts/{id}/boards` | | 400 | `account.boards_not_supported` | The account is not Pinterest, which is the only platform with boards | | 403 | `whatsapp.number_banned` | Meta banned the WhatsApp number, so it cannot be connected | | 400 | `whatsapp.ambiguous_phone_number` | The WhatsApp Business account has more than one phone number. Pass `phone_number_id` | | 400 | `whatsapp.no_phone_number` | The WhatsApp Business account has no phone number. Add one, then connect again | | 409 | `whatsapp.business_app_number` | The number is still in the WhatsApp Business app and is already registered. Do not call register for it | | 400 | `platform.bluesky.auth_factor_required` | The Bluesky account has email two-factor authentication on. Retry with the emailed code as `metadata.auth_factor_token` | ## BYOK app credentials Platforms that use [your own app](/guides/platform-credentials) (X) return these codes. | HTTP | Code | When it occurs | | - | - | - | | 412 | `byok.credentials_missing` | You saved no app credentials for the platform. `POST /v1/accounts/connect` returns it | | 401 | `byok.credentials_invalid` | The platform rejected the saved app credentials. When you connect an account, it arrives as the `error` parameter on your redirect URI | | 412 | `byok.app_access_denied` | The platform's app tier does not allow the call. Upgrade the app's access on the platform. When you connect an account, it arrives as the `error` parameter on your redirect URI | ## Brands, invites, and keys | HTTP | Code | When it occurs | | - | - | - | | 400 | `validation.field_required` | `name` is missing when creating a brand or a key, or `brand_id` is missing when creating an invite | | 400 | `validation.field_invalid` | `expires_in_days` is not one of 1, 3, 7, 14, or 30 (including when it is omitted) | | 409 | `resource.conflict` | `POST /v1/invites` found an active invite for the brand and platform, or a connected account for that platform on the brand. Only `error.message` tells the two apart | | 400 | `validation.scope_invalid` | A key was created or edited with an unknown scope. The message names it | | 400 | `validation.brand_invalid` | A key's `brand_ids` names a brand that does not exist or that you do not own | | 403 | `billing.brands_limit` | Creating the brand would exceed your plan's allowance | An empty `GET /v1/invites` list does not mean `POST /v1/invites` succeeds: the brand can already hold a connected account for the platform. Check `GET /v1/accounts` as well. ## Publish a post These codes come from `POST /v1/posts`, `POST /v1/posts/validate`, the post lifecycle endpoints, and the post media fields. Platform-specific media rules are under [Platform-specific codes](#platform-specific-codes). See [Publishing](/guides/publishing) and [Delivery](/guides/delivery). | HTTP | Code | When it occurs | | - | - | - | | 400 | `publishing.validation_failed` | The post failed validation. `error.meta.issues` lists each issue. Fix them, or send `skip_validation` | | 400 | `publishing.no_targets` | The post has no publishable targets | | 400 | `publishing.duplicate_target` | One account appears twice in the request with the same Page | | 400 | `validation.field_required` | `text` or another required post field is missing | | 400 | `validation.board_id_required` | A Pinterest target has no `board_id` | | 400 | `validation.duplicate_board_id` | A Pinterest target sets both `board_id` and `page_id`. Use `board_id` | | 400 | `validation.use_board_id` | A Pinterest board was set in `platform_data.board_id`. Set `board_id` on the target | | 400 | `validation.use_page_id` | `board_id` was set on a non-Pinterest target. Use `page_id` | | 409 | `resource.conflict` | The same account has a post with the same title and text that was created in the last 24 hours and is now `published`. Send `skip_duplicate_check: true` to publish the content again | | 409 | `post.state_invalid` | The post's status does not allow the call. Edit works on `draft`, `scheduled`, or `failed`. Publish works on `draft` or `scheduled`. Retry works on `failed` or `partial`. Delete does not work on a `publishing` post. `meta.state` is the current status | | 400 | `post.no_retryable_deliveries` | The post has no failed targets to retry | | 422 | `account.platform_unsupported` | `POST /v1/posts` or `PATCH /v1/posts/{pid}` names a target account on Telegram or App Store Connect, which do not publish posts. `error.meta.account_id` names it. Remove that target | | 403 | `account.reconnection_required` | A publish that would go out now names a target account that must be reconnected: `POST /v1/posts` without `scheduled_at` or `queue_id`, `POST /v1/posts/{pid}/publish`, or `POST /v1/posts/{pid}/retry`. `error.meta.id` names the account. Nothing is published and the post keeps its status. [Reconnect the account](/guides/connect-accounts#reconnect-an-account), then send the request again | | 422 | `publishing.media.missing_source` | A media item has no source | | 422 | `publishing.media.invalid_source_type` | `source_type` is not `url`, `media_id`, or `platform_attachment_id` | | 422 | `publishing.media.missing_type` | A `platform_attachment_id` item has no `type` (`image`, `video`, `audio`, or `file`) | | 422 | `publishing.media.untyped_url` | The media type cannot be read from the `url`. Use a URL with an extension such as `.jpg` or `.mp4`, or upload the file | | 422 | `publishing.media.unsupported_source` | The platform does not accept `platform_attachment_id` media. Only Instagram, Threads, LinkedIn, and X do | | 422 | `publishing.media.mixing_unsupported` | Instagram or Threads cannot combine a `platform_attachment_id` with other media. It must be the only item | | 500 | `publishing.media.proxy_unavailable` | SocialAPI could not prepare the media for the platform. Retry the request | | 429 | `publishing.velocity_limit` | See [Rate limits](#rate-limits) | `POST /v1/posts` can also return the `billing.*` and `validation.post_limit_exhausted` codes in [Billing and plan allowances](#billing-and-plan-allowances). Publishing failures that happen after the request returns appear on the post: see [Delivery failures](#delivery-failures). ## Delivery failures A target that fails after `POST /v1/posts` returned is a failed delivery. It does not return an HTTP error. Its code is in `targets[].error.code` on the post (`GET /v1/posts/{pid}`) and in `results[].error.code` of the `post.failed` and `post.partial` webhooks. `GET /v1/events` has no error code. See [Delivery](/guides/delivery#target-errors). | Code | When it occurs | | - | - | | `publishing.delivery.account_load_failed` | SocialAPI could not load the connected account when the delivery ran | | `account.permission_missing` | The account did not have the `posts.write` [permission](/guides/permissions) when the delivery ran | | `publishing.delivery.page_resolve_failed` | SocialAPI could not resolve the target's Facebook Page | | `publishing.delivery.timeout` | Sending did not finish within 15 minutes | | `validation.publish` | The post failed validation when the delivery ran, after the request had been accepted | | `platform..publish_failed` | The platform failed the publish and gave no code SocialAPI maps | | `platform..` | Any [platform error](#platform-errors) the platform raised during the publish | | `system.internal` | The target never finished, or a SocialAPI error stopped it | ## Inbox: comments, messages, and reviews See [Comments](/guides/comments), [Direct messages](/guides/direct-messages), [Reviews](/guides/reviews), and [Mentions](/guides/mentions). Messages and replies spend interaction credits, so they can also return `validation.interaction_limit_exhausted`. | HTTP | Code | When it occurs | | - | - | - | | 400 | `platform.instagram.window_closed`, `platform.facebook.window_closed` | The 24-hour messaging window is closed. The person must message you first | | 400 | `platform.instagram.private_reply_ineligible`, `platform.facebook.private_reply_ineligible` | The comment cannot receive a private reply: it is older than 7 days, or already received one | | 422 | `platform.instagram.attachment_failed`, `platform.facebook.attachment_failed` | The platform could not fetch or accept an attachment on a DM or a private reply. Check that the URL is publicly reachable and the file type and size are supported, then send again. A private reply refused this way leaves the comment eligible | | 422 | `platform.instagram.thread_unavailable`, `platform.facebook.thread_unavailable` | The platform refused to deliver to this conversation. It can be restricted or unavailable | | 409 | `platform.instagram.no_page` | A DM or private reply on a Facebook Login account with no Facebook Page linked to it. Reconnect the account | | 422 | `platform.instagram.invalid_parameter` | Instagram rejected a parameter. Retrying unchanged will not help | | 403 | `platform.instagram.permission_denied`, `platform.facebook.permission_denied`, `platform.whatsapp.permission_denied` | The scopes granted to the connected account do not cover the action | | 400 | `platform.whatsapp.invalid_parameter` | WhatsApp rejected a parameter. `error.message` carries WhatsApp's reason | | 400 | `validation.field_required` | The message has no `text`, attachment, or interactive content. A review ID or a comment path parameter is missing | | 400 | `validation.field_invalid` | `status` is not `active` or `archived`, `moderation_status` is not an allowed value, or `order` is not `newest`, `rating_asc`, or `rating_desc` | | 400 | `validation.attachment_invalid` | An attachment has no `source_type` | | 400 | `validation.attachment_type_required` | An attachment of source `platform_attachment_id` has no `type` | | 400 | `validation.media_unresolved` | A `media_id` attachment could not be resolved | | 400 | `validation.invalid_message_tag` | The `message_tag` is not supported. Instagram Login accepts only `HUMAN_AGENT` | | 400 | `validation.message_required` | `message_id` is missing when reacting to a message | | 400 | `validation.participant_required` | `participant_id` is missing when reacting to a message, or the conversation's X participant is unknown | | 400 | `validation.invalid_reaction` | The reaction is not a single emoji character | | 400 | `validation.interactive_conflict` | More than one of `quick_replies`, `buttons`, and `cards` (Instagram), or of `buttons` and `list_sections` (WhatsApp), was sent | | 400 | `validation.too_many_quick_replies`, `validation.too_many_buttons`, `validation.too_many_cards`, `validation.too_many_sections`, `validation.too_many_rows` | An interactive message exceeds a count limit. Instagram: 13 quick replies, 3 buttons per card, 10 cards. WhatsApp: 3 buttons, 10 sections, 10 rows across all sections | | 400 | `validation.invalid_button_count` | A button template has more than 3 buttons: on an Instagram DM, or on an Instagram or Facebook private reply | | 400 | `validation.invalid_button_title`, `validation.title_too_long`, `validation.quick_reply_title_too_long`, `validation.card_text_too_long`, `validation.description_too_long`, `validation.text_too_long` | A title or text is empty or too long. Limits: button title 20 characters, quick reply title 20, card title 80, card subtitle 80, button template text 640 (Instagram DMs, and private replies on Instagram and Facebook), WhatsApp interactive text 1,024, WhatsApp `list_button` 20, WhatsApp section title 24, WhatsApp row title 24, WhatsApp row description 72 | | 400 | `validation.invalid_button_type` | A button `type` is not `url` or `postback` | | 400 | `validation.button_type_unsupported` | A WhatsApp interactive button is not `postback`. URL buttons exist only in templates | | 400 | `validation.cards_unsupported`, `validation.quick_replies_unsupported` | WhatsApp has no carousel or quick replies. Use buttons or `list_sections` | | 400 | `validation.duplicate_payload` | WhatsApp button payloads are not unique | | 400 | `validation.attachment_unsupported` | X direct messages cannot carry attachments through the API. Send text only | | 400 | `validation.text_required` | X requires message text | | 403 | `platform.bluesky.not_own_record` | Bluesky lets you delete only your own replies. Hide the reply instead | | 403 | `platform.bluesky.not_thread_owner` | Only the author of the original Bluesky post can hide replies to it | | 422 | `platform.bluesky.hidden_replies_full` | The Bluesky thread already holds 300 hidden replies | | 409 | `platform.bluesky.stale_record` | The Bluesky record changed during the request. Retry | | 404 | `resource.not_found`, `platform..` | The comment's post was deleted on the platform. Later requests for the same post return the same 404 | WhatsApp messaging errors are in [WhatsApp](#whatsapp). Telegram's are in [Telegram](#telegram). ## Automations Returned by `POST /v1/automations` and `PATCH /v1/automations/{id}`. The rules behind each code are in [Automations](/guides/automations). | HTTP | Code | When it occurs | | - | - | - | | 409 | `automation.limit_reached` | The account already has 50 Automations. `error.meta.max` is `50` | | 422 | `automation.platform_unsupported` | The account is not an Instagram or Facebook account | | 404 | `resource.not_found` | No Automation with this ID is visible to your key (`error.meta.resource` is `automation`) | | 404 | `account.not_found` | `account_id` is not an active account your key can reach | | 403 | `auth.insufficient_scope` | The key lacks `automations:write`, or the scope one of the actions needs (`inbox:write` or `dms:send`). `meta.required_scope` names it | A definition that breaks a rule returns `400` with one of these codes, and `error.meta.field` points at the offending field (for example `steps[1].buttons`). | Code | When it occurs | | - | - | | `validation.field_required` | `account_id` is missing on create | | `validation.name_invalid` | `name` is empty or longer than 100 characters | | `validation.status_invalid` | `status` is not `active` or `paused` | | `validation.trigger_invalid` | `trigger` is not `comment` or `dm` | | `validation.post_ids_invalid` | `post_ids` on a `dm` Automation, more than 100 IDs, or an ID that is empty or over 100 characters | | `validation.steps_invalid` | Fewer than 1 or more than 10 steps | | `validation.step_key_invalid` | A step key is duplicated or not 1 to 32 characters of `a-z`, `0-9` and `_`, or no step is keyed `start` | | `validation.step_ref_invalid` | A `next_step` or `fallthrough_step` names a step that does not exist, or a button on a comment Automation leads back to `start` | | `validation.fallthrough_invalid` | `start` has a `fallthrough_step` | | `validation.fallthrough_cycle` | `fallthrough_step` links form a cycle | | `validation.filter_invalid` | Unknown filter type, more than 10 filters on a step, or `follows_account` on `start` or with a `keyword` | | `validation.filter_unsupported` | `follows_account` (or a `follow_gate`) on a Facebook account | | `validation.keywords_invalid` | A keyword filter has no keywords or more than 50, or a keyword is empty or over 100 characters | | `validation.match_invalid` | `match` is not `contains` or `exact` | | `validation.actions_invalid` | A step has no actions | | `validation.action_invalid` | Unknown action type | | `validation.action_not_allowed` | The action is not available on that step: steps after `start` accept only `send_dm`, and each trigger has its own allowed actions | | `validation.action_duplicate` | The same action type appears twice on a step | | `validation.texts_invalid` | An action has no texts or more than 10, a text is empty or over 1,000 characters, or over 640 characters on a `private_reply` or `send_dm` whose step has buttons | | `validation.buttons_invalid` | More than 3 buttons, buttons on a step without a `private_reply` or `send_dm`, a title that is missing or over 20 characters, a `url` button without an `http` or `https` URL, a `postback` button without `next_step`, a button carrying the other type's field, or an unknown button type | | `validation.permission_missing` | The trigger or an action needs a [permission](/guides/permissions#automations) the account does not have. `error.meta.permission` names it | | `validation.follow_gate_invalid` | `follow_gate` sent together with `steps`, or with the `dm` trigger | An action that fails when an Automation runs is not returned as an error. It is recorded on the run with the same code the API call would have returned: see [Firings](/guides/automations#firings). ## Webhooks | HTTP | Code | When it occurs | | - | - | - | | 400 | `validation.field_required` | `url` is missing | | 400 | `validation.field_invalid` | `url` is not a valid `https` URL, or an event type in `events` is unknown | | 400 | `validation.body_invalid` | A `PATCH` set no fields | | 400 | `validation.webhook_verification_failed` | `POST /v1/webhooks/{id}/test` could not build a request to the endpoint URL | ## Media library and exports | HTTP | Code | When it occurs | | - | - | - | | 413 | `billing.storage_quota` | The upload would exceed your plan's storage. Check `GET /v1/media/storage`, delete unused media, or change plan | | 400 | `account.exports_not_supported` | The account is a Pinterest account. Pinterest data is read live and cannot be exported | | 400 | `account.export_enrichment_not_supported` | `include_transcript` or `include_vision` on a TikTok account on the default connection, whose exports hold captions and metrics only. Export again without those options. A TikTok [creator connection](/platforms/tiktok#creator-connection) accepts both | | 400 | `validation.field_invalid` | The export is not completed yet, so its videos cannot be read | | 429 | `billing.export_limit`, `billing.export_cooldown`, `billing.export_concurrent` | See [Billing and plan allowances](#billing-and-plan-allowances) | ## Analytics and insights | HTTP | Code | When it occurs | | - | - | - | | 400 | `validation.from_invalid`, `validation.to_invalid` | `from` or `to` is not formatted `YYYY-MM-DD` | | 400 | `validation.range_inverted` | The end of a date or time range is before its start | | 400 | `validation.range_too_wide` | The analytics range is longer than 400 days | | 403 | `account.permission_missing` | The account does not have the `analytics.read` [permission](/guides/permissions). Account analytics and the platform insight endpoints return it, and so does post analytics when no target of the post has the permission | | 403 | `platform.instagram.insights_scope_missing` | Instagram withdrew the insights access. Reconnect the account | | 403 | `platform.threads.insights_scope_missing` | The account was connected before 2026-10-17 or without the `threads_manage_insights` scope. Reconnect the account | | 403 | `platform.facebook.insights_scope_missing` | The Page was connected before 2026-10-17, its login declined the `read_insights` scope, or the person who connected it does not hold the Analyze task on the Page. A reconnect fixes the first two; for the third, grant the Analyze task on Facebook, then reconnect | | 422 | `platform.instagram.insights_unavailable` | Instagram has no insights of the requested kind for this media | | 400 | `validation.days_invalid` | Facebook Page insights `days` is below 1 or above 365 | | 400 | `validation.metric_required` | No metric was passed to a Threads or Google Business Profile insights request | | 400 | `validation.metric_invalid` | A metric is not valid for the request (and, on Threads, for the `level`) | | 400 | `validation.level_invalid` | Threads insights `level` is not `media` or `account` | | 400 | `validation.post_id_required` | Threads insights at `level=media` has no `post_id` | | 400 | `validation.range_not_supported` | `since` or `until` was sent at Threads `level=media`, which returns lifetime totals | | 400 | `validation.since_invalid`, `validation.until_invalid` | Threads `since` or `until` is not a Unix timestamp in seconds | | 400 | `validation.since_too_early`, `validation.until_too_early` | Threads `since` or `until` is before 2024-04-13 | | 400 | `validation.breakdown_required`, `validation.breakdown_unknown`, `validation.breakdown_multiple`, `validation.breakdown_not_applicable`, `validation.breakdown_range_unsupported` | A Threads `follower_demographics` request has a missing, unknown, repeated, misplaced breakdown, or sends `since` or `until`. The breakdowns are `country`, `city`, `age`, and `gender` | | 400 | `validation.start_date_required`, `validation.start_date_invalid`, `validation.end_date_required`, `validation.end_date_invalid` | A Google insights date is missing or not formatted `YYYY-MM-DD` | | 400 | `validation.start_month_required`, `validation.start_month_invalid`, `validation.end_month_required`, `validation.end_month_invalid` | A Google search keywords month is missing or not formatted `YYYY-MM` | | 400 | `validation.page_size_invalid` | Google search keywords `limit` is negative or above 100 | | 400 | `validation.username_required` | `username` is missing from a Threads profile lookup | ## Google Business Profile | HTTP | Code | When it occurs | | - | - | - | | 422 | `google.media.single_item_required` | A media upload carried other than exactly one media item | | 422 | `google.media.invalid_category` | The media `category` is not an allowed value | | 422 | `google.media.unresolvable` | The media item could not be resolved | | 400 | `validation.invalid_media_category` | The media `category` is not an allowed value | | 400 | `validation.invalid_media_source` | The media source is not a `url`, a stored media ID, or a Google upload reference | | 400 | `validation.missing_media_source` | A media source is required | | 400 | `validation.media_not_in_account` | The media item does not belong to this location | | 400 | `validation.media_not_updatable` | A Google post's media cannot change after publishing. Delete the post and create a new one | | 400 | `validation.invalid_call_to_action` | The call to action is not valid. `error.message` gives the reason | | 400 | `validation.no_fields_to_update` | A location update set no updatable field | | 503 | `platform.google.service_disabled` | Google's API is temporarily unavailable. Retry the request | | 403 | `platform.google.access_denied` | Google denied access to the location | ## Platform-specific codes ### Instagram and Facebook | HTTP | Code | When it occurs | | - | - | - | | 422 | `platform.instagram.container_failed` | Instagram could not process the media. Check the file against Instagram's specifications | | 422 | `platform.instagram.container_expired` | The media container expired before publishing. Create the post again | | 504 | `platform.instagram.container_timeout` | The media was still processing after about 5 minutes | | 503 | `platform.instagram.1`, `platform.instagram.2` | Instagram reported a temporary failure. Retry the request | | 403 | `platform.instagram.200` | Instagram says the connected account has not granted a scope the call needs. Reconnect the account to grant it | | 404 | `platform.instagram.100` | Instagram says the object does not exist, or does not accept the request. Check the ID | | 404 | `platform.instagram.9010` | Instagram has no account for the participant ID, for example because the person deleted or deactivated the account | | 409 | `platform.instagram.9007` | Instagram is not ready to publish the media yet. Retry the publish | | 403 | `platform.instagram.230` | Instagram withholds the participant profile: the person has not messaged the account or tapped an icebreaker or the persistent menu | | 404 | `platform.facebook.100` | Facebook says the object does not exist, or the Page lacks a scope the call needs. Check the ID and the Page scopes | | 403 | `platform.facebook.230` | Facebook requires the user's consent to access the resource. Reconnect the account | | 504 | `platform.facebook.reel_timeout`, `platform.facebook.story_timeout` | Facebook did not finish processing the Reel or Story video within the time allowed | | 422 | `platform.facebook.reel_processing_failed`, `platform.facebook.story_processing_failed` | Facebook could not process the Reel or Story video | | 400 | `validation.media_invalid` | A Facebook Reel does not have exactly one video, and the post was sent with `skip_validation: true`. Without it, the post returns `400 publishing.validation_failed` | | 400 | `validation.media_mixed` | A Facebook multi-photo post includes a video, and the post was sent with `skip_validation: true`. Without it, the post returns `400 publishing.validation_failed` | | 400 | `validation.media_too_many` | A Facebook multi-photo post has more than 10 images, and the post was sent with `skip_validation: true`. Without it, the post returns `400 publishing.validation_failed` | ### Threads | HTTP | Code | When it occurs | | - | - | - | | 422 | `platform.threads.THREADS_API__LINK_LIMIT_EXCEEDED` | The post holds more than 5 unique links | | 422 | `platform.threads.ERROR`, `platform.threads.EXPIRED` | Threads rejected the media, or the media expired before publishing | | 504 | `platform.threads.container_timeout` | Threads did not finish processing the media after about 4 minutes | | 422 | `platform.threads.801` | `follower_demographics` was requested for an account with fewer than 100 followers | | 429 | `platform.threads.rate_limit` | Threads' own rate limit. Credit rules: see [when a credit comes back](/guides/plans#when-a-credit-comes-back) | | 403 | `platform.threads.` | A call needs a Threads scope that the account lacks, such as hiding a reply without `threads_manage_replies`. `` is Meta's error code | | 404 | `resource.not_found` | The post, reply, or profile does not exist, or a profile is not discoverable | ### TikTok | HTTP | Code | When it occurs | | - | - | - | | 400 | `validation.media_required` | A video post has no media URL | | 400 | `validation.media_count` | A photo post does not hold 2 to 35 images | | 400 | `validation.media_type_mismatch` | An image was sent as a video. Set `media_type` to `photo` and attach 2 or more JPEG or WebP images | | 400 | `validation.unsupported_photo_format` | A photo post includes an image that is not JPEG or WebP | | 400 | `validation.unsupported_media_type` | `media_type` is not `video` or `photo` | | 400 | `validation.post_mode` | `post_mode` is neither `direct` nor `inbox` | | 400 | `validation.privacy_level_required` | A photo carousel has no `privacy_level`. On a TikTok [creator connection](/platforms/tiktok#creator-connection), a published video or photo carousel (not a draft) has none | | 400 | `validation.posting_limit` | A TikTok [creator connection](/platforms/tiktok#creator-connection) has no privacy level TikTok allows right now, so it cannot post. `can_post` is `false` in [Creator info](/platforms/tiktok#creator-info) | | 400 | `validation.privacy_level_unsupported` | A video post sets a `privacy_level` other than `PUBLIC_TO_EVERYONE`, or a `privacy_level` that is not a string. `meta.privacy_level` is the value you sent | | 400 | `validation.invalid_privacy_level` | A photo carousel sets a `privacy_level` the account does not allow, or, on a TikTok creator connection, a video or a photo carousel does. `meta.privacy_level` is the value you sent | | 400 | `validation.branded_content_private` | A photo carousel sets `brand_content_toggle: true` with `privacy_level: "SELF_ONLY"`, or, on a TikTok creator connection, a video or a photo carousel does | | 400 | `validation.comment_id_invalid` | A comment ID is a bare TikTok comment ID. Hide, unhide, reply, and list replies need the `id` or `platform_id` returned by the comments endpoint. `meta.comment_id` is the value you sent | | 400 | `validation.mention_reply_unsupported` | A reply to a TikTok mention names no comment. TikTok replies only to mentions made in a comment | | 400 | `platform.tiktok.40000`, `platform.tiktok.40002`, `platform.tiktok.40011`, `platform.tiktok.40904`, `platform.tiktok.40905`, `platform.tiktok.40906`, `platform.tiktok.40907`, `platform.tiktok.40908`, `platform.tiktok.40914`, `platform.tiktok.40915` | TikTok refused a parameter or a media file. `error.message` carries TikTok's reason. Fix the field it names | | 403 | `platform.tiktok.40001`, `platform.tiktok.40118`, `platform.tiktok.40125` | The account's authorization lacks a scope the call needs. Reconnect the account to grant it | | 422 | `platform.tiktok.publish_failed` | TikTok rejected the post while processing it, or could not report its outcome. `error.message` carries TikTok's reason. Check the account in the TikTok app before retrying | | 401 | `platform.tiktok.auth` | TikTok's authorization for the account ended. Later calls return `403 account.reconnection_required` until you reconnect | | 401 | `platform.tiktok.unauthorized` | TikTok refused the call as unauthorized without saying the authorization ended. The account stays usable. Reconnect if it repeats | | 429 | `platform.tiktok.rate_limit` | TikTok's own request limit of 40 requests per minute for each account and each TikTok call. Credit rules: see [when a credit comes back](/guides/plans#when-a-credit-comes-back). Wait 1 minute, then retry | | 502 | `platform.tiktok.api_error` | TikTok returned no usable answer, or accepted a post without returning its publish ID. Check the account in the TikTok app before retrying a post | A TikTok code that this table does not list returns `502` with the code after `platform.tiktok.`. ### X, LinkedIn, YouTube, and Zalo | HTTP | Code | When it occurs | | - | - | - | | 400 | `validation.media_count` | An X post has more media items than X accepts | | 400 | `validation.media_type` | X does not accept the media type, or a LinkedIn multi-image post includes a video | | 400 | `validation.media_size` | An X media file is too large to upload in the chunks X allows | | 402 | `platform.twitter.usage_capped` | Your own X developer app is out of API credits or reached its X usage cap. Add credits or raise the spending limit in the X Developer Console. Waiting does not clear it | | 422 | `platform.linkedin.processing_failed` | LinkedIn failed to process the media | | 504 | `platform.linkedin.upload_timeout`, `platform.linkedin.processing_timeout` | A LinkedIn image upload or video processing timed out | | 403 | `platform.linkedin.access_denied` | LinkedIn refused the action for this account. Reconnecting does not change it. Read `message` | | 400 | `validation.video_url` | A YouTube video URL is not accessible | | 400 | `validation.video_size` | The size of the YouTube video file could not be determined | | 403 | `platform.youtube.insufficientPermissions` | The connection lacks a scope the call needs. Reconnect the account to grant it | | 403 | `platform.youtube.account_suspended` | YouTube suspended or closed the account or its channel. Reconnecting does not restore access. Resolve the suspension with YouTube. SocialAPI stops background sync for the account | | 422 | `platform.youtube.processing_failure` | YouTube refused to delete the comment: only comments your channel wrote can be deleted | | 422 | `platform.youtube.comments_disabled` | The video has comments turned off. Retrying does not help until the channel owner turns them back on, and the error can continue for up to 24 hours after that | ### WhatsApp WhatsApp has the most codes because of its messaging window, frequency caps, and template states. See [WhatsApp](/platforms/whatsapp). | HTTP | Code | When it occurs | | - | - | - | | 400 | `whatsapp.window_closed` | The 24-hour customer service window is closed. Send an approved template | | 429 | `whatsapp.frequency_cap` | The recipient reached the daily marketing message limit | | 429 | `whatsapp.pair_rate_limit` | Too many messages to the same recipient | | 429 | `whatsapp.throughput_exceeded` | The account's messaging throughput limit is exceeded | | 403 | `whatsapp.user_opted_out` | The recipient opted out of messages | | 400 | `whatsapp.undeliverable` | The recipient is not on WhatsApp | | 404 | `whatsapp.template_not_found` | The template does not exist for this WhatsApp Business account | | 400 | `whatsapp.template_paused` | The template is paused because of quality issues | | 400 | `validation.template_invalid` | A template failed validation. `meta.errors` lists the issues | | 400 | `validation.body_empty` | A template update set no fields | | 400 | `whatsapp.not_registered` | The number is not registered for the WhatsApp Cloud API. Register it before sending | | 400 | `whatsapp.pin_mismatch` | The number has a two-step verification PIN SocialAPI does not hold. Turn two-step verification off for the number, then retry | | 400 | `whatsapp.media_download_failed` | WhatsApp could not download the media from the URL. Check that it is publicly reachable | | 400 | `whatsapp.media_upload_failed` | WhatsApp rejected the media file | | 400 | `validation.attachment_id_unsupported` | WhatsApp does not accept `platform_attachment_id`. Send the media as a `url` or `media_id` | | 400 | `validation.caption_unsupported` | A WhatsApp audio message cannot carry a caption | | 400 | `validation.image_format_unsupported` | A WhatsApp image is not JPEG or PNG | | 400 | `validation.voice_unsupported` | `voice` was set on an attachment that is not audio | | 404 | `whatsapp.no_account` | No WhatsApp account is connected | | 502 | `whatsapp.unknown` | WhatsApp returned an error SocialAPI does not map. Retry with backoff | ### Telegram | HTTP | Code | When it occurs | | - | - | - | | 403 | `telegram.cannot_message` | The bot cannot message this chat. The person must start the bot first, or the bot was blocked or removed. There is no 24-hour window once the bot is started | | 400 | `validation.telegram_bad_request` | Telegram rejected the request. Common causes are text over 4,096 characters and an attachment URL Telegram could not fetch. `error.message` carries Telegram's description | ### Zalo | HTTP | Code | When it occurs | | - | - | - | | 422 | `platform.zalo.not_deliverable` | The message is outside the customer service messaging window, or the recipient is not eligible | | 401 | `platform.zalo.auth` | The access token expired or was revoked, or the Official Account is no longer authorized | | 202 | `platform.zalo.article_processing` | Zalo is still processing the article when the request stopped waiting. The article can still publish: check before retrying | | 429 | `platform.zalo.rate_limit` | The per-app or per-Official Account quota is used up | | 400 | `validation.platform_param` | Zalo rejected a parameter. `error.message` carries Zalo's reason | | 404 | `resource.not_found` | The Zalo account or object does not exist | | 502 | `platform.zalo.e` | Zalo returned an error SocialAPI does not map. `` is the absolute value of Zalo's numeric code (Zalo `-105` becomes `platform.zalo.e105`) | | 502 | `platform.zalo.api_error` | The request failed before Zalo answered with an error | `platform.zalo.not_deliverable` is the most common: Zalo allows a customer service message within 7 days of the person's last interaction with your Official Account (free within 48 hours), and refuses later sends with `422`. ### Bluesky | HTTP | Code | When it occurs | | - | - | - | | 400 | `platform.bluesky.auth_factor_required` | See [Connect an account](#connect-an-account) | | 403 | `platform.bluesky.dm_scope_missing` | The app password has no direct message access. Create a new app password with the option on, then reconnect. Posts, comments, and mentions keep working | | 403 | `platform.bluesky.account_takedown` | Bluesky suspended the account. Reconnecting does not restore access | | 429 | `platform.bluesky.rate_limit` | Bluesky's quota is used up. Sign-in is limited to 30 requests per 5 minutes per account | | 400 | `validation.field_invalid` | Bluesky could not resolve the handle, or an image is larger than 2 MB. `meta.field` names the field | | 400 | `validation.platform_param` | Bluesky rejected a parameter. `error.message` carries Bluesky's reason | `platform.bluesky.dm_scope_missing` affects only the DM endpoints. Treat it as a failure of one capability, not a dead connection. ### App Store Connect | HTTP | Code | When it occurs | | - | - | - | | 400 | `validation.field_required` | A connect request has no `key_id` or `private_key`, or the key can see more than 1 app and the request has no `app_id`. `meta.field` names the field. When `app_id` is missing, `meta.apps` lists up to 200 of the apps the key can see. See [Connect more than one app](/platforms/app-store-connect#connect-more-than-one-app) | | 400 | `validation.field_invalid` | `private_key` is not the contents of an App Store Connect private key file, the key can see no app with the `app_id` sent, or the key can see no app. `meta.field` names the field | | 400 | `validation.app_store_connect_rejected` | App Store Connect refused the request as invalid: a reply, a reviews page request, or the app lookup on connect. `error.message` carries App Store Connect's reason. On a reply, the interaction credit is refunded | | 401 | `platform.app_store_connect.auth` | App Store Connect rejected the API key on connect. Check the Key ID, the Issuer ID, and that the key is not revoked | | 401 | `platform.app_store_connect.unauthorized` | App Store Connect rejected the API key on a later call. If it repeats, check the key in App Store Connect, then connect the app again with a working key and the account's `brand_id` (see [Reconnect an account](/guides/connect-accounts#reconnect-an-account)) | | 403 | `platform.app_store_connect.forbidden` | App Store Connect refused the API key's role. Replies need the Customer Support, Admin, or Account Holder role | | 404 | `resource.not_found` | On a reviews list or a reply: the app or the review no longer exists, or the key cannot see it | | 429 | `platform.app_store_connect.rate_limit` | The API key's hourly request limit on App Store Connect is used up. Wait, then retry | | 502 | `platform.app_store_connect.` | App Store Connect returned an error SocialAPI does not map. `` is App Store Connect's error code, or `api_error` when it sends none. `error.message` carries App Store Connect's reason. Retry later | ### Pinterest | HTTP | Code | When it occurs | | - | - | - | | 400 | `board.name_required` | A board has no name | | 400 | `board.privacy_invalid` | A board's privacy can be changed only to `PUBLIC` or `SECRET` | | 400 | `board.update_empty` | A board update set no field | | 403 | `platform.pinterest.403` | Pinterest refused the request for this account or board. `error.message` carries Pinterest's reason. Reconnecting does not clear it | | 404 | `platform.pinterest.404` | Pinterest reports that the board or Pin does not exist | ## Platform errors When a platform refuses a call, the code is `platform..`, where `` is the platform slug. SocialAPI forwards the platform's HTTP status when it maps one, and returns `502` otherwise. | HTTP | Code | When it occurs | | - | - | - | | 401 | `platform..auth` | The platform rejected the saved token. Reconnect the account | | 403 | `platform.twitter.insufficient_scope` | X refused the call because the account's access lacks a scope. `meta.scope` names it for a media upload. The account does not become `token_expired`. Reconnect the account to grant it | | 401 | `platform..unauthorized` | The platform answered 401 without saying the token is bad. A healthy account keeps working. Reconnect if it persists | | 429 | `platform..rate_limit` | The platform's own limit. See [Rate limits](#rate-limits) | | 422 | `platform..publish_failed` | A publish failed with an error SocialAPI does not map. Appears in [Delivery failures](#delivery-failures) | | 502 | `platform..api_error` | A failure reaching the platform, or a response with no usable error | | 502 | `platform..` | The platform returned its own error code. The status is `502` unless the code has its own row above, such as `404 platform.facebook.100` | Branch on the prefix and suffix: `platform.` with `.auth` means reconnect, with `.rate_limit` means back off. ## System errors | HTTP | Code | When it occurs | | - | - | - | | 500 | `system.internal` | The request failed on SocialAPI's side. Retry idempotent requests. If it persists, send the `request_id` to support | | 503 | `system.internal` | The host of a media URL in the request could not be looked up at that moment. `meta.field` is `media`. Nothing was created or changed. Retry the request | ## Handling errors Read `error.code` from the response, then branch. Reconnect on a dead token, back off on a throttle, show the field on a validation failure, and log `request_id` for anything else. ```typescript theme={null} const resp = await fetch("https://api.social-api.ai/v1/accounts", { headers: { Authorization: `Bearer ${process.env.SOCAPI_KEY}` }, }); if (!resp.ok) { const { error, request_id } = await resp.json(); const { code, message, meta } = error; if (code === "account.reconnection_required" || (code.startsWith("platform.") && code.endsWith(".auth"))) { return reconnect(); // POST /v1/accounts/connect } if (resp.status === 429 && meta?.retry_after_seconds) { await sleep(meta.retry_after_seconds * 1000); return retry(); } if (code.startsWith("validation.")) { return showFieldError(meta?.field, message); } throw new Error(`${code}: ${message} (request_id ${request_id})`); } ``` For a `429` without `meta.retry_after_seconds` (a platform limit), wait and retry with backoff. A plan allowance such as `validation.post_limit_exhausted` does not clear until the billing period ends: check `GET /v1/usage`. ## Related * [Debugging](/guides/debugging) for `X-Request-ID` and what to send to support * [Rate limits](/guides/rate-limits) for every 429 code, its window, and how to retry * [Plans and usage](/guides/plans) for allowances and past-due accounts * [Track post delivery](/guides/delivery) for target errors on a post * [Scoped API keys](/guides/scoped-keys) for `auth.insufficient_scope` * [Authentication](/guides/authentication) for API keys and OAuth 2.1 access tokens # IDs Source: https://docs.social-api.ai/guides/ids Look up the ID format of every SocialAPI object, learn which IDs stay stable, and which create requests are not idempotent. Every object SocialAPI returns has an `id` string. Treat every ID as an opaque string: store it as text, send it back unchanged, and never parse it or depend on its length, characters, or format. The formats below tell you how to recognize an ID, not how to read one. ## ID formats | Object | ID format | Example | | - | - | - | | Connected account | `acc_` prefix | `acc_01HZ9X3Q4R5M6N7P8V2K0W1J` | | Facebook Page or other page-level destination of an account | `sapi_page_` prefix | `sapi_page_01HZ9X3Q4R5M6N7P8V2K0W1J` | | Pinterest board | `sapi_board_` prefix | `sapi_board_01HZ9X3Q4R5M6N7P8V2K0W1J` | | Login (the `login_id` of a Facebook, Google, or LinkedIn connection) | `cred_` prefix | `cred_01HZ9X3Q4R5M6N7P8V2K0W1J` | | Automation | `aut_` prefix | `aut_01HZ9X3Q4R5M6N7P8V2K0W1J` | | Automation Firing | `afr_` prefix | `afr_01HZ9X3Q4R5M6N7P8V2K0W1J` | | Comment | `sapi_cmt_` prefix | `sapi_cmt_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I` | | Review | `sapi_rev_` prefix | `sapi_rev_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I` | | Direct message, as the `id` of a `dm.*` webhook event and in `message_id` and `message_ids` of a send response | `sapi_dm_` prefix | `sapi_dm_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I` | | Mention | `sapi_mnt_` prefix | `sapi_mnt_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I` | | Message, as the `id` of an item in `GET /v1/inbox/conversations/{id}/messages` | UUID, no prefix | `7c9e6679-7425-40de-944b-e07fc1f90ae7` | | Post, brand, invite, API key, webhook endpoint, webhook delivery, media file, conversation, event, export, announcement | UUID, no prefix | `7c9e6679-7425-40de-944b-e07fc1f90ae7` | The `sapi_cmt_`, `sapi_rev_`, `sapi_dm_`, and `sapi_mnt_` IDs are interaction IDs: the `id` of each item returned by the comment, review, and mention endpoints, of each direct message in a webhook event, and of each message a send request returns. A message in a conversation's message list has a UUID `id` instead, and its `platform_id` is the platform's own ID. The two are different values for the same message. An API key's secret starts with `sapi_key_`, which is not an ID: the `id` of a key is a UUID, and the secret is shown once when you create the key. Fields named `platform_id` hold the platform's own ID for the same object, such as an Instagram comment ID. They are different values from `id`. ## What each endpoint accepts * **Connected account:** every endpoint that takes an account uses `acc_` IDs. * **Post targets:** `page_id` and `board_id` accept the SocialAPI ID (`sapi_page_`, `sapi_board_`) or the platform's own ID. * **Comment routes:** `:postId` takes the platform's post ID. `:commentId` and `comment_id` take a `sapi_cmt_` ID or the platform's comment ID. * **Review routes:** `:id` takes a `sapi_rev_` ID or the platform's review ID. * **Conversation routes:** `:id` takes the conversation `id` returned by `GET /v1/inbox/conversations`. It is a UUID, not a `sapi_dm_` ID. * **Mention replies:** `media_id` takes a `sapi_mnt_` ID or the platform's media ID. * **Read a post:** `GET /v1/posts/{pid}` takes the SocialAPI post ID or a platform post ID. A platform post ID that matches more than one post returns `409 ambiguous_post_id`: use the SocialAPI post ID. * **Post metrics:** `GET /v1/posts/{pid}/metrics` takes the SocialAPI post ID or a platform post ID, with the same `409 ambiguous_post_id`. A SocialAPI interaction ID of the wrong type returns `400 validation.id_invalid`: for example a `sapi_rev_` ID in a comment route. An interaction ID created for a different platform than the account returns `400 validation.id_platform_mismatch`. ## Use IDs in requests List commented posts, then reply to one comment using its `id`: ```bash theme={null} curl -X POST "https://api.social-api.ai/v1/inbox/comments/17895695668004550" \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "comment_id": "sapi_cmt_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "text": "Thank you!" }' ``` Response (HTTP 200): ```json theme={null} { "success": true, "comment_id": "sapi_cmt_7Q2KDWZ9SFN4RAHB6GXEM3YLTPV1C5UJ" } ``` `comment_id` in the response is the ID of your new reply. In this request, `17895695668004550` is the platform's post ID. Hide a comment, with the same `sapi_cmt_` ID in the path: ```bash theme={null} curl -X POST "https://api.social-api.ai/v1/inbox/comments/17895695668004550/sapi_cmt_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I/hide" \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{"account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J"}' ``` See [Comments](/guides/comments), [Reviews](/guides/reviews), and [Mentions](/guides/mentions) for every route that takes an interaction ID. ## What stays stable * **Interaction IDs:** the same comment, review, or mention always has the same `id`, in every response and every webhook event. Store it and join on it. For a direct message, the `sapi_dm_` ID is stable but the message list does not return it. To match a listed message to a webhook event, compare `platform_id`. * **Connected accounts:** an `acc_` ID stays the same for the life of the connection. Reconnecting the same platform account to your SocialAPI account keeps its `acc_` ID. Disconnecting an account with `DELETE /v1/accounts/{id}` deletes it, and connecting it again creates a new `acc_` ID. * **Every other object:** an ID never changes for the life of the object. Store IDs as text, not as a typed `uuid` column: formats differ per object type, so never assume one format across objects. ## Create requests are not idempotent No create endpoint accepts an idempotency key. When a request times out and you repeat it, check first whether the first request succeeded. | Request | What a repeat does | | - | - | | `POST /v1/brands` | Creates another brand. Brand names are not unique | | `POST /v1/keys` | Creates another API key | | `POST /v1/webhooks` | Creates another webhook endpoint, even for the same URL | | `POST /v1/automations` | Creates another Automation, up to the limit of 50 Automations per account | | `POST /v1/invites` | Returns `409 resource.conflict` while an active invite exists for the same brand and platform | | `POST /v1/posts` | Returns `409 resource.conflict` when the same account already has a post with the same title and text that was created in the last 24 hours and is now `published`. Send `skip_duplicate_check: true` to publish the same content again. Drafts, scheduled posts, and posts still `publishing` are not compared | | Replies and direct messages | Send another reply or message and cost another interaction credit | After a timeout, persist the ID from the first response when you have it. Otherwise list the object (`GET /v1/brands`, `GET /v1/posts`) and look for it before you create another. For webhook endpoints, `X-SocialAPI-Delivery` is the same value on every retry of one delivery. Use it to ignore a duplicate. See [Webhooks](/guides/webhooks). ## Related * [Pagination](/guides/pagination): cursors are opaque strings too * [Comments](/guides/comments): routes that take `sapi_cmt_` IDs * [Reviews](/guides/reviews): routes that take `sapi_rev_` IDs * [Track post delivery](/guides/delivery): post and target fields in post responses * [Webhooks](/guides/webhooks): `X-SocialAPI-Delivery` and duplicate events # Unified inbox Source: https://docs.social-api.ai/guides/inbox Read and answer comments, direct messages, reviews, and mentions from every connected platform through one inbox API with one set of IDs and cursors. The unified inbox is SocialAPI's REST API for reading and answering what people send to your connected accounts: comments, direct messages (DMs), reviews, and mentions. Each area has its own endpoints. All of them use the same API key, the same [IDs](/guides/ids), the same cursor [pagination](/guides/pagination), and the same `501 resource.not_supported` answer when a platform lacks a feature. Replies and DMs cost interaction credits, and reads cost nothing. Which platforms support which action is in the [capability matrix](/platforms/overview). An account needs the matching [permission](/guides/permissions) for each area: `comments.read` and `comments.write`, `messages.read` and `messages.write`, or `reviews.read` and `reviews.write`. A call without it returns `403 account.permission_missing` (the lists of stored comments, conversations, and messages and `GET /v1/inbox/reviews` are the exceptions: see [Account permissions](/guides/permissions#what-an-account-can-do-right-now)), and the account's events for that area do not arrive. ## The four areas | Area | What it covers | Guide | | - | - | - | | Comments | Comments and replies on your posts: list, reply, private reply, hide, delete, like | [Comments](/guides/comments) | | Direct messages | Conversations and messages: list, send text, attachments, and buttons, react, mark as read | [Direct messages](/guides/direct-messages) | | Reviews | Reviews of a business location or an app, and the business's public reply | [Reviews](/guides/reviews) | | Mentions | Posts and comments that tag your account, and replies to them | [Mentions](/guides/mentions) | Answer Instagram and Facebook comments and DMs that match keywords without running a webhook handler by using an [Automation](/guides/automations). ## The interaction shape An `Interaction` is the one object that webhook events and `GET /v1/accounts/{id}/mentions` use for a comment, DM, or mention. This is one item of that mentions list: ```json theme={null} { "id": "sapi_mnt_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "platform": "instagram", "type": "mention", "author": { "username": "janesmith" }, "content": { "text": "Loving the new range from @acmecorp", "media": [{ "url": "https://scontent.cdninstagram.com/v/t51/range.jpg", "type": "image" }] }, "metadata": { "media_id": "17895695668004550", "permalink": "https://www.instagram.com/p/C8xY2AbC/", "media_type": "IMAGE", "like_count": 12, "comments_count": 3 }, "received_at": "2026-10-13T14:35:00Z", "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" } ``` `id` starts with `sapi_cmt_`, `sapi_dm_`, or `sapi_mnt_` by type. Every field is in [Interaction payload](/guides/webhook-events#interaction-payload). The list endpoints of the other areas return their own item shapes, described in each guide: comments with a `capabilities` object, conversations and messages, and reviews with a `reply`. IDs have one format everywhere: see [IDs](/guides/ids). ## Receive new items SocialAPI sends new inbound items to your server as [webhook events](/guides/webhook-events) when the platform reports them to SocialAPI. | Area | Events | | - | - | | Comments | `comment.received` | | Direct messages | `dm.received`, `dm.sent`, `dm.deleted`, `dm.referral`, `dm.postback`, and `dm.status.sent`, `dm.status.delivered`, `dm.status.read`, `dm.status.failed` | | Reviews | `review.received`, `review.updated` | | Mentions | `mention.received` | Which platform emits which event is stated per event on [Webhook events](/guides/webhook-events). X, YouTube, Bluesky, Pinterest, LinkedIn, and App Store Connect send nothing to SocialAPI. Where they support comments, DMs, mentions, or reviews, read them with the list endpoints. [Register an endpoint](/guides/webhooks#register-an-endpoint) once to receive the events of the other platforms. ## Costs Interaction credits count the replies and DMs you send. A plan with an interaction allowance returns `429 validation.interaction_limit_exhausted` when it is used up. * **1 credit:** a public reply to a comment, a reply to a review, a reply to a mention, and one DM request, however many messages it sends. * **No credit:** every read, private replies, hiding, liking, and deleting a comment, message reactions, and a manual sync. [Plans and usage](/guides/plans#what-counts-as-a-post-or-interaction) is the home of these rules. ## Fetch an inbound attachment `GET /v1/inbox/media/{id}` returns the bytes of an attachment that arrived with a WhatsApp or Telegram message. On every other platform, the attachment URL on a message is the platform's own URL: you fetch it directly, and it expires when the platform says. WhatsApp and Telegram files can be downloaded only with the connected account's own credentials, which your API key cannot present. For those two platforms, `attachment_url` on the message and `content.media[].url` on the `dm.received` event are a SocialAPI URL of the form `https://api.social-api.ai/v1/inbox/media/{id}`. SocialAPI starts fetching the file when the message arrives. It stays available for 30 days, then returns `410`. ```bash theme={null} curl "https://api.social-api.ai/v1/inbox/media/7f3c1e2a-9b4d-4e5f-8a6b-1c2d3e4f5a6b" \ -H "Authorization: Bearer $SOCAPI_KEY" \ -o attachment ``` | Status | Meaning | | - | - | | `200` | The attachment bytes, with `Content-Type` set to the original media type | | `404` | The media ID is unknown, or it belongs to another SocialAPI account | | `410` | The file is gone for good: it expired, or the platform reported the source unrecoverable. Stop retrying | | `503` | Too many requests are waiting for files. The response carries `Retry-After: 1`. Retry after 1 second | | `504` | SocialAPI is still fetching the file. Send the request again until you get `200` or `410`: this is not a failure | A request for a file that is still being fetched waits up to 15 seconds for it before it returns `504`. `410` and `504` mean opposite things: `410` means stop, `504` means keep retrying. A file that cannot be downloaded at all, such as a Telegram file over 20 MB, returns `410`. ## Refresh the inbox manually Use `POST /v1/inbox/sync` to refresh conversations and messages on demand, for example to load a thread before you open it. | Method | Path | Description | | - | - | - | | `POST` | `/v1/inbox/sync` | Start a sync for one account's conversation list, or for one thread | | `GET` | `/v1/inbox/sync` | Read the sync state of a target | With only `account_id`, the sync refreshes that account's conversation list, then loads the messages of up to 20 of its most recent conversations that were never loaded. Add `conversation_id` to refresh that one thread's messages instead. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/inbox/sync \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }' ``` Response (HTTP 202): ```json theme={null} { "data": { "state": "syncing", "started_at": "2026-10-13T14:30:00Z", "finished_at": null, "last_synced_at": null, "error": "", "synced": 0 } } ``` | Field | Description | | - | - | | `account_id` | Request body and `GET` query parameter. Required. The connected account to sync | | `conversation_id` | Request body and `GET` query parameter. Optional. The thread to sync instead of the conversation list | | `state` | `idle`, `syncing`, or `failed` | | `started_at` | When the current or most recent job started | | `finished_at` | When the job finished. `null` while `state` is `syncing` | | `last_synced_at` | When this target last completed a sync. `null` in the `POST` response: read it from `GET` | | `error` | The reason the last job failed. Empty otherwise | | `synced` | Conversations added for a list sync, or messages added for a thread sync. For display only | To wait for the result, poll `GET /v1/inbox/sync` with the same `account_id` and `conversation_id` until `state` is no longer `syncing`, then read the conversation or message list again: ```bash theme={null} curl "https://api.social-api.ai/v1/inbox/sync?account_id=acc_01HZ9X3Q4R5M6N7P8V2K0W1J" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` A `POST` for a target that is already syncing returns that running job and starts no second one. Each target accepts 1 sync you start every 5 minutes. A second attempt inside that time returns `429 inbox.sync_rate_limited` with a `Retry-After` header holding the seconds to wait, and `error.meta.retry_after_seconds` repeats it. Reads of the inbox never use this allowance. `GET /v1/inbox/conversations` and `GET /v1/inbox/conversations/{id}/messages` also return `sync_state` and `last_synced_at`, so a client that already polls them does not need `GET /v1/inbox/sync`. An unknown `account_id` or `conversation_id` returns `404`, and a missing `account_id` returns `400 validation.field_required`. ## Scopes A scoped key needs the scope that each guide lists for an operation: `inbox:read`, `inbox:write`, `dms:read`, or `dms:send`. Starting or reading a sync needs `dms:read`. [Scoped API keys](/guides/scoped-keys#scopes) lists what each scope grants. ## Related * [Comments](/guides/comments): list, reply to, moderate, and privately answer comments. * [Direct messages](/guides/direct-messages): conversations, messages, attachments, and buttons. * [Reviews](/guides/reviews): read Google Business Profile and App Store Connect reviews and reply. * [Mentions](/guides/mentions): list the posts that tag your account and reply. * [Webhook events](/guides/webhook-events): every inbox event and its payload. * [Account permissions](/guides/permissions): limit what a connected account can read and send. * [Plans and usage](/guides/plans): interaction credits and allowances. # Upload media Source: https://docs.social-api.ai/guides/media Upload images and videos to your SocialAPI media library with one request or a presigned URL, or attach a public URL or platform attachment ID to a post. Media in SocialAPI is the images and videos you attach to a post. A post carries an ordered `media` array, and each item names where the file comes from: your media library, a public URL, or a media ID you created on the platform. Order decides carousel sequence. Uploading, verifying, and listing media cost no credits. A credit is charged when a post is published or scheduled. Storage counts against your plan: Free has 100 MB, and every paid plan is unlimited. See [Plans and usage](/guides/plans#plan-allowances). ## Endpoints | Method | Path | Description | | - | - | - | | `POST` | `/v1/media/upload` | Upload a file of up to 50 MB in one request | | `GET` | `/v1/media/upload-url` | Get a signed URL to upload a larger file | | `POST` | `/v1/media/{id}/verify` | Confirm a signed-URL upload | | `GET` | `/v1/media` | List the files in your library | | `DELETE` | `/v1/media/{id}` | Delete a file | | `GET` | `/v1/media/storage` | Read your storage use and quota | ## Media sources Each item of `media` has a `source_type` and a `source`: | `source_type` | `source` is | Use it when | | - | - | - | | `media_id` | The ID of a file in your library | You uploaded the file to SocialAPI | | `url` | A public `http` or `https` URL | The file is already hosted and you do not want to upload it | | `platform_attachment_id` | A media ID from the platform's own API | You uploaded the file to the platform yourself. Add `type`: `image`, `video`, `audio`, or `file` | ```json theme={null} { "text": "Behind the scenes from our studio shoot.", "media": [ { "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }, { "source_type": "url", "source": "https://cdn.example.com/photo.jpg" } ], "platform_data": { "instagram": { "content_type": "carousel" } }, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }] } ``` `media` is also accepted on a target, where it replaces the post's list for that target. A target without `media` uses the post's list. The older `media_ids` array still works: `["abc"]` equals `[{"source_type": "media_id", "source": "abc"}]`. When you send both, `media` wins. Responses return `media_ids` next to `media`, listing the `media_id` items only. ## Choose an upload path | | Server-side upload | Signed URL upload | | - | - | - | | Requests | 1 | 3 | | Largest file | 50 MB | No SocialAPI limit. Your storage quota and the platform's limits apply | | Use it for | Small files, and clients that cannot call another host (AI sandboxes) | Large files, especially video | ## Upload with one request Send the file as multipart form data in the `file` field: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/media/upload \ -H "Authorization: Bearer $SOCAPI_KEY" \ -F "file=@photo.jpg" ``` Response (HTTP 201): ```json theme={null} { "media_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479" } ``` The file is `ready` at once. A request without `file` returns `400 validation.field_required`. A file over 50 MB returns `400 validation.field_invalid`. ## Upload with a signed URL **1. Request a signed URL** Send the file's MIME type as `media_type` and its name as `filename`. Both are required. ```bash theme={null} curl "https://api.social-api.ai/v1/media/upload-url?media_type=video/mp4&filename=reel.mp4" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "media_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "upload_url": "https://storage.example.com/media/reel.mp4?X-Amz-Signature=abc123", "expires_at": "2026-10-13T09:15:00Z" } ``` The response has no public URL: the file becomes usable through its `media_id`. `upload_url` stops working at `expires_at`, 15 minutes after the request. The media is `pending`. **2. Upload the bytes** Send the file with `PUT` to `upload_url`. The `Content-Type` header must equal the `media_type` from step 1. ```bash theme={null} curl -X PUT "https://storage.example.com/media/reel.mp4?X-Amz-Signature=abc123" \ -H "Content-Type: video/mp4" \ --data-binary @reel.mp4 ``` **3. Verify the upload** Verification is required. It confirms the file arrived, records its size, and makes the media `ready`. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/media/f47ac10b-58cc-4372-a567-0e02b2c3d479/verify \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "success": true } ``` If the file is not at the upload URL when you verify, the media becomes `failed` and the request returns `404 media.not_found`. If the file would take you over your storage quota, SocialAPI deletes it, the media becomes `failed`, and the request returns `413 billing.storage_quota`. Media has 3 statuses: `pending` (created, bytes not confirmed), `ready` (usable in posts), and `failed`. Only `ready` media publishes. A post that names media that is not `ready` returns `400 publishing.validation_failed`, as [Track post delivery](/guides/delivery#verify-media-before-you-publish) describes. ## Attach media to a post Pass `media` on `POST /v1/posts`. One item is an image or a video, and 2 or more items are a carousel, within each platform's limits. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Behind the scenes from our studio shoot.", "publish_now": true, "media": [ { "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" } ], "platform_data": { "instagram": { "content_type": "feed" } }, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }] }' ``` The formats, sizes, and counts each platform accepts are on its publishing page. `GET /v1/posts/validate` returns them as data. See [Publish a post](/guides/publishing#validate-before-you-publish). ## Publish from a public URL A `url` source is not stored in your library and uses no storage quota. ```json theme={null} { "media": [ { "source_type": "url", "source": "https://cdn.example.com/photo.jpg" } ] } ``` * **Reachable:** the URL must be a public `http` or `https` address when you create the post and again when the post publishes. A URL that resolves to a private, loopback, or link-local address returns `400 validation.field_invalid`. A post scheduled for later fails when the URL stops working before it publishes. * **Typed:** SocialAPI reads the file type from the URL. When it cannot, the request returns `422 publishing.media.untyped_url`. Use a URL with an extension such as `.jpg` or `.mp4`, or upload the file. * **Lookup failure:** when the host cannot be looked up for a moment, the request returns `503 system.internal`. Send it again. For a post scheduled far ahead, upload the file and use a `media_id`: a link that expires or changes before the post publishes fails the post. ## Publish a platform attachment ID If you uploaded media through a platform's own API, pass its ID and SocialAPI publishes it without a second upload. `type` is required. ```json theme={null} { "media": [ { "source_type": "platform_attachment_id", "source": "18234567890123456", "type": "image" } ] } ``` Instagram, Threads, LinkedIn, and X accept `platform_attachment_id`. Any other platform returns `422 publishing.media.unsupported_source` for that target. A missing `type` returns `422 publishing.media.missing_type`. | Platform | Mix an attachment ID with other media | | - | - | | LinkedIn, X | Yes. Combine it freely with `url` and `media_id` items | | Instagram, Threads | No. The attachment ID must be the only item in `media`, because it is one finished photo, video, or carousel. More items return `422 publishing.media.mixing_unsupported` | These errors apply per target: the post still publishes to its other targets. To build a mixed carousel on Instagram or Threads, send `url` or `media_id` items. ## Manage your library `GET /v1/media` lists your `ready` files, newest first. Each item has `id`, `filename`, `content_type`, `size_bytes`, `status`, `created_at`, and a `url` for previewing the file. The `url` stops working after 2 hours. `limit` is 1 to 100 and defaults to 50. `cursor` is the `next_cursor` of the previous page. See [Pagination](/guides/pagination). ```bash theme={null} curl "https://api.social-api.ai/v1/media?limit=50" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` `DELETE /v1/media/{id}` deletes a file and returns HTTP 204. SocialAPI does not check whether a post uses the file, so a scheduled post that uses a deleted file fails when it publishes. A file that does not exist returns `404 media.not_found`. `GET /v1/media/storage` returns your storage use: ```bash theme={null} curl https://api.social-api.ai/v1/media/storage \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "used_bytes": 52428800, "limit_bytes": 104857600, "count": 12 } ``` `limit_bytes` is `-1` when your plan has no limit. `GET /v1/media/storage`, `GET /v1/media/upload-url`, `POST /v1/media/upload`, and `POST /v1/media/{id}/verify` also return the `X-Storage-Used` and `X-Storage-Limit` headers. Only `ready` files count. ## Limits | Limit | Value | | - | - | | Server-side upload | 50 MB per file. Larger returns `400 validation.field_invalid` | | Signed upload URL | Valid for 15 minutes | | Storage, Free plan | 100 MB. An upload over it returns `413 billing.storage_quota` | | Storage, paid plans | Unlimited | | Library list | 100 files per page | | Library preview `url` | Valid for 2 hours | SocialAPI reads the duration of a video when the upload completes. You do not send it. ## Scopes | Scope | Grants | | - | - | | `media:read` | List the library | | `media:write` | Upload, get a signed URL, verify, and delete media | `GET /v1/media/storage` needs no scope: any valid API key can call it. ## Use from an AI agent | Tool | What it does | | - | - | | `social_api_get_media_upload_url` | Gets a signed URL and a `media_id` | | `social_api_verify_media_upload` | Confirms the upload | | `social_api_list_media` | Lists the library | | `social_api_delete_media` | Deletes a file | | `social_api_get_storage_usage` | Reads storage use and quota | An agent uploads with the signed URL. The server-side upload has no tool. See [MCP tools](/integrations/tools). ## Related * [Publish a post](/guides/publishing): the `media` field and validation * [Track post delivery](/guides/delivery): media status and delivery errors * [Plans and usage](/guides/plans): storage allowance for each plan * [Pagination](/guides/pagination): cursor lists * [Errors](/guides/errors): every media error code # Mentions Source: https://docs.social-api.ai/guides/mentions List the posts and comments that mention or tag a connected account, and reply to a mention as a public comment, through one mentions API. A mention is a post or comment on a platform that tags or @mentions your connected account. SocialAPI's mentions API lists them for 1 account and replies to a mention as a public comment, including on a post that someone else owns, where the normal comment reply is not allowed. A reply costs 1 interaction credit, and listing costs nothing. What a platform counts as a mention differs: see [What the list returns](#what-the-list-returns). ## Endpoints | Method | Path | Description | | - | - | - | | `GET` | `/v1/accounts/{id}/mentions` | List the mentions of 1 connected account, newest first | | `POST` | `/v1/accounts/{id}/mentions/reply` | Reply to a mention as a public comment | `{id}` is the connected account's `acc_` ID. Mentions belong to 1 account: there is no list across accounts. ## Your first mention reply List the mentions of an account: ```bash theme={null} curl "https://api.social-api.ai/v1/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/mentions?limit=20" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "id": "sapi_mnt_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "platform": "instagram", "type": "mention", "author": { "username": "janesmith" }, "content": { "text": "Loving the new range from @acmecorp" }, "metadata": { "media_id": "17895695668004550", "permalink": "https://www.instagram.com/p/C8xY2AbC/", "media_type": "IMAGE", "like_count": 12, "comments_count": 3 }, "received_at": "2026-10-13T14:35:00Z", "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" } ], "count": 1 } ``` Reply to it. `media_id` takes the mention's `sapi_mnt_` ID or the platform's media ID: ```bash theme={null} curl -X POST "https://api.social-api.ai/v1/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/mentions/reply" \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "media_id": "sapi_mnt_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "text": "Thanks for the shout-out!" }' ``` Response (HTTP 200): ```json theme={null} { "success": true, "comment_id": "sapi_cmt_TT974YVQH4RAHNN2HY2DJSNY9111D7LMTP1X" } ``` The reply is a public comment. `comment_id` is the new comment's `sapi_cmt_` ID. ## List mentions Each item is an `Interaction`: see [The Interaction shape](/guides/inbox#the-interaction-shape) and [Interaction payload](/guides/webhook-events#interaction-payload) for every field. | Parameter | Description | | - | - | | `since` | Only mentions created after this time, in RFC 3339 format | | `limit` | Mentions per page. The default is 20. A value above 100 is treated as 100. A value below 1 or one that is not an integer returns `400 validation.field_invalid` | | `cursor` | The `next_cursor` of the previous page | A malformed `since`, `limit`, or `cursor` returns `400 validation.field_invalid`. The response holds `data`, `count` (the number of items in `data`), and `next_cursor` when more mentions exist. Paging is in [Pagination](/guides/pagination#paginated-endpoints). Each call that returns at least one item also sends a `mention.received` event: see [Events](#events). ## What the list returns Each platform exposes a different slice of what people write about your account, so the list holds different things. * **Instagram:** only media in which another account tagged yours. An @mention in a caption or in a comment is not in the list. It arrives as a [`mention.received`](/guides/webhook-events#mention-received) event, with `metadata.mention_type` set to `caption` or `comment`. * **Facebook:** the posts that tag the Page. A mention inside a comment arrives as a `mention.received` event. * **Threads:** public posts that tag the account. * **TikTok:** public posts that mention the account in the caption, and comments that mention it. TikTok Business Accounts only, and each mention appears 24 to 48 hours after it is made. * **X and Bluesky:** posts that mention the account. * **LinkedIn:** posts and comments that mention the company Page, from the last 60 days. Company Pages (`connection_type: "organization"`) only: a member profile returns `501 resource.not_supported`. Other platforms return `501 resource.not_supported`. Each platform's page has the rules and limits: [Instagram](/platforms/instagram#mentions), [Facebook](/platforms/facebook#mentions), [Threads](/platforms/threads#mentions), [TikTok](/platforms/tiktok#mentions), [X](/platforms/twitter), [Bluesky](/platforms/bluesky), and [LinkedIn](/platforms/linkedin). The [capability matrix](/platforms/overview) compares them. ## Reply to a mention `POST /v1/accounts/{id}/mentions/reply` posts a public comment as the connected account. | Field | Description | | - | - | | `media_id` | Required. The post the mention is on: the `sapi_mnt_` ID from the list, or the platform's media ID | | `text` | Required. The reply text | | `comment_id` | Set only when the mention is inside a comment. Omit it to reply to a mention in the post's caption | A missing `media_id` or `text` returns `400 validation.field_required`. A reply costs 1 interaction credit. Replying works on Instagram, Facebook, Threads, Bluesky, LinkedIn Pages, and TikTok (mentions made in a comment only: a caption mention returns `400 validation.mention_reply_unsupported`). X lists mentions but cannot reply to them: the call returns `501 resource.not_supported`. Reply to a comment on your own post with [Comments](/guides/comments) instead. ## Limits | Limit | Value | | - | - | | Mentions per page | Default 20, up to 100 | | Cost of a reply | 1 interaction credit | | Cost of a list | None | ## Errors and debugging Check these in order when a mention call fails or returns nothing: 1. **`401`, `403 auth.insufficient_scope`.** The key is invalid, or a scoped key lacks the scope in [Scopes](#scopes). 2. **`404 account.not_found`, `403 account.reconnection_required`.** The `acc_` ID is not yours, or the account needs [reconnecting](/guides/connect-accounts#reconnect-an-account). 3. **`403 account.permission_missing`.** The account lacks the [permission](/guides/permissions) the call needs: `comments.read` to list mentions, and `comments.write` to reply to one. `error.meta.permission` names it. 4. **`400 validation.field_required`, `400 validation.field_invalid`.** A required field is missing, or a query parameter is wrong. 5. **`501 resource.not_supported`.** The platform has no mentions, or cannot reply to them. 6. **`429 validation.interaction_limit_exhausted`.** The plan's interaction allowance is used up. 7. **An empty list.** The platform does not list that kind of mention. On Instagram, subscribe to `mention.received` for caption and comment mentions. On Threads, the list can omit mentions until Meta grants the scope for every account: see [Threads mentions](/platforms/threads#mentions). The full catalog is in [Errors](/guides/errors#inbox-comments-messages-and-reviews). ## Scopes | Scope | Operations | | - | - | | `inbox:read` | List mentions | | `inbox:write` | Reply to a mention | ## Events SocialAPI sends [`mention.received`](/guides/webhook-events#mention-received) when Instagram, Facebook, or Threads reports that your account was mentioned. The event and the list share the same `id` on Threads, so you can match them. An account without the `comments.read` [permission](/guides/permissions) receives no `mention.received` events from the platform. `GET /v1/accounts/{id}/mentions` also sends a `mention.received` event on every call that returns at least one item, on every platform that lists mentions. Its `data` is an array of the listed items and it has no `raw_payload`. A call that returns no items sends no event. See [Mentions you list](/guides/webhook-events#mentions-you-list). ## Use from an AI agent | Tool | What it does | | - | - | | `social_api_get_mentions` | List the posts and comments that mention or tag a connected account | | `social_api_reply_to_mention` | Reply publicly to a mention, including on a post the account does not own | See [MCP tools](/integrations/tools#mentions). ## Related * [Comments](/guides/comments): answer the comments on your own posts. * [Webhook events](/guides/webhook-events#mentions): the `mention.received` payload per platform. * [Unified inbox](/guides/inbox): the Interaction shape and the other inbox areas. * [Account permissions](/guides/permissions): the permissions an account needs to list and answer mentions. * [Platform support](/platforms/overview): which platform lists and answers mentions. * [Plans and usage](/guides/plans#what-counts-as-a-post-or-interaction): what costs an interaction credit. # Pagination Source: https://docs.social-api.ai/guides/pagination Page through SocialAPI list endpoints with limit and cursor, and look up the default page size, maximum, and next-cursor field of every paginated endpoint. SocialAPI paginates large lists with opaque cursors. Send `limit` to set the page size, read the next cursor from the response, and send it back as `cursor` until the response has no next cursor. Never build, parse, or change a cursor: pass back exactly the value the API returned. ## Page through a list Request the first page without a cursor: ```bash theme={null} curl "https://api.social-api.ai/v1/posts?status=draft&limit=2" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "text": "Spring sale starts Friday: https://example.com/spring", "status": "draft", "hidden": false, "retry_count": 0, "created_at": "2026-10-01T08:00:00Z", "updated_at": "2026-10-01T08:00:00Z" }, { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "text": "New arrivals are in the shop.", "status": "draft", "hidden": false, "retry_count": 0, "created_at": "2026-10-02T09:30:00Z", "updated_at": "2026-10-02T09:30:00Z" } ], "pagination": { "limit": 2, "has_more": true, "next_cursor": "7Kq2xVn9RzdE4mLpWb8Ya1Tc" } } ``` Request the next page with the same filters and the returned cursor: ```bash theme={null} curl "https://api.social-api.ai/v1/posts?status=draft&limit=2&cursor=7Kq2xVn9RzdE4mLpWb8Ya1Tc" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` The last page has no next cursor: the field is absent or an empty string, depending on the endpoint. Stop when the next cursor is absent or empty. On endpoints that return `pagination.has_more`, it is `false` on the last page. A cursor records a position, not the filters or sort order. Send the same filters, `sort`, and `limit` on every page of one list. A cursor reused with different filters continues from the same position under the new filters. ## Fetch every page ```javascript theme={null} async function listAll(path, params) { const items = []; let cursor = ""; do { const query = new URLSearchParams(params); if (cursor) query.set("cursor", cursor); const resp = await fetch(`https://api.social-api.ai${path}?${query}`, { headers: { Authorization: `Bearer ${process.env.SOCAPI_KEY}` }, }); const body = await resp.json(); items.push(...body.data); cursor = body.pagination.next_cursor || ""; } while (cursor); return items; } const drafts = await listAll("/v1/posts", { status: "draft", limit: "100" }); ``` Read the next cursor from the field listed for the endpoint in the table below: it is `pagination.next_cursor` on some endpoints and a top-level `next_cursor` on others. ## Parameters | Parameter | Type | Description | | - | - | - | | `limit` | integer | Items per page. The default and the maximum differ per endpoint. See the table below | | `cursor` | string | The next cursor from the previous response. Omit it for the first page | ## Paginated endpoints | Endpoint | Default `limit` | Maximum `limit` | Next cursor field | `limit` of 0 or less | | - | - | - | - | - | | `GET /v1/posts` | 25 | 100 | `pagination.next_cursor` | Uses the default | | `GET /v1/events` | 50 | 100 | `pagination.next_cursor` | Uses the default | | `GET /v1/inbox/conversations` | 25 | 100 | `pagination.next_cursor` | Uses the default | | `GET /v1/inbox/conversations/{id}/messages` | 25 | 200 | `pagination.next_cursor` | Uses the default | | `GET /v1/inbox/comments` | 25 | 100 | `pagination.next_cursor` | `400 validation.field_invalid` | | `GET /v1/inbox/comments/{postId}` | 25 | 100 | `pagination.next_cursor` | `400 validation.field_invalid` | | `GET /v1/inbox/comments/{postId}/{commentId}/replies` | 10 | 100 | `pagination.next_cursor` | `400 validation.field_invalid` | | `GET /v1/inbox/reviews/{accountId}` | 20 | 50 | `pagination.next_cursor` | `400 validation.field_invalid` | | `GET /v1/accounts/{id}/mentions` | 20 | 100 | `next_cursor` | `400 validation.field_invalid` | | `GET /v1/automations/{id}/firings` | 20 | 100 | `next_cursor` | `400 validation.field_invalid` | | `GET /v1/media` | 50 | 100 | `next_cursor` | Uses the default | | `GET /v1/webhooks/{id}/deliveries` | 50 | 100 | `next_cursor` | Uses the default | | `GET /v1/platforms/pinterest/accounts/{id}/pins` | 50 | 50 | None: one page only | Uses the default | | `GET /v1/platforms/google/accounts/{id}/media` | Set by Google | Not enforced | `next_cursor` | Ignored: Google sets the page size | | `GET /v1/platforms/google/accounts/{id}/search-keywords` | 100 | 100 | `data.next_cursor` | `0` uses the default. A negative value returns `400 validation.page_size_invalid` | | `GET /v1/platforms/whatsapp/accounts/{id}/templates` | Set by Meta | Not enforced | `paging.cursors.after` | Ignored: Meta sets the page size | Rules for an out-of-range `limit`: * **Above the maximum:** the API uses the maximum, except on `GET /v1/webhooks/{id}/deliveries`, where a value above 100 uses 50. * **Text instead of a number:** treated like 0, except on `GET /v1/inbox/reviews/{accountId}`, where text uses 20 and only a number below 1 returns `400 validation.field_invalid`. * **Google and WhatsApp endpoints:** positive values go to the platform unchecked, so the platform decides what it accepts. The exception is `GET /v1/platforms/google/accounts/{id}/search-keywords`, where a negative value or a value above 100 returns `400 validation.page_size_invalid`. * **Platform page limits:** `GET /v1/inbox/comments/{postId}` can return fewer comments than `limit`, because the platform sets its own page size. YouTube returns at most 100 per page, TikTok at most 30 plus any hidden comments added to the first page, and Bluesky returns one page. On TikTok, those added hidden comments can take the first page past `limit`: see [TikTok comments](/platforms/tiktok#comments). `GET /v1/posts` rejects a malformed cursor with `400 validation.field_invalid` and `error.meta.field` set to `cursor`. Pass only values the API returned. `GET /v1/events` returns the events inside your plan's event history. See [Plans and usage](/guides/plans#history-and-export-allowances). ## Lists with no pagination These endpoints return every item in one response: * `GET /v1/accounts` (filter with `brand_id`), `GET /v1/brands`, `GET /v1/keys`, `GET /v1/keys/scopes`, `GET /v1/invites`, `GET /v1/announcements`, `GET /v1/billing/plans` * `GET /v1/webhooks`, `GET /v1/webhooks/events` * `GET /v1/automations` (filter with `account_id`) * `GET /v1/accounts/{id}/pages`, `GET /v1/accounts/{id}/boards` * `GET /v1/inbox/reviews` * `GET /v1/platforms/facebook/logins/{loginId}/pages`, `GET /v1/platforms/google/logins/{loginId}/locations`, `GET /v1/platforms/linkedin/logins/{loginId}/profiles` * `GET /v1/platforms/instagram/accounts/{id}/media/{mediaId}/collaborators` * `GET /v1/exports` returns the 50 newest exports. Older exports are not listed. * `GET /v1/exports/{id}/videos` Analytics, metrics, and insights endpoints return a date range or a time series instead of pages. `GET /v1/usage` returns one object. ## Related * [API reference](/api-reference/introduction): every query parameter on each list endpoint * [IDs](/guides/ids): store IDs and cursors as opaque strings * [Errors](/guides/errors): the `validation.*` codes list endpoints return * [Plans and usage](/guides/plans): how long events and webhook deliveries stay listed on each plan * [Rate limits](/guides/rate-limits): the limits that apply to every request, including platform limits on reads # Account permissions Source: https://docs.social-api.ai/guides/permissions Connect an account with only the permissions your integration uses, so the person connecting it is never asked for more. Account permissions limit what one connected account can do. A brand's first account on a platform gets every permission the platform supports unless you send `permissions`. Send `permissions` when you connect an account to ask for less: the person connecting sees a shorter consent screen where the platform allows it, and the account can do only what you selected. Permissions are not [API key scopes](/guides/scoped-keys). A permission limits what a connected account can do on its platform and is written `resource.action`. A key scope limits what an API key can do in SocialAPI and is written `resource:action`. Connecting an account and changing its permissions cost no credits. ## Endpoints | Method | Path | Description | | - | - | - | | `POST` | `/v1/accounts/connect` | Connect an account. Send `permissions` to limit it | | `POST` | `/v1/invites` | Create an invite link. Send `permissions` to limit the account it connects | | `GET` | `/v1/invites` | List a brand's invite links, each with its `permissions` | | `GET` | `/v1/accounts` | List accounts, each with its `permissions` and `effective_permissions` | | `PATCH` | `/v1/accounts/{id}` | Replace an account's permissions | ## Connect with permissions This request connects an Instagram account that can read and answer comments and nothing else: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/connect \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "instagram", "brand_id": "1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f", "redirect_uri": "https://app.example.com/oauth/callback", "permissions": ["comments.read", "comments.write"] }' ``` Response (HTTP 202): the usual connect response, described in [Connect accounts](/guides/connect-accounts#response-fields). After the person approves, `GET /v1/accounts` returns the account in `data` with both lists (other fields omitted): ```json theme={null} { "id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "instagram", "brand_id": "1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f", "status": "active", "permissions": ["comments.read", "comments.write"], "effective_permissions": ["comments.read", "comments.write"] } ``` `permissions` is a list of the names in [The permissions](#the-permissions). Duplicates collapse, and the lists are always in the order of that table. * **Omitted.** SocialAPI keeps the permissions of the brand's existing account on that platform. A brand with none gets every permission. Reconnecting without `permissions` never changes them. * **An empty list.** The request returns `400 validation.permissions_empty`. * **An unknown name.** The request returns `400 validation.permission_unknown`, and `error.meta.permission` holds the name. * **A name the platform has no use for.** The request is accepted. The name stays in `permissions` and never appears in `effective_permissions`. For example `reviews.read` on Instagram. ## The permissions | Permission | Covers | | - | - | | `posts.read` | Reading the account's posts from the platform, including posts published outside SocialAPI: `POST /v1/accounts/{id}/sync`, stories, collaborators, and exports | | `posts.write` | Publishing, editing, and deleting posts, the platform's posting options, such as TikTok creator info, and accepting or declining collaborator invites | | `comments.read` | Reading comments and mentions, and the `comment.received` and `mention.received` webhooks | | `comments.write` | Replying to, hiding, deleting, and liking comments, and replying to mentions | | `messages.read` | Reading conversations, messages, participant profiles, and follow status, and the `dm.*` webhooks | | `messages.write` | Sending and reacting to messages, and private replies to comments | | `reviews.read` | Reading reviews and the `review.*` webhooks | | `reviews.write` | Replying to reviews | | `analytics.read` | Account, brand, and post analytics, live post metrics, and platform insights | ## Permissions per platform An account can use a permission only on a platform that supports it. The other names are accepted and ignored. | Platform | Supported permissions | Consent screen | | - | - | - | | Instagram | `posts.read`, `posts.write`, `comments.read`, `comments.write`, `messages.read`, `messages.write`, `analytics.read` | Follows the selection | | Facebook | `posts.read`, `posts.write`, `comments.read`, `comments.write`, `messages.read`, `messages.write`, `analytics.read` | Follows the selection | | Threads | `posts.read`, `posts.write`, `comments.read`, `comments.write`, `analytics.read` | Follows the selection | | Google Business Profile | `posts.read`, `posts.write`, `reviews.read`, `reviews.write`, `analytics.read` | The same for every selection | | TikTok | `posts.read`, `posts.write`, `comments.read`, `comments.write`, `analytics.read` | The same for every selection. A [creator connection](/platforms/tiktok#creator-connection) supports `posts.read`, `posts.write`, and `analytics.read`, and follows the selection | | LinkedIn | Member profile (`connection_type: "profile"`): `posts.write`, `comments.write`, and `analytics.read`. Company Page (`connection_type: "organization"`): `posts.read`, `posts.write`, `comments.read`, `comments.write`, and `analytics.read`. See [LinkedIn](/platforms/linkedin#connect-an-account) | Follows the selection | | X / Twitter | `posts.read`, `posts.write`, `comments.read`, `comments.write`, `messages.read`, `messages.write`, `analytics.read` | Follows the selection | | YouTube | `posts.read`, `posts.write`, `comments.read`, `comments.write`, `analytics.read` | The same for every selection | | Pinterest | `posts.read`, `posts.write`, `analytics.read` | Follows the selection | | WhatsApp | `messages.read`, `messages.write` | The same for every selection | | Telegram | `messages.read`, `messages.write` | No selection step | | Zalo | `posts.read`, `posts.write`, `messages.read`, `messages.write` | The same for every selection | | Bluesky | `posts.read`, `posts.write`, `comments.read`, `comments.write`, `messages.read`, `messages.write` | No selection step | | App Store Connect | `reviews.read`, `reviews.write` | No selection step | * **Follows the selection.** The consent screen asks for the platform scopes of the permissions you selected. A platform scope that serves more than one permission can make the screen show more than you selected: Instagram asks for comments and for messages as 1 scope each, covering reading and writing. Facebook asks for messages as 1 scope, and asks for `pages_read_user_content` for both `comments.read` and `comments.write`. Pinterest asks for `boards:read` and `pins:read` for both `posts.read` and `posts.write`. Threads asks for publishing and deleting as 1 pair of scopes shared by `posts.write` and `comments.write`. X asks for `tweet.write` once for `posts.write` and `comments.write`. The account stays limited to your selection: an Instagram account connected with `comments.read` alone cannot reply to comments. * **The same for every selection.** The platform shows its fixed list of scopes. Google Business Profile asks for 3 identity scopes and 1 scope, `business.manage`, that covers all of its permissions. YouTube asks for 1 scope that covers everything. TikTok (except a creator connection), WhatsApp, and Zalo ask for no scope that depends on your selection. The account is still limited to your selection, and an account connected with `reviews.read` alone cannot reply to reviews. * **No selection step.** Telegram, Bluesky, and App Store Connect connect without a consent screen, so SocialAPI alone enforces your selection. Each platform's page lists the scopes it requests, under Scopes. ## What an account can do right now Every account has two lists: | Field | Meaning | | - | - | | `permissions` | What you selected, or `null` for every permission, including ones added later. Always present | | `effective_permissions` | What the account can use now. Always a list, empty when none apply | `effective_permissions` is `permissions` limited to what the platform supports and what the person connecting granted. The person can decline part of the consent screen, and a platform can withdraw access later. A call that needs a permission outside `effective_permissions` fails with `403 account.permission_missing`, and `error.meta.permission` names it. The refusal comes before the call reaches the platform. A platform's unsupported operation keeps its usual answer, `501 resource.not_supported`, whatever the permissions. These calls answer differently: * **Publishing** (`POST /v1/posts`, `POST /v1/posts/{pid}/publish`, `POST /v1/posts/{pid}/retry`, and a scheduled post when it is delivered) reports `account.permission_missing` on the target and returns no `403`. * **Removing a published post from a platform.** `DELETE /v1/posts/{pid}` reports the target in `results` with `outcome: "failed"` and leaves it `published`. `POST /v1/posts/{pid}/unpublish` returns `500 system.internal`, and the target stays `published`. * **Live post metrics** (`GET /v1/posts/{pid}/metrics`) report `resource.not_found` on the target. * **Stored lists.** `GET /v1/inbox/comments`, `GET /v1/inbox/conversations`, and `GET /v1/inbox/conversations/{id}/messages` return what SocialAPI stored and never return `account.permission_missing`. * **Review accounts.** `GET /v1/inbox/reviews` does not check `reviews.read`. A refused call costs what its path charges: * A comment reply, a DM, a review reply, and a mention reply cost no credit, because the check comes before the charge. * A post follows the post credit rules in [Plans and usage](/guides/plans#when-a-credit-comes-back). * Every other call is free. An account without a permission also stops receiving what it covers: * **Webhooks and Automations.** Comments, mentions, messages, and reviews for a permission the account lacks are dropped: they do not reach your webhook endpoints, your Automations, or the inbox. With `messages.write` alone, the account still receives `dm.status.read` on Instagram, `dm.status.delivered` and `dm.status.read` on Facebook, and every `dm.status.*` event on WhatsApp. It receives no `dm.sent` and no `dm.status.sent` on Instagram or Facebook. * **Posts.** A post to an account without `posts.write` fails on its target with `account.permission_missing`, whether you publish it now or schedule it. For a scheduled post, the failure appears when the delivery runs. * **Analytics.** The account adds no new points to its stored analytics history. When an account loses a permission you did not remove yourself, you receive an [`account.updated` webhook](/guides/webhook-events#account-updated) with `change` set to `permissions_reduced`. Reconnect the account to ask for the permission again. On X, when X refuses a call because the account's access lacks a scope, the call returns `403 platform.twitter.insufficient_scope`. `error.meta.scope` names the scope when it is known (`media.write` for a media upload). The account does not become `token_expired`. When the scope is named, the permissions that need it leave `effective_permissions` until you reconnect the account: `posts.write` for `media.write`. Other platforms return their own `platform..*` code for the same refusal and do not change `effective_permissions`. ## Invite links `POST /v1/invites` takes the same `permissions` field. The account the person connects gets exactly that list, and on a platform whose consent screen [follows the selection](#permissions-per-platform) the person is asked only for what those permissions need. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/invites \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "brand_id": "1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f", "platform": "instagram", "expires_in_days": 7, "permissions": ["comments.read", "comments.write"] }' ``` Response (HTTP 201): ```json theme={null} { "id": "3f6c1a52-9d4e-4b7a-8c21-5e0f7a9b1d34", "platform": "instagram", "token": "a55755cf75f73b56deb3dfe0c993fd36fc4c2bfa7e1d94c60b8a3f52d17e9c40", "url": "https://api.social-api.ai/invite/a55755cf75f73b56deb3dfe0c993fd36fc4c2bfa7e1d94c60b8a3f52d17e9c40", "expires_at": "2026-10-20T09:00:00Z", "permissions": ["comments.read", "comments.write"] } ``` The response and every item of `GET /v1/invites` return `permissions` as you set it. `null` means you omitted it: when the link is used, SocialAPI applies the rule for an omitted `permissions` described in [Connect with permissions](#connect-with-permissions). ## Change an account's permissions `PATCH /v1/accounts/{id}` replaces an account's permissions. `permissions` is required: send a list, or `null` to give the account every permission, including ones added later. ```bash theme={null} curl -X PATCH https://api.social-api.ai/v1/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{"permissions": ["comments.read", "comments.write", "messages.read"]}' ``` Response (HTTP 200): the account, with its new `permissions` and `effective_permissions`. ```json theme={null} { "id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "instagram", "name": "Acme Corp", "username": "acmecorp", "brand_id": "1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f", "status": "active", "permissions": ["comments.read", "comments.write", "messages.read"], "effective_permissions": ["comments.read", "comments.write", "messages.read"] } ``` A request with no `permissions` field returns `400 validation.field_required`. A value that is neither a list of strings nor `null` returns `400 validation.body_invalid`. An empty list returns `400 validation.permissions_empty`, and an unknown name returns `400 validation.permission_unknown`. An ID that is not one of your accounts, or that a [brand-restricted key](/guides/scoped-keys) cannot reach, returns `404 account.not_found`. ### Removing a permission deletes data > [!WARNING] > > Removing a permission takes effect at once and deletes the data it covered. Adding the permission back does not restore the data. | Permission removed | Deleted at once | | - | - | | `comments.read` | The account's comments and the posts they belong to | | `messages.read` | The account's conversations and messages, with their attachments | | `posts.read` | Posts SocialAPI read from the platform, including their analytics history. Posts you published through SocialAPI are kept | | `analytics.read` | The account's stored analytics history | | `posts.write`, `comments.write`, `messages.write`, `reviews.read`, `reviews.write` | Nothing. A post to the account fails on its target with `account.permission_missing`, and the other calls that need the permission fail with `403 account.permission_missing` | If the deletion fails, the call fails and the permissions stay unchanged. Connecting the account again with a narrower `permissions` list deletes the same data. A permission the person declines on the consent screen when reconnecting leaves `effective_permissions`, and SocialAPI sends `account.updated`. It also deletes the same data, except on the first reconnect of an account connected before account permissions existed. ### Adding a permission Adding a permission takes effect at once when the platform already granted it to the account. Otherwise the permission appears in `permissions` but not in `effective_permissions`: reconnect the account so the person can grant it. Once a permission is in `effective_permissions`, the events it covers arrive again. ## Automations An [Automation](/guides/automations) needs permissions on its account. Creating or updating it returns `400 validation.permission_missing` when its trigger or actions need a permission outside the account's `effective_permissions`, and `error.meta.permission` names it. | Automation part | Needs | | - | - | | A `comment` trigger | `comments.read` | | A `dm` trigger | `messages.read` | | A `follows_account` filter | `messages.read` | | A `reply_comment` or `hide_comment` action | `comments.write` | | A `private_reply` or `send_dm` action | `messages.write` | An update is checked only when it changes `steps` or sets `status` to `active`. An Automation is not paused when its account later loses a permission: its actions fail on the [Firing](/guides/automations#firings) with `account.permission_missing`. ## Errors * `403 account.permission_missing`: the account does not have the permission the call needs. `error.meta.permission` names it. Add it with `PATCH /v1/accounts/{id}`, or reconnect the account when the platform has not granted it. * `400 validation.permission_unknown`: a value in `permissions` is not a permission. `error.meta.permission` names it. * `400 validation.permissions_empty`: `permissions` is an empty list. * `400 validation.permission_missing`: an Automation needs a permission the account does not have. `error.meta.permission` names it. * `403 platform.twitter.insufficient_scope`: X refused the call because the account's access lacks a scope. Every code is described in [Errors](/guides/errors). ## Scopes | Scope | Grants | | - | - | | `accounts:read` | `GET /v1/accounts` | | `accounts:manage` | `PATCH /v1/accounts/{id}` and `POST /v1/accounts/connect` | Invite endpoints need a full-access key. See [Scoped API keys](/guides/scoped-keys). ## Events `account.updated` with `change: "permissions_reduced"` fires when an account loses a permission you did not remove yourself. It does not fire when you remove a permission with `PATCH /v1/accounts/{id}`. The payload is in [Webhook events](/guides/webhook-events#account-updated). ## Use from an AI agent | Tool | What it does | | - | - | | `social_api_connect_account` | Starts a connection. Accepts `permissions` | | `social_api_create_invite` | Creates an invite link. Accepts `permissions` | | `social_api_update_account_permissions` | Replaces an account's permissions. Send `unrestricted: true` for every permission. Sending neither `permissions` nor `unrestricted` returns an error | See [MCP tools](/integrations/tools) for every tool. ## Related * [Connect accounts](/guides/connect-accounts) for the connect flow and the account fields * [Webhook events](/guides/webhook-events) for the `account.updated` payload * [Automations](/guides/automations) for the permissions each trigger and action needs * [Scoped API keys](/guides/scoped-keys) for limiting what an API key can do * [Errors](/guides/errors) for every error code # Plans and usage Source: https://docs.social-api.ai/guides/plans Compare SocialAPI plans, see what costs a post or interaction credit, read your usage from the API, and learn what happens when a payment fails. Every SocialAPI plan includes every platform and every feature, with two exceptions listed below. Plans differ by allowances: how many brands you can create, how many posts and interactions you can send per billing period, how much media you can store, how long your history is kept, and how many analytics exports you can run. A brand groups the connected accounts of one business, client, or project. Pricing pages call a brand a "profile": the two words mean the same thing. Compare prices on the [pricing page](https://social-api.ai/pricing) and change your plan in the dashboard under **Billing**. ## Endpoints | Method | Path | Description | | - | - | - | | `GET` | `/v1/billing/plans` | List every plan with its prices and allowances | | `GET` | `/v1/usage` | Read your usage in the current billing period and your allowances | | `GET` | `/v1/media/storage` | Read your media storage use and allowance | ## Plan allowances | Plan (`tier`) | Name | Brands | Posts per billing period | Interactions per billing period | Media storage | Price per month | Price per year | | - | - | - | - | - | - | - | - | | Free (`free`) | Hobby | 2 | 10 | 50 | 100 MB | \$0 / €0 | \$0 / €0 | | Starter (`starter`) | Side Hustle | 10 | Unlimited | Unlimited | Unlimited | \$29 / €26 | \$278 / €247 | | Pro (`pro`) | Full Send | 50 | Unlimited | Unlimited | Unlimited | \$109 / €97 | \$1,044 / €929 | | Business (`business`) | Empire | 200 | Unlimited | Unlimited | Unlimited | \$349 / €311 | \$3,348 / €2,980 | | Enterprise (`enterprise`) | Enterprise | Unlimited | Unlimited | Unlimited | Unlimited | Custom | Custom | Every paid plan is sold in USD and in EUR; pick the currency at checkout. A subscription keeps the currency it started in, including when you change plan. `GET /v1/users/me` returns it as `billing_currency`: `usd`, `eur`, or an empty string when you never subscribed. 100 MB is 104,857,600 bytes. What each limit returns when you reach it: | Allowance | When you reach it | | - | - | | Brands | Creating another brand returns `403 billing.brands_limit` | | Posts | The next publish returns `429 validation.post_limit_exhausted` | | Interactions | The next reply or DM returns `429 validation.interaction_limit_exhausted` | | Media storage | An upload that would exceed it returns `413 billing.storage_quota`. See [Media](/guides/media) | Two features depend on the plan rather than on an allowance: * **WhatsApp template messages** need a paid plan. On the Free plan, `POST /v1/platforms/whatsapp/accounts/{id}/send-template` returns `403 whatsapp.paid_plan_required`. Replies to people who messaged you first work on every plan. See [WhatsApp](/platforms/whatsapp). * **Visual analysis in exports** (`include_vision`) needs a paid plan. On the Free plan, SocialAPI ignores `include_vision: true` and runs the export without it. ### Profile packs On the Pro (Full Send) and Business (Empire) plans, your brand allowance grows in Profile packs of 50 brands. When your brand count reaches the scale threshold of your allowance (90% by default), SocialAPI adds a pack, within the pack budget you set. Packs you no longer need are released at the end of the billing period. Set the threshold and the budget in the dashboard under **Billing**. Each pack held raises `brands_limit` in `GET /v1/usage` by 50. ## Facebook Pages per brand When `facebook_page_selection` is `true` on your SocialAPI account (`GET /v1/users/me`), each brand holds at most 2 active Facebook Pages on Free, 5 on Starter, 10 on Pro, 20 on Business, and any number on Enterprise. A Page is active when `is_active` is `true` in `GET /v1/accounts/{id}/pages`. Choosing or assigning more returns `403 platform.facebook.page_cap`, with the allowance in `error.meta.limit`. Pages cost no credits. When `facebook_page_selection` is `false`, no Page cap applies. When `facebook_page_selection` is `true` and you change to a plan with a lower allowance, or a cancellation moves your SocialAPI account to the Free plan, SocialAPI sets `is_active` to `false` on the Pages of each brand above the new allowance, starting from the last Page added, and keeps the Pages added first. A Page with `is_active: false` cannot be published to. ## History and export allowances | Allowance | Free | Starter | Pro | Business | Enterprise | | - | - | - | - | - | - | | Event history in `GET /v1/events` | 1 day | 7 days | 30 days | 90 days | 90 days | | Webhook delivery log | 3 days | 7 days | 30 days | 90 days | 90 days | | Analytics exports per billing period | 2 | 10 | 20 | Unlimited | Unlimited | | Videos per export | 30 | 200 | 500 | All | All | | Wait between 2 exports of the same connected account | 7 days | 24 hours | 1 hour | 1 hour | None | | Report link lifetime | 7 days | 30 days | 30 days | 90 days | 90 days | | Transcripts (`include_transcript`) | Yes | Yes | Yes | Yes | Yes | | Visual analysis (`include_vision`) | No | Yes | Yes | Yes | Yes | Your SocialAPI account runs 1 export at a time on every plan. When an export cannot start: * The billing period's export allowance is used up: `429 billing.export_limit`. It resets at the start of the next billing period. * The same connected account was exported less than the wait above ago: `429 billing.export_cooldown`. * Another export is still running: `429 billing.export_concurrent`. Pinterest accounts cannot be exported on any plan: an export returns `400 account.exports_not_supported`. ## The plan catalog `GET /v1/billing/plans` returns every plan with its prices and allowances, ordered from cheapest to most expensive. Read it instead of hardcoding the table above. Any API key can call it, and it costs no credits. ```bash theme={null} curl https://api.social-api.ai/v1/billing/plans \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200), shortened to 2 of the 5 plans: ```json theme={null} { "plans": [ { "tier": "free", "name": "Hobby", "tagline": "Kick the tires, no card required", "monthly_price_usd": 0, "annual_price_usd": 0, "monthly_price_eur": 0, "annual_price_eur": 0, "popular": false, "limits": { "profiles": 2, "posts_per_month": 10, "interactions_per_month": 50, "storage_bytes": 104857600, "event_retention_days": 1, "exports_per_month": 2 } }, { "tier": "pro", "name": "Full Send", "tagline": "For growing teams and products", "monthly_price_usd": 109, "annual_price_usd": 1044, "monthly_price_eur": 97, "annual_price_eur": 929, "popular": true, "limits": { "profiles": 50, "posts_per_month": -1, "interactions_per_month": -1, "storage_bytes": -1, "event_retention_days": 30, "exports_per_month": 20 } } ] } ``` | Field | Description | | - | - | | `tier` | The plan's identifier: `free`, `starter`, `pro`, `business`, or `enterprise` | | `name` | The plan's display name | | `monthly_price_usd` | Price per month in USD. `-1` means custom pricing | | `annual_price_usd` | Price for a full year in USD, charged once per year. `-1` means custom pricing | | `monthly_price_eur` | Price per month in EUR. `-1` means custom pricing | | `annual_price_eur` | Price for a full year in EUR, charged once per year. `-1` means custom pricing | | `popular` | `true` for the plan the pricing page highlights | | `limits.profiles` | Brands allowed. `-1` means unlimited | | `limits.posts_per_month` | Post credits per billing period. `-1` means unlimited | | `limits.interactions_per_month` | Interaction credits per billing period. `-1` means unlimited | | `limits.storage_bytes` | Media storage in bytes. `-1` means unlimited | | `limits.event_retention_days` | Days of event history in `GET /v1/events` | | `limits.exports_per_month` | Analytics exports per billing period. `-1` means unlimited | ## What counts as a post or interaction Post and interaction credits are counted on plans with a post or interaction allowance (the Free plan). On plans where both are unlimited, nothing is counted and `posts_used` and `interactions_used` stay at `0`. **1 post credit** for each of these requests: * `POST /v1/posts` with `publish_now: true` or a `scheduled_at`. Saving a draft costs nothing. * `POST /v1/posts/{pid}/publish`, which publishes a draft or a scheduled post. It costs 1 credit even when the post was scheduled with `POST /v1/posts`, which already cost 1 credit. * `POST /v1/posts/{pid}/retry`. A post costs 1 credit whatever the number of targets. **1 interaction credit** for each of these requests: * A public reply to a comment: `POST /v1/inbox/comments/{postId}`. * A reply to a review: `POST /v1/inbox/reviews/{id}/reply`. * A reply to a mention: `POST /v1/accounts/{id}/mentions/reply`. * A DM: `POST /v1/inbox/conversations/{id}/messages`. One request costs 1 credit, even when it sends more than one message. * A WhatsApp template: `POST /v1/platforms/whatsapp/accounts/{id}/send-template`. **No credit** for everything else, including private replies, hiding, liking, or deleting a comment, reactions to messages, every read, and every account, brand, key, webhook, and Automation operation. The same rules apply to [MCP tools](/integrations/tools) and to the actions an [Automation](/guides/automations) carries out: each costs what the equivalent API request costs. ### When a credit comes back SocialAPI gives the credit back when: * Any request that costs a credit fails with a `validation.*` error. * `POST /v1/posts` fails with `publishing.validation_failed`. * `POST /v1/posts`, `POST /v1/posts/{pid}/publish`, or `POST /v1/posts/{pid}/retry` fails with `429 publishing.velocity_limit`. * Every target of `POST /v1/posts` with `publish_now: true`, or of `POST /v1/posts/{pid}/publish`, fails. * SocialAPI refuses a reply or DM before sending it because the conversation's messaging window is closed (see [Direct messages](/guides/direct-messages)). A comment reply, DM, review reply, or mention reply refused with `403 account.permission_missing` is never charged: see [Permissions](/guides/permissions). Any other failure keeps the credit spent. That includes a request the platform refuses (for example `429 platform.{platform}.rate_limit` on a reply or DM), a `409 resource.conflict` for duplicate content, a post that fails on only some of its targets, and a retry that fails again. ## Billing periods Allowances reset at the start of each billing period. * **Paid plans:** the billing period is your subscription's current period. It starts on the date you subscribed and renews with each payment. * **Free plan:** the billing period starts at 00:00 UTC on the day of the month your SocialAPI account was created, and ends at the same time on that day of the next month. `period_start` and `period_end` in `GET /v1/usage` give the exact bounds of the current period. ## Check your usage `GET /v1/usage` returns what you used in the current billing period and your allowances. Any API key can call it, and it costs no credits. ```bash theme={null} curl https://api.social-api.ai/v1/usage \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200) for a SocialAPI account on the Free plan: ```json theme={null} { "brands_used": 1, "brands_limit": 2, "profile_units_used": 1, "profile_units_limit": 2, "page_allowance": 2, "page_billing_exempt": false, "posts_used": 7, "posts_limit": 10, "interactions_used": 23, "interactions_limit": 50, "period_start": "2026-10-04T00:00:00Z", "period_end": "2026-11-04T00:00:00Z" } ``` | Field | Description | | - | - | | `brands_used` | Brands in your SocialAPI account | | `brands_limit` | Brands your plan allows, including Profile packs. `-1` means unlimited | | `profile_units_used` | Always equal to `brands_used` | | `profile_units_limit` | Always equal to `brands_limit` | | `page_allowance` | Ignore this field | | `page_billing_exempt` | Deprecated. Facebook Pages are not billed: ignore this field | | `posts_used` | Post credits spent in the current billing period | | `posts_limit` | Post credits per billing period. `-1` means unlimited | | `interactions_used` | Interaction credits spent in the current billing period | | `interactions_limit` | Interaction credits per billing period. `-1` means unlimited | | `period_start` | Start of the current billing period (ISO 8601, UTC) | | `period_end` | End of the current billing period (ISO 8601, UTC). Credits reset at this time | `GET /v1/media/storage` returns your media storage: `used_bytes`, `limit_bytes` (`-1` means unlimited), and `count`, the number of stored files. Any API key can call it, whatever its scopes, and it costs no credits. ### Usage headers Responses also report usage in headers, so you can track it without calling `GET /v1/usage`: | Header | Sent on | Value | | - | - | - | | `X-Posts-Limit` | Requests that spend a post credit, on a plan with a post allowance | Post credits per billing period | | `X-Posts-Remaining` | Same as `X-Posts-Limit` | Post credits left in the current billing period | | `X-Interactions-Limit` | Requests that spend an interaction credit, on a plan with an interaction allowance | Interaction credits per billing period | | `X-Interactions-Remaining` | Same as `X-Interactions-Limit` | Interaction credits left in the current billing period | | `X-Storage-Used` | `GET /v1/media/storage`, `GET /v1/media/upload-url`, `POST /v1/media/upload`, and `POST /v1/media/{id}/verify`, on every plan | Bytes of media stored | | `X-Storage-Limit` | Same as `X-Storage-Used` | Media storage allowance in bytes. `-1` means unlimited | ## When you reach an allowance Over the post or interaction allowance, the request returns HTTP 429 and nothing is sent: ```json theme={null} { "error": { "code": "validation.post_limit_exhausted", "message": "Monthly post limit exhausted" }, "request_id": "a1b2c3d4" } ``` The interaction allowance returns the same shape with `validation.interaction_limit_exhausted`. Both reset at `period_end`. To continue before then, upgrade your plan. `POST /v1/posts/import` checks the allowance before it creates anything. When the CSV holds more valid rows than you have post credits left, drafts included, it returns `429 billing.post_limit` and creates no post. The posting velocity caps are separate from plan allowances: upgrading does not raise them. See [Rate limits](/guides/rate-limits). ## Change or cancel your plan Change or cancel your plan in the dashboard under **Billing**. A cancelled subscription moves your SocialAPI account to the Free plan at the end of the billing period. When a plan change lowers your allowances: * Brands above the new allowance stay. Creating another brand returns `403 billing.brands_limit` until you are below the allowance. * When `facebook_page_selection` is `true`, Facebook Pages above the new per-brand allowance stop being active, starting from the last Page added. See [Facebook Pages per brand](#facebook-pages-per-brand). ## Past-due accounts Your SocialAPI account becomes past due when a subscription payment fails for the second time (the first charge and one automatic retry). While it is past due: * **API:** every endpoint returns `402 billing.past_due`, except every method of `/v1/users/me` and every `/v1/billing/*` endpoint, which stay available so you can pay. * **MCP server:** every request returns HTTP 402 with the code `billing.past_due`. * **Webhooks:** SocialAPI delivers no events to your webhook endpoints. Events from this time are not queued, get no delivery record, and are not sent after you pay. * **Automations:** Automations carry out no actions and record no Firings. * **Platform events:** SocialAPI still records comments, messages, and other platform events. Read them with `GET /v1/events` after you pay, within your plan's event history. * **Scheduled posts:** scheduled posts still publish at their scheduled time. `GET /v1/users/me` shows the state: `billing_blocked` is `true`, `billing_blocked_at` is when it began, and `billing_block_reason` is `past_due`. Pay the open invoice in the dashboard under **Billing** to restore access. Access returns when the payment succeeds, with no request to make. If every automatic retry fails and the subscription is cancelled, your SocialAPI account moves to the Free plan and access returns on the Free plan's allowances. ## Use from an AI agent | Tool | What it does | | - | - | | `social_api_get_usage` | Returns the `brands_*`, `posts_*`, `interactions_*`, `period_start`, and `period_end` fields of `GET /v1/usage`. It omits `profile_units_*`, `page_allowance`, and `page_billing_exempt` | | `social_api_get_storage_usage` | Returns bytes used, the storage allowance in bytes (`-1` means unlimited), and the number of stored files | ## Related * [Rate limits](/guides/rate-limits): the request rate limit, posting velocity caps, and platform limits that apply on every plan * [Errors](/guides/errors): every `billing.*` code with its status * [Media](/guides/media): uploads, storage, and the storage quota * [Webhooks](/guides/webhooks): event delivery and the delivery log * [Automations](/guides/automations): what each action costs # Platform apps Source: https://docs.social-api.ai/guides/platform-credentials See which app connects each platform: SocialAPI's own apps for most, and your own X app, Telegram bot, Bluesky app password, or App Store Connect key. A platform app is the registration that a social platform requires before software can ask a person for access to their account. SocialAPI connects Instagram, Facebook, Threads, Google Business Profile, TikTok, LinkedIn, YouTube, Pinterest, Zalo, and WhatsApp through its own apps, so you register no app, apply for no app review, and keep no platform secret. X connects through an X developer app that you create. Telegram connects through your bot's token, Bluesky through an app password, and App Store Connect through your App Store Connect API key. Which app connects a platform changes only what you provide when you [connect an account](/guides/connect-accounts). It does not change the endpoints you call afterwards. ## Who connects each platform | Platform | Slug | App that connects it | What you provide | | - | - | - | - | | Instagram | `instagram` | SocialAPI's app | Nothing. The person signs in to Instagram, or to Facebook with [Facebook Login](/platforms/instagram#connect-through-facebook-login), and approves | | Facebook | `facebook` | SocialAPI's app | Nothing. The person signs in to Facebook and approves | | Threads | `threads` | SocialAPI's app | Nothing. The person signs in to Threads and approves | | Google Business Profile | `google` | SocialAPI's app | Nothing. The person signs in to Google and approves | | TikTok | `tiktok` | SocialAPI's app | Nothing. The person signs in to TikTok and approves | | LinkedIn member profile or company Page | `linkedin` | SocialAPI's app | Nothing. The person signs in to LinkedIn and approves | | YouTube | `youtube` | SocialAPI's app | Nothing. The person signs in to Google and approves | | Pinterest | `pinterest` | SocialAPI's app | Nothing. The person signs in to Pinterest and approves | | Zalo | `zalo` | SocialAPI's app | Nothing. The person signs in to Zalo and approves for their Official Account | | WhatsApp | `whatsapp` | SocialAPI's Meta app | Nothing to register, no domain to allowlist. The person completes Meta's signup through `auth_url` | | X / Twitter | `twitter` | Your own X developer app | The app's client ID and client secret, saved in the SocialAPI dashboard | | Telegram | `telegram` | Your own Telegram bot, no app | The bot token from [@BotFather](https://t.me/BotFather) | | Bluesky | `bluesky` | None | The account's handle and an app password | | App Store Connect | `app_store_connect` | Your own App Store Connect API key, no app | The key's Key ID and private key file, plus the Issuer ID for a Team key and the app ID when the key sees more than 1 app. See [App Store Connect](/platforms/app-store-connect#connect-an-account) | | Trustpilot | `trustpilot` | Not connectable | Nothing. Trustpilot is not available | Every connectable platform in the table is open to every plan; `trustpilot` is listed but cannot be connected and returns `403 billing.platform_not_available`. Two features need a paid plan: sending a WhatsApp template message, and visual analysis in exports (`include_vision`). See [Plans](/guides/plans). A slug not in the table also returns `403 billing.platform_not_available`. ## Why X uses your own app X bills API usage to the developer account that owns the app, in pay-per-use credits. X's free API tier is closed to new developer accounts. SocialAPI therefore connects X only through an app that you register, so every read and write on your X accounts is billed to your X developer account, not to SocialAPI. You save one X app for your SocialAPI account and, optionally, a different X app for a single brand. Starting an X connection without saved credentials fails with `412 byok.credentials_missing`. [Connect X with your own app](/platforms/twitter/own-app) walks through the setup. ## Telegram, Bluesky, and App Store Connect have no consent screen Telegram, Bluesky, and App Store Connect connect in one request. You send the credential in `metadata` on `POST /v1/accounts/connect`, and SocialAPI answers with the connected account: * **Telegram:** a bot token. You create the bot with [@BotFather](https://t.me/BotFather). The token does not expire. It stays valid until you revoke it in @BotFather, including after you disconnect the account. See [Telegram](/platforms/telegram). * **Bluesky:** the account's handle and an app password that you create in Bluesky's settings. Never send the account's real password. See [Bluesky](/platforms/bluesky). * **App Store Connect:** your App Store Connect API key: its Key ID and private key file, plus the Issuer ID for a Team key and the app ID when the key sees more than 1 app. The key stays valid until you revoke it in App Store Connect, including after you disconnect the account. See [App Store Connect](/platforms/app-store-connect#connect-an-account). ## Scopes each platform asks for Each platform's page lists the scopes that SocialAPI requests when the person connects, and what each one is for: [Instagram](/platforms/instagram), [Facebook](/platforms/facebook), [Threads](/platforms/threads), [Google Business Profile](/platforms/google), [TikTok](/platforms/tiktok), [LinkedIn](/platforms/linkedin), [YouTube](/platforms/youtube), [Pinterest](/platforms/pinterest), [Zalo](/platforms/zalo), and [X / Twitter](/platforms/twitter). Send `permissions` when you connect an account to ask the person for fewer scopes: see [Account permissions](/guides/permissions). ## FAQ > [accordion] Do I need to register an app or apply for app review? > > Not for Instagram, Facebook, Threads, Google Business Profile, TikTok, LinkedIn, YouTube, Pinterest, Zalo, or WhatsApp. SocialAPI's apps already exist, and you connect through them. For X you create your own developer app, as described in [Connect X with your own app](/platforms/twitter/own-app). > [accordion] Whose name appears on the platform's consent screen? > > The name of the app that connects the platform: SocialAPI's app for Instagram, Facebook, Threads, Google Business Profile, TikTok, LinkedIn, YouTube, Pinterest, Zalo, and WhatsApp, and your own X app for X. > [accordion] Can I use my own Meta, TikTok, or Google app? > > No. Only X connects through your own app. Every other platform that has a consent screen connects through SocialAPI's app. > [accordion] Does my plan change which app connects a platform? > > No. Plans change allowances such as brands, not apps. See [Plans](/guides/plans). > [accordion] What happens when a platform limits or revokes SocialAPI's app? > > Every account connected through that app is affected at the same time. SocialAPI posts incidents on [status.social-api.ai](https://status.social-api.ai) and as `incident` notices in [Announcements](/guides/announcements). X accounts are not affected, because each one connects through your own app. > [accordion] Do I handle the platform's access tokens? > > No. SocialAPI keeps them and refreshes them. You send your SocialAPI API key. When a platform ends an authorization, the account's `status` changes and you reconnect it: see [Account status](/guides/connect-accounts#account-status). ## Related * [Connect a social account](/guides/connect-accounts) for starting, reconnecting, and disconnecting connections * [Connect X with your own app](/platforms/twitter/own-app) for the X developer app setup * [Supported platforms](/platforms/overview) for what each platform can do once connected * [Plans](/guides/plans) for allowances that differ by plan * [Announcements](/guides/announcements) for incident and maintenance notices # Publish a post Source: https://docs.social-api.ai/guides/publishing Publish, validate, edit, and import posts to Instagram, Facebook, TikTok, YouTube, and more with one POST /v1/posts request and per-platform delivery status. A post in SocialAPI is one piece of content sent to one or more connected accounts. You create it with `POST /v1/posts`, name each destination in `targets`, and read the delivery result per platform. The same request publishes to Instagram, Facebook, Threads, TikTok, YouTube, LinkedIn, X, Pinterest, Google Business Profile, Zalo, and Bluesky. Zalo publishes text articles only, and Bluesky publishes text and up to 4 images. What each platform accepts is on its publishing page, linked from [Platform support](#platform-support). A post costs 1 post credit when it is published or scheduled, whatever the number of targets. Saving a draft is free. See [Plans and usage](/guides/plans#what-counts-as-a-post-or-interaction). ## Endpoints | Method | Path | Description | | - | - | - | | `POST` | `/v1/posts` | Create a draft, schedule a post, or publish it now | | `GET` | `/v1/posts` | List posts. Parameters are in the table below | | `GET` | `/v1/posts/{pid}` | Read one post with its targets | | `PATCH` | `/v1/posts/{pid}` | Edit a `draft`, `scheduled`, or `failed` post | | `POST` | `/v1/posts/{pid}/publish` | Publish a `draft` or `scheduled` post now | | `POST` | `/v1/posts/{pid}/retry` | Re-send the failed targets of a `failed` or `partial` post | | `POST` | `/v1/posts/{pid}/unpublish` | Remove the published copies from the platforms and keep the post | | `DELETE` | `/v1/posts/{pid}` | Delete a post | | `POST` | `/v1/posts/validate` | Check a post against platform rules without creating it | | `GET` | `/v1/posts/validate` | Read each platform's text and media rules | | `POST` | `/v1/posts/import` | Create up to 500 posts from a CSV file | `GET /v1/posts` takes these query parameters, all optional. Pagination is in [Pagination](/guides/pagination). | Parameter | Description | | - | - | | `status` | `draft`, `scheduled`, `publishing`, `published`, `partial`, `failed`, or `cancelled` | | `platform` | A platform slug, such as `instagram` | | `account_ids` | Connected account IDs, comma-separated | | `page_id` | A platform page ID, for a Facebook account that has more than one Page | | `brand_id` | Posts with a target in this brand | | `from`, `to` | RFC 3339 timestamps, both inclusive. They match the post's publish time, else its scheduled time, else its creation time | | `search` | Case-insensitive substring of the post's `text` | | `hidden` | `true` also returns posts whose `hidden` field is `true` | | `sort` | `scheduled_asc` (default), `scheduled_desc`, `created_asc`, or `created_desc` | ## Your first post Publish one text post to a Threads account and a Facebook Page: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Spring sale starts today: https://example.com/spring", "publish_now": true, "targets": [ {"account_id": "acc_01HZ9X8B3C4D5F6G7H9J2K1M"}, {"account_id": "acc_01HZ9X4T6B8C2D5F7G9H1J3K", "page_id": "sapi_page_01HZ9X5V7C9D3E6G8H1J4K2M"} ] }' ``` Response (HTTP 201): ```json theme={null} { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "text": "Spring sale starts today: https://example.com/spring", "status": "publishing", "hidden": false, "retry_count": 0, "created_at": "2026-10-13T09:00:00Z", "updated_at": "2026-10-13T09:00:00Z", "targets": [ { "account_id": "acc_01HZ9X8B3C4D5F6G7H9J2K1M", "platform": "threads", "status": "publishing" }, { "account_id": "acc_01HZ9X4T6B8C2D5F7G9H1J3K", "page_id": "sapi_page_01HZ9X5V7C9D3E6G8H1J4K2M", "platform": "facebook", "status": "publishing" } ] } ``` The post is accepted, and delivery continues after the response. Read the post again or wait for the `post.published`, `post.partial`, or `post.failed` event. [Track post delivery](/guides/delivery) explains the statuses, the HTTP codes, and the per-target errors. ## Choose what the request does | You send | Result | | - | - | | `publish_now: true` and at least one target | The post is published now. Its status is `publishing` | | `publish_now: true`, at least one target, and `stagger` | The first target publishes now and the others follow at the staggered gaps. Its status is `scheduled`. See [Stagger targets](/guides/scheduling#stagger-targets) | | `scheduled_at` in the future and at least one target | The post is scheduled. See [Schedule posts](/guides/scheduling) | | Neither, or no targets | The post is saved as a draft. It is not published, even when you send targets | | Both `publish_now` and `scheduled_at` | The post is scheduled | A `scheduled_at` that is not RFC 3339 returns `400 validation.field_invalid`. With at least one target, a `scheduled_at` in the past returns `400 validation.field_invalid`. With no targets, the post is saved as a draft. ## Fields every platform shares | Field | Type | Description | | - | - | - | | `text` | string | The post body. Required, unless every target supplies its own `text` or a `platform_data` field the platform uses as the body (a Pinterest Pin description). A missing body returns `400 validation.field_required` | | `title` | string | The post title. Used by YouTube (required there) and Zalo (the article title, which falls back to `text`). Other platforms ignore it, including LinkedIn and Google Business Profile. A Pinterest Pin title is `platform_data.pinterest.title` | | `media` | array | The ordered media list. Each item has `source_type` (`media_id`, `url`, or `platform_attachment_id`), `source`, and `type` for a `platform_attachment_id`. See [Upload media](/guides/media) | | `visibility` | string | `public`, `private`, `connections_only`, or `logged_in`. Which values apply depends on the platform | | `first_comment` | string | A comment SocialAPI posts after publishing. Instagram, Facebook, and YouTube read it. Every other platform ignores it, and Facebook Page Stories ignore it | | `scheduled_at` | string | The publish time (RFC 3339). See [Schedule posts](/guides/scheduling) | | `publish_now` | boolean | Publish immediately. Default `false` | | `stagger` | object | Publish the targets one after another with a random gap of `min_seconds` to `max_seconds` between them. Not allowed on a draft. See [Stagger targets](/guides/scheduling#stagger-targets) | | `skip_validation` | boolean | Publish without the pre-publish validation. Default `false`. The platform can still reject the post | | `skip_duplicate_check` | boolean | Skip the duplicate-content check. Default `false` | | `segments` | array | Continuation posts that publish as a thread on X, each with `text`. See [X publishing](/platforms/twitter/publishing) | | `platform_data` | object | Platform-specific fields, keyed by platform slug. See [Where platform data goes](#where-platform-data-goes) | | `targets` | array | The destinations. See below | Each item of `targets` has: | Field | Description | | - | - | | `account_id` | The connected account. Required | | `page_id` | The `sapi_page_` ID of the Facebook Page when the account has more than one | | `board_id` | The Pinterest board. Required on a Pinterest target. See [Pinterest boards](/platforms/pinterest/boards) | | `text`, `title`, `media`, `first_comment`, `visibility`, `scheduled_at` | Override the post-level value for this target. An empty `media` array means this target publishes no media | | `platform_data` | Platform-specific fields for this target | A target that does not set a field inherits the post-level value. The same account and Page twice in one request returns `400 publishing.duplicate_target`. SocialAPI ignores request fields it does not recognize. `POST /v1/posts` is not idempotent: a request returns `409 resource.conflict` when the same account has a post with the same title and text that was created in the last 24 hours and is now `published`. Send `skip_duplicate_check: true` to post the same content again. See [IDs](/guides/ids#create-requests-are-not-idempotent). ## Where platform data goes Put platform-specific fields in `platform_data`, in one of 2 places: * **On the post, under the platform slug:** `platform_data.instagram` applies to every Instagram target. * **On a target, as flat keys:** `targets[].platform_data` applies to that target only. When both set the same key, the target's value wins. A platform slug used as a key inside a target's `platform_data` is dropped, so write `{"reply_control": "everyone"}` on a target, not `{"threads": {...}}`. ```json theme={null} { "text": "New video is live.", "publish_now": true, "platform_data": { "youtube": {"tags": ["launch"], "category_id": "22"} }, "targets": [ {"account_id": "acc_01HZ9X9C4D5F6G7H8J1K2M3N", "platform_data": {"made_for_kids": false}} ] } ``` The keys each platform accepts are on its publishing page, for example [YouTube platform data fields](/platforms/youtube/publishing#platform-data-fields). ## Validate before you publish `POST /v1/posts/validate` checks a post without creating it and costs no credit. Name the platforms with `platforms`, `account_ids`, or `targets`. It accepts the same `text`, `media`, `scheduled_at`, `segments`, and `platform_data` fields as `POST /v1/posts`. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts/validate \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Spring sale starts today: https://example.com/spring", "account_ids": ["acc_01HZ9X8B3C4D5F6G7H9J2K1M"] }' ``` Response (HTTP 200): ```json theme={null} { "valid": true, "errors": [], "warnings": [] } ``` `errors` lists the violations that block publishing. `warnings` lists the rest, for example text within 10% of a platform's length limit. Each issue has `platform`, `field`, `message`, and, when it applies to one target, `target` and `segment_index`. A `scheduled_at` that is not in the future returns `400 validation.field_invalid`. Calling it without any platform returns `400 validation.field_required`. `POST /v1/posts` runs the same validation unless you send `skip_validation: true`. A post with errors returns `400 publishing.validation_failed`, and `error.meta.issues` holds the same issues, each with an extra `type` key set to `error`. Drafts skip validation until you publish them. `GET /v1/posts/validate` returns each platform's text and media rules, so a client can check content before it sends a request. Add `?platform=instagram` for one platform. An unknown platform returns `400 validation.field_invalid`. ```bash theme={null} curl "https://api.social-api.ai/v1/posts/validate?platform=instagram" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": { "instagram": { "text": {"max_length": 2200, "counting_mode": "chars", "max_hashtags": 30, "max_mentions": 20}, "media": { "supported_image_types": ["image/jpeg"], "max_image_size_bytes": 8388608, "supported_video_types": ["video/mp4", "video/quicktime"], "max_video_size_bytes": 314572800, "min_video_duration_sec": 3, "max_video_duration_sec": 900 }, "carousel": {"supported": true, "min_items": 2, "max_items": 10, "allow_video": true, "images_only": false, "photos_only": false}, "scheduling": {"supported": true, "mode": "server_side"}, "supports_platform_attachment_id": true, "supports_mixed_media": false } } } ``` The endpoint covers Instagram, Facebook, Threads, LinkedIn (`linkedin`, which covers member profiles and company Pages; `?platform=linkedin_page` returns `400 validation.field_invalid`), YouTube, TikTok, Google Business Profile, Bluesky, and X. `counting_mode` is `chars`, `utf16`, or `bytes`. TikTok's per-creator privacy levels and maximum duration come from `GET /v1/accounts/{id}/creator-info`. See [TikTok publishing](/platforms/tiktok/publishing). `supports_mixed_media` is about `platform_attachment_id` items only: `false` means a `platform_attachment_id` item must be the only item in `media` and cannot sit next to `url` or `media_id` items. It does not limit a carousel, which can mix images and videos where `carousel.allow_video` is `true` and `carousel.images_only` is `false`, as on Instagram. ## Drafts A post saved without `publish_now` or `scheduled_at` is a `draft`. It costs no credit, skips validation, and can have no targets. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "Draft of the spring announcement."}' ``` The response is HTTP 201 with `"status": "draft"`. Publish it later with `POST /v1/posts/{pid}/publish`, which costs 1 post credit. ## Edit a post before it publishes `PATCH /v1/posts/{pid}` changes a post that has not been delivered yet. Send only the fields you change. ```bash theme={null} curl -X PATCH https://api.social-api.ai/v1/posts/7c9e6679-7425-40de-944b-e07fc1f90ae7 \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "Spring sale starts tomorrow: https://example.com/spring"}' ``` The response is HTTP 200 with the updated post. Editing costs no credit. * **Editable statuses:** `draft`, `scheduled`, and `failed`. Any other status returns `409 post.state_invalid`, and `error.meta.state` is the current status. A `published` post cannot be edited on any platform, except a YouTube video, which has its own endpoint. See [YouTube publishing](/platforms/youtube/publishing). * **Editable fields:** `text`, `title`, `media`, `visibility`, `first_comment`, `scheduled_at`, `hidden`, `segments`, `platform_data`, and `targets`. Sending `targets` replaces all the post's targets. * **Validation:** an edit to content runs the validation again. It returns `400 publishing.validation_failed` for a post with errors, unless you send `skip_validation: true`. * **After a failure:** edit a `failed` post, then call `POST /v1/posts/{pid}/retry`. * **Event:** SocialAPI sends `post.updated` with `changed_fields`. ## Import posts from a CSV file `POST /v1/posts/import` creates posts from a CSV file sent as multipart form data in the `file` field. The first row is a header and must start with the `text` column. The file holds at most 500 data rows and 5 MB. | Column | Description | | - | - | | `text` | The post body. Required | | `title` | The post title | | `account_ids` | Connected account IDs separated by `\|`. Each ID is one target | | `scheduled_at` | RFC 3339 time. A row without it is saved as a draft | | `visibility` | The visibility | | `first_comment` | The first comment | | `platform_data` | A JSON object applied to every target of the row | | `board_id` | The Pinterest board, applied to every target of the row. Give Pinterest its own row | Columns are read by position, in this order. A row can end early: only `text` is required. There is no `publish_now` column, so an imported post is a draft or a scheduled post. ```bash theme={null} curl -X POST "https://api.social-api.ai/v1/posts/import?dry_run=true" \ -H "Authorization: Bearer $SOCAPI_KEY" \ -F "file=@posts.csv" ``` Response (HTTP 200): ```json theme={null} { "total_rows": 3, "created": 0, "failed": 1, "errors": [ {"row": 3, "message": "invalid date format for scheduled_at"} ], "posts": [] } ``` `dry_run=true` checks only each row's `text`, `scheduled_at`, and `platform_data` format, and creates nothing. Row numbers count the header as row 1, so the first data row is row 2. A request with no `file` field returns `400 validation.field_required`. A file over 5 MB, a file that is not valid CSV, a header that does not start with `text`, a file with no data rows, or a file with more than 500 data rows returns `400 validation.field_invalid`. When the file has more valid rows than you have post credits left, drafts included, the request returns `429 billing.post_limit` and creates nothing. Each created post is a normal post: it costs 1 post credit when scheduled and nothing as a draft. Without `dry_run`, a row that passes these checks but fails when SocialAPI creates the post (for example `409 resource.conflict` or `400 publishing.validation_failed`) is listed in `errors` with its row number and message and counted in `failed`. The other rows are still created, and the response is still 200: check `created`, `failed`, and `errors`. ## Unpublish and delete `POST /v1/posts/{pid}/unpublish` removes the published copies from the platforms and keeps the post. It returns 200 `{"success": true}` when every targeted copy was removed, `400 validation.field_invalid` when no target is `published` with a `platform_post_id`, `501 resource.not_supported` when at least one remaining target is on a platform that cannot delete posts (such as TikTok), and `500 system.internal` when a platform fails to delete a copy. With a `501` or `500`, every target that was removed is `unpublished`. See [Unpublish a post](/guides/delivery#unpublish-a-post). `DELETE /v1/posts/{pid}` deletes a post and also removes its published copies. See [Delete a post](/guides/delivery#delete-a-post). ## Platform support Each platform accepts different content, sizes, and `platform_data` keys. The cross-platform matrix is on [Platform support](/platforms/overview). The details are on each platform's publishing page: * [Instagram](/platforms/instagram/publishing) * [Facebook](/platforms/facebook/publishing) * [Threads](/platforms/threads/publishing) * [TikTok](/platforms/tiktok/publishing) * [YouTube](/platforms/youtube/publishing) * [LinkedIn](/platforms/linkedin/publishing) * [X / Twitter](/platforms/twitter/publishing) * [Pinterest](/platforms/pinterest/publishing) * [Google Business Profile](/platforms/google/publishing) * [Zalo](/platforms/zalo/publishing) * [Bluesky](/platforms/bluesky/publishing) Every platform accepts `scheduled_at`. A platform that cannot do something returns the error described on its page, for example `501 resource.not_supported`. ## Limits | Limit | Value | | - | - | | Post credits | 1 per published or scheduled post, whatever the number of targets. At the allowance, `POST /v1/posts` returns `429 validation.post_limit_exhausted` | | Posting speed per account | Per hour and per 24 hours, by platform. At the cap, an immediate publish returns `429 publishing.velocity_limit`. See [Rate limits](/guides/rate-limits#posting-velocity-caps) | | CSV import | 500 data rows and 5 MB per file. No `file` field returns `400 validation.field_required`. A file over 5 MB, over 500 rows, with a header that does not start with `text`, or with no data rows returns `400 validation.field_invalid` | ## Errors `400 publishing.validation_failed`, `409 resource.conflict`, `409 post.state_invalid`, and `429 publishing.velocity_limit` are the codes you branch on most. Every code is in [Errors](/guides/errors#publish-a-post). A failure after the request returned appears on the target, not as an HTTP error. A target on an account without the `posts.write` [permission](/guides/permissions) fails with `account.permission_missing`. A publish that would go out now on an account that needs [reconnecting](/guides/connect-accounts#reconnect-an-account) returns `403 account.reconnection_required` and publishes nothing. See [Track post delivery](/guides/delivery#target-errors). ## Scopes | Scope | Grants | | - | - | | `posts:read` | List and read posts | | `posts:write` | Create, edit, publish, retry, unpublish, validate (`POST /v1/posts/validate`), import, and delete posts | | `media:write` | Upload and verify the media a post uses | `GET /v1/posts/validate` needs no scope: any valid API key can call it. A brand-restricted key can publish only to accounts in its brands. See [Scoped API keys](/guides/scoped-keys). ## Events | Event | When it fires | | - | - | | `post.scheduled` | You create a scheduled post | | `post.updated` | You edit a post | | `post.published`, `post.partial`, `post.failed` | The post settles. Each fires once | | `post.retried` | You retry a post | | `post.unpublished`, `post.deleted` | You unpublish or delete a post | Payloads are in [Webhook events](/guides/webhook-events). ## Use from an AI agent | Tool | What it does | | - | - | | `social_api_create_post` | Creates, schedules, or publishes a post | | `social_api_update_post` | Edits a draft, scheduled, or failed post | | `social_api_publish_post` | Publishes a draft or scheduled post now | | `social_api_validate_post` | Checks a post without creating it | | `social_api_get_constraints` | Returns each platform's rules | The import endpoint has no tool. See [MCP tools](/integrations/tools). ## Related * [Track post delivery](/guides/delivery): statuses, HTTP codes, target errors, retry, and delete * [Schedule posts](/guides/scheduling): publish at a chosen time * [Upload media](/guides/media): upload files and attach media * [Platform support](/platforms/overview): what each platform publishes * [Errors](/guides/errors): every error code with its HTTP status # Rate limits Source: https://docs.social-api.ai/guides/rate-limits Stay within SocialAPI's request rate limit, posting velocity caps, and each platform's own limits, and handle HTTP 429 with Retry-After. SocialAPI applies two rate limits of its own: a [request rate limit](#request-rate-limit) per SocialAPI account, and a [posting velocity cap](#posting-velocity-caps) per publish target. Each platform also applies its own limits. All of them answer HTTP 429. Read the `Retry-After` header when it is present and wait that many seconds before you send the request again. These limits do not depend on your plan, and upgrading does not raise them. The request rate limit grows with your connected accounts. Post and interaction allowances per billing period are on [Plans and usage](/guides/plans). A 429 from SocialAPI's own limits on `/v1` uses the standard error envelope. `error.meta.retry_after_seconds` holds the same number of seconds as the `Retry-After` header: ```json theme={null} { "error": { "code": "publishing.velocity_limit", "message": "publishing too fast: this instagram account is limited to 25 posts per hour, retry in 1841s", "meta": { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "window": "hour", "limit": 25, "retry_after_seconds": 1841 } }, "request_id": "a1b2c3d4" } ``` ## Every 429 code | Code | What it limits | Retry-After | | - | - | - | | `ratelimit.exceeded` | [Requests per minute](#request-rate-limit) for your SocialAPI account | Yes | | `publishing.velocity_limit` | [Posts per hour and per 24 hours](#posting-velocity-caps) for one publish target | Yes | | `platform.{platform}.rate_limit` | The [platform's own limit](#platform-limits) | When the platform reports a wait | | `inbox.sync_rate_limited` | Repeated `POST /v1/inbox/sync` for the same target. See [Refresh the inbox manually](/guides/inbox#refresh-the-inbox-manually) | Yes | | `accounts.sync_rate_limited` | Repeated `POST /v1/accounts/{id}/sync` for the same connected account. See [Analytics](/guides/analytics) | Yes | | `validation.post_limit_exhausted`, `validation.interaction_limit_exhausted`, `billing.post_limit`, `billing.export_limit`, `billing.export_cooldown`, `billing.export_concurrent` | Plan allowances. See [Plans and usage](/guides/plans) | No | A request refused by `ratelimit.exceeded` or `publishing.velocity_limit` never reaches the platform and spends no credit. ## Request rate limit Every authenticated request to `/v1` and to the MCP server counts against one per-minute budget for your SocialAPI account. Every API key, dashboard session, and AI agent connection of the account shares that budget, so spreading requests over more API keys does not raise it. | Connected accounts on your SocialAPI account | Requests per minute | | - | - | | 0 to 2 | 60 | | 3 to 2,000 | 600 | | More than 2,000 | 1,200 | Authenticated responses can carry three headers: | Header | Description | | - | - | | `X-RateLimit-Limit` | Your budget in requests per minute | | `X-RateLimit-Remaining` | Requests left before your budget resets | | `X-RateLimit-Reset` | Whole seconds until your budget resets | A request to `/v1` past the budget returns `429 ratelimit.exceeded`, with `Retry-After` and `error.meta.retry_after_seconds` set to the seconds until your budget resets, and `error.meta.limit` set to your budget. On the MCP server, the 429 body is `{"error":"rate limit exceeded","code":"ratelimit.exceeded"}`, with the same `Retry-After` and `X-RateLimit-*` headers and no `meta`. A request rejected with `401` does not count and carries none of the three headers. Read the headers only when they are present. ## Posting velocity caps SocialAPI caps how fast one publish target can publish, on top of your plan's post allowance. A publish target is one connected account, or one Facebook Page or Pinterest board when the account publishes to more than one Page or board. Each Facebook Page of a connection has its own counters. | Platform | Per hour | Per 24 hours | | - | - | - | | Instagram | 25 | 45 | | Facebook | 25 | 100 | | Threads | 25 | 250 | | X / Twitter | 25 | 50 | | TikTok | 25 | 13 | | Pinterest | No hourly cap | 1,000 | | Every other platform | 25 | 50 | The hourly window opens with the first publish counted in it and lasts 1 hour. The 24-hour window opens the same way and lasts 24 hours. On TikTok, the 24-hour cap of 13 binds before the hourly cap of 25. On Instagram, SocialAPI's cap of 45 sits below Instagram's 50 posts per rolling 24 hours. On TikTok, SocialAPI's cap of 13 sits below TikTok's own limit of 15 posts per day per account. Reaching SocialAPI's cap gives you `publishing.velocity_limit` and a `Retry-After`, instead of a platform rejection after the attempt. TikTok also limits publishing to 6 posts per minute per account. SocialAPI does not enforce the 6 per minute: when you send a 7th post within a minute, TikTok refuses it and the target fails with a `platform.tiktok.*` code. ### What happens at a cap * **Immediate publish** (`POST /v1/posts` with `publish_now: true`, `POST /v1/posts/{pid}/publish`, `POST /v1/posts/{pid}/retry`): the request returns `429 publishing.velocity_limit` and creates nothing. `meta.window` is `hour` or `day`, `meta.limit` is the cap reached, and `meta.account_id` is the connected account. A refused request spends no post credit. * **More than one target in one request:** the request is all or nothing. If one target is at a cap, the whole request is refused and no target's counter moves. * **Scheduled post:** a scheduled post is never failed by a velocity cap. SocialAPI holds the target and publishes it once the window clears, so it goes out late. The wait does not count as a failed delivery. ## Platform limits Each platform limits what an app can do for an account. When the platform refuses a request for its own rate limit, SocialAPI returns `429 platform.{platform}.rate_limit`, for example `platform.instagram.rate_limit`. Match on the `platform.*.rate_limit` pattern to handle every platform. When the platform reports how long to wait, SocialAPI passes it on as `Retry-After` and `error.meta.retry_after_seconds`. Without them, wait at least 60 seconds, then retry with exponential backoff. | Platform | Limit | | - | - | | Instagram | 50 posts per rolling 24 hours per account | | Threads | 250 posts and 1,000 replies per rolling 24 hours per account | | TikTok | 15 posts per day and 6 per minute per account | | YouTube | A daily quota per Google Cloud project: 100 video uploads and 10,000 units for other calls, reset at midnight Pacific Time | | Google Business Profile | 10 location updates per minute per Business Profile | | App Store Connect | An hourly request limit per App Store Connect API key, set by Apple: see [App Store Connect](/platforms/app-store-connect#limits) | | Facebook, X / Twitter | No single fixed number: the platform limits each app and each account, and reports usage in its response headers | ### Read a platform's remaining quota `GET /v1/accounts/{id}/limits` returns the quota the platform reports for one connected account, as an object of numbers. It needs the `accounts:read` scope and costs no credits. ```bash theme={null} curl https://api.social-api.ai/v1/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/limits \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200) for an Instagram account: ```json theme={null} { "posts_remaining": 47 } ``` | Platform | Keys | | - | - | | Instagram | `posts_remaining`: posts left in Instagram's rolling 24 hours | | Threads | `posts_remaining`, `posts_total`, `replies_remaining`, `replies_total` | | YouTube | `daily_quota_units`, `daily_upload_quota_calls`, and the unit cost of each operation (keys ending in `_cost` or `_cost_per_page`) | | WhatsApp | `quality_rating`: `3` high, `2` medium, `1` low, `0` unknown. `messaging_tier_cap`: unique recipients allowed per 24 hours, `-1` for unlimited | | Bluesky | `write_points_per_hour` (5,000) and `write_points_per_day` (35,000) | | X / Twitter | An empty object | Zalo no longer reports its message quota: do not rely on `remain` and `total`. Every platform that is neither in the table nor Zalo returns `501 resource.not_supported`. ### Reads and platform limits Reading comments, reviews, mentions, and posts costs no credits. Reads count against the [request rate limit](#request-rate-limit) like every other request. The two sync endpoints in the table above have their own cooldowns. Reads still count against the platform's limit for the account. To detect new comments and messages, subscribe to [webhook events](/guides/webhook-events) instead of polling. If your interface fetches the same page of results repeatedly, cache it on your side. ## Handling 429 **1. Read Retry-After** When the response has a `Retry-After` header, wait that many seconds. The same value is in `error.meta.retry_after_seconds`. **2. Back off when there is no Retry-After** For `platform.*.rate_limit` without `Retry-After`, wait 60 seconds, then double the wait on each further 429. **3. Do not retry plan allowances** `validation.post_limit_exhausted`, `validation.interaction_limit_exhausted`, and the `billing.*` codes do not clear within minutes. Check `GET /v1/usage` and wait for `period_end`, or change your plan. ```javascript theme={null} async function callWithRetry(url, options, maxAttempts = 4) { let waitSeconds = 60; for (let attempt = 1; attempt <= maxAttempts; attempt++) { const resp = await fetch(url, options); if (resp.status !== 429) return resp; const body = await resp.json(); const code = body.error.code; if (code.startsWith("validation.") || code.startsWith("billing.")) { return resp; // an allowance or daily cap: retrying does not help } const retryAfter = Number(resp.headers.get("Retry-After")); const delay = retryAfter > 0 ? retryAfter : waitSeconds; await new Promise((resolve) => setTimeout(resolve, delay * 1000)); waitSeconds *= 2; } throw new Error("Still rate limited after " + maxAttempts + " attempts"); } ``` ## Use from an AI agent | Tool | What it does | | - | - | | `social_api_get_account_limits` | Returns the same keys as `GET /v1/accounts/{id}/limits` | ## Related * [Plans and usage](/guides/plans): post, interaction, storage, and export allowances per plan * [Errors](/guides/errors): every error code with its HTTP status * [Track post delivery](/guides/delivery): what a refused or held publish looks like on the post * [Scheduling](/guides/scheduling): how scheduled posts go out * [Webhook events](/guides/webhook-events): receive new comments and messages instead of polling # Reviews Source: https://docs.social-api.ai/guides/reviews List the reviews of your connected business locations and apps, read their rating summaries, and post the business's public reply through one reviews API. SocialAPI's reviews API lists the reviews left on a connected business location or app, reports its rating summary, and posts the business's public reply. Google Business Profile and App Store Connect have reviews. A call for an account on any other platform returns `422 account.platform_unsupported`. A reply costs 1 interaction credit, and reads cost nothing. Google moderates every reply: a reply is accepted first and goes live only after Google approves it. An App Store Connect reply returns no `state`, and can take up to 24 hours to appear on the App Store. ## Endpoints | Method | Path | Description | | - | - | - | | `GET` | `/v1/inbox/reviews` | List your review-capable accounts, each with its rating summary. Returns no reviews | | `GET` | `/v1/inbox/reviews/{account_id}` | List the reviews of one account, with that account's summary | | `POST` | `/v1/inbox/reviews/{id}/reply` | Post the business's public reply to a review | Reviews cannot be created or deleted through the API, and a reply cannot be edited or deleted. To change a reply, send a new one: a review has 1 business reply, and a new reply replaces it. ## The two list endpoints The two `GET` endpoints answer different questions. * **`GET /v1/inbox/reviews`** returns 1 row per review-capable account: its name, average rating, review count, and the time of its newest review. Use it to build a list of locations and apps. Add `platform=google` or `platform=app_store_connect` to filter. * **`GET /v1/inbox/reviews/{account_id}`** returns the reviews of the 1 account you name. Use it to read, sort, and page through them. ```bash theme={null} curl https://api.social-api.ai/v1/inbox/reviews \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "name": "Joe's Pizza (Downtown)", "platform": "google", "average_rating": 4.2, "total_count": 87, "last_review_at": "2026-10-12T02:24:05Z", "status": "ok" } ] } ``` `status` is `ok`, `reconnection_required` (the account needs [reconnecting](/guides/connect-accounts#reconnect-an-account)), or `error` (the platform refused the request or did not answer in time). The rating fields and `last_review_at` are `null` unless `status` is `ok`. A failing account never fails the whole list. ## List the reviews of an account ```bash theme={null} curl "https://api.social-api.ai/v1/inbox/reviews/acc_01HZ9X3Q4R5M6N7P8V2K0W1J?limit=20&order=newest" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` | Parameter | Description | | - | - | | `limit` | 1 to 50 reviews per page. The default is 20. A value above 50 is treated as 50, and a value below 1 returns `400 validation.field_invalid` | | `cursor` | The `next_cursor` of the previous page | | `order` | `newest` (default), `rating_asc`, or `rating_desc`. Another value returns `400 validation.field_invalid` | Response (HTTP 200): ```json theme={null} { "data": [ { "id": "sapi_rev_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "rating": 3, "text": "Great pizza, slow service.", "author": { "name": "Jane Smith", "avatar_url": "https://lh3.googleusercontent.com/a/abc" }, "reply": null, "created_at": "2026-10-12T18:24:31Z", "updated_at": "2026-10-12T18:24:31Z" } ], "summary": { "average_rating": 4.2, "total_count": 87 }, "pagination": { "has_more": true, "next_cursor": "CgwI9ZyPyQYQ6IiYuQE" } } ``` `summary` covers the whole location or app, not the page. Every field of a review, including `media` and `reply_url`, is in [Google Business Profile reviews](/platforms/google/reviews#list-the-reviews-of-a-location). App Store Connect reviews also carry `title`, the reviewer's headline, and report an `average_rating` of `0`: see [App Store Connect reviews](/platforms/app-store-connect#reviews). Paging follows [Pagination](/guides/pagination#paginated-endpoints). ## Reply to a review ```bash theme={null} curl -X POST https://api.social-api.ai/v1/inbox/reviews/sapi_rev_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I/reply \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "text": "Thank you for your review, Jane!" }' ``` Response (HTTP 200): ```json theme={null} { "success": true, "reply_id": "sapi_rev_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "state": "PENDING" } ``` `{id}` is the review's `sapi_rev_` ID or the platform's review ID. `account_id` and `text` are required: a missing one returns `400 validation.field_required`. `state` is the reply's moderation state on Google. App Store Connect returns no `state`. > [!WARNING] > > **On Google Business Profile, a successful reply is not yet published.** `success: true` means Google accepted the reply for review. Nobody can see it until its `state` is `APPROVED`. On App Store Connect, the reply returns no `state` and can take up to 24 hours to appear on the App Store. ## Moderation of replies Google moderates replies, so a reply moves through 3 states. | `state` | Meaning | | - | - | | `PENDING` | Submitted and awaiting Google's review. A new reply returns this | | `APPROVED` | Published and visible | | `REJECTED` | Never published. `reply.policy_violation` holds Google's reason | SocialAPI sends a [`review.updated`](/guides/webhook-events#review-updated) event when Google decides, with `change` set to `reply_approved` or `reply_rejected`. You can also read the review again and check `reply.state`. After a rejection, send a new reply that passes Google's content rules. [Google Business Profile reviews](/platforms/google/reviews#replies-are-moderated) explains them. ## Platform support Google Business Profile and App Store Connect have reviews. SocialAPI does not read Facebook reviews (Meta deprecated Page recommendations in Graph API v22.0), Instagram has none, and Trustpilot is not available in SocialAPI: see [Trustpilot](/platforms/trustpilot). The [capability matrix](/platforms/overview) shows every platform. Google's connect rules, location data, and review limits are in [Google Business Profile reviews](/platforms/google/reviews), and App Store Connect's in [App Store Connect](/platforms/app-store-connect#reviews). ## Limits | Limit | Value | | - | - | | Reviews per page | Default 20, up to 50 | | Replies per review | 1. A new reply replaces the old one | | Reply text | 4,096 bytes on Google. No SocialAPI limit on App Store Connect: see [App Store Connect limits](/platforms/app-store-connect#limits). The MCP tool accepts 2,200 bytes | | Cost of a reply | 1 interaction credit | ## Errors and debugging Check these in order when a review call fails: 1. **`401`, `403 auth.insufficient_scope`.** Your SocialAPI API key is invalid, or a scoped key lacks the scope in [Scopes](#scopes). 2. **`401 platform.app_store_connect.unauthorized`, `403 platform.app_store_connect.forbidden`, `429 platform.app_store_connect.rate_limit`.** App Store Connect rejected the App Store Connect API key, refused its role, or the key's hourly limit is used up. See [App Store Connect review errors](/platforms/app-store-connect#review-errors). 3. **`404 account.not_found`, `403 account.reconnection_required`.** The `acc_` ID is not yours, or the account needs reconnecting. 4. **`403 account.permission_missing`.** The account lacks the [permission](/guides/permissions) the call needs: `reviews.read` to list the reviews of an account, and `reviews.write` to reply. `error.meta.permission` names it. 5. **`422 account.platform_unsupported`.** The account's platform has no reviews. 6. **`400 validation.id_invalid`, `400 validation.id_platform_mismatch`.** The review ID is not a review ID, or it belongs to another platform. 7. **`400 validation.field_required`, `400 validation.field_invalid`.** `account_id` or `text` is missing, or `limit` or `order` is wrong. 8. **`429 validation.interaction_limit_exhausted`.** The plan's interaction allowance is used up. The full catalog is in [Errors](/guides/errors#inbox-comments-messages-and-reviews). ## Scopes | Scope | Operations | | - | - | | `inbox:read` | List accounts with reviews, list reviews | | `inbox:write` | Reply to a review | ## Events SocialAPI sends [`review.received`](/guides/webhook-events#review-received) when a customer posts a new review and [`review.updated`](/guides/webhook-events#review-updated) when a review changes or Google decides on a reply. Both are for Google Business Profile, and an account without the `reviews.read` [permission](/guides/permissions) receives neither. Google can miss a notification and does not replay it, so read `GET /v1/inbox/reviews/{account_id}` on a schedule to fill any gap. ## Use from an AI agent | Tool | What it does | | - | - | | `social_api_list_review_accounts` | List review-capable accounts with their rating summaries | | `social_api_get_reviews` | List the reviews of one account | | `social_api_reply_to_review` | Reply to a review. On Google Business Profile and App Store Connect, a new reply replaces the old one | SocialAPI has no endpoint to delete a business reply. See [MCP tools](/integrations/tools#reviews). ## Related * [Google Business Profile reviews](/platforms/google/reviews): review fields, reply moderation, and review events. * [App Store Connect](/platforms/app-store-connect#reviews): review titles, replies, and limits for App Store reviews. * [Webhook events](/guides/webhook-events#reviews): the `review.received` and `review.updated` payloads. * [Comments](/guides/comments): Google Business Profile has no comments, so use this guide for review replies. * [Plans and usage](/guides/plans#what-counts-as-a-post-or-interaction): what costs an interaction credit. * [Account permissions](/guides/permissions): the permissions an account needs to read and answer reviews. * [Unified inbox](/guides/inbox): the other inbox areas and manual sync. # Schedule posts Source: https://docs.social-api.ai/guides/scheduling Schedule a post to publish at a future time on every platform, edit or cancel it, and learn how posting caps and failures affect a scheduled post. Scheduling publishes a post at a time you choose. Send `scheduled_at` with `POST /v1/posts`, and SocialAPI holds the post and sends it to each target when the time arrives. Every platform accepts `scheduled_at`: SocialAPI does the waiting, not the platform. A scheduled post costs 1 post credit when you create it, whatever the number of targets. Editing and cancelling cost nothing. See [Plans and usage](/guides/plans#what-counts-as-a-post-or-interaction). ## Endpoints | Method | Path | Description | | - | - | - | | `POST` | `/v1/posts` | Schedule a post with `scheduled_at` | | `GET` | `/v1/posts?status=scheduled` | List scheduled posts | | `PATCH` | `/v1/posts/{pid}` | Change the time, content, or targets of a scheduled post | | `POST` | `/v1/posts/{pid}/publish` | Publish a scheduled post now | | `DELETE` | `/v1/posts/{pid}` | Cancel a scheduled post | ## Schedule a post Set `scheduled_at` to an RFC 3339 time in the future and name at least one target: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Launching today: https://example.com/launch", "scheduled_at": "2026-10-20T09:00:00Z", "targets": [ {"account_id": "acc_01HZ9X8B3C4D5F6G7H9J2K1M"}, {"account_id": "acc_01HZ9X4T6B8C2D5F7G9H1J3K", "page_id": "sapi_page_01HZ9X5V7C9D3E6G8H1J4K2M"} ] }' ``` Response (HTTP 201): ```json theme={null} { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "text": "Launching today: https://example.com/launch", "status": "scheduled", "scheduled_at": "2026-10-20T09:00:00Z", "hidden": false, "retry_count": 0, "created_at": "2026-10-13T09:00:00Z", "updated_at": "2026-10-13T09:00:00Z", "targets": [ { "account_id": "acc_01HZ9X8B3C4D5F6G7H9J2K1M", "platform": "threads", "status": "pending", "scheduled_at": "2026-10-20T09:00:00Z" }, { "account_id": "acc_01HZ9X4T6B8C2D5F7G9H1J3K", "page_id": "sapi_page_01HZ9X5V7C9D3E6G8H1J4K2M", "platform": "facebook", "status": "pending", "scheduled_at": "2026-10-20T09:00:00Z" } ] } ``` The post is `scheduled` and each target is `pending`. SocialAPI sends `post.scheduled` when the post is created. * **Time:** with at least one target, a `scheduled_at` in the past returns `400 validation.field_invalid`. With no targets, the post is saved as a draft. A value that is not RFC 3339 returns `400 validation.field_invalid`. Include the UTC offset or `Z`. * **One time per target:** a target can set its own `scheduled_at`, which overrides the post's. The post's `scheduled_at` is the earliest of them. * **Without targets:** a post with no targets is saved as a draft, even with `scheduled_at`. * **Both flags:** when you send `publish_now` and `scheduled_at` together, the post is scheduled. * **Validation:** SocialAPI validates the post when you schedule it, so a violation returns `400 publishing.validation_failed` immediately, and validates the post again when its time arrives. * **Allowance:** at the post allowance, scheduling returns `429 validation.post_limit_exhausted` and creates nothing. ## Stagger targets Add `stagger` to publish the targets one after another, with a random gap between them, instead of all at the same time. Set `min_seconds` and `max_seconds` in whole seconds: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Launching today: https://example.com/launch", "scheduled_at": "2026-10-20T10:00:00Z", "stagger": {"min_seconds": 60, "max_seconds": 300}, "targets": [ {"account_id": "acc_01HZ9X8B3C4D5F6G7H9J2K1M"}, {"account_id": "acc_01HZ9X4T6B8C2D5F7G9H1J3K"}, {"account_id": "acc_01HZ9X6W8D1E4F7H9J2K5L3N"}, {"account_id": "acc_01HZ9X7Y9F2G5H8J1K4M6P4Q"} ] }' ``` The first target publishes at `scheduled_at`. Each next target publishes a random delay between `min_seconds` and `max_seconds` after the previous one, in the order the targets are listed. With 4 targets and a 60 to 300 second gap, the times are, for example, 10:00:00, 10:02:41, 10:03:52, and 10:08:30. Read each target's planned time from `targets[].scheduled_at` with `GET /v1/posts/{pid}`. * **Immediate start:** with `publish_now: true` and no `scheduled_at`, the first target publishes now and the others follow. The response still has `status: "scheduled"`, because the later targets are still waiting. * **Fixed gap:** set `min_seconds` equal to `max_seconds`. * **Explicit target times:** a target that sets its own `scheduled_at` keeps that time and is left out of the sequence. It does not move the targets after it. * **Required:** a post that would be saved as a draft (no `scheduled_at` and no `publish_now`) cannot use `stagger`. It returns `400 validation.field_invalid` with `meta.field` set to `stagger`. * **Bounds:** `0 <= min_seconds <= max_seconds <= 86400`. A value outside them returns `400 validation.field_invalid`. * **Cost:** 1 post credit for the whole post. * **Results:** the post settles through the usual `post.published`, `post.partial`, and `post.failed` events. * **Rescheduling:** `PATCH /v1/posts/{pid}` with a post-level `scheduled_at` moves every target to that single time and removes the spread. To keep a spread, send a `scheduled_at` on each target. * **Where it applies:** `stagger` applies only to `POST /v1/posts`. `PATCH /v1/posts/{pid}`, `POST /v1/posts/{pid}/publish`, and `POST /v1/posts/validate` ignore it, and CSV import has no column for it. ## How a scheduled post moves through statuses ```mermaid theme={null} flowchart LR A[POST /posts with scheduled_at] --> B[scheduled] B -->|PATCH scheduled_at| B B -->|DELETE| D[deleted] B -->|scheduled time reached| C[targets publishing] B -->|POST /posts/pid/publish| C C --> E[published, partial, or failed] E -->|failed or partial: POST /posts/pid/retry| C ``` A scheduled post keeps the status `scheduled` while it waits for its time and while SocialAPI sends it, until every target has settled. While SocialAPI sends it, each target shows `publishing`. The post then becomes `published`, `partial`, or `failed`, and SocialAPI sends one `post.published`, `post.partial`, or `post.failed` event. The statuses and the per-target fields are in [Track post delivery](/guides/delivery#post-status). ## Edit or reschedule `PATCH /v1/posts/{pid}` changes a `scheduled` post. A new time or target list takes effect at once. ```bash theme={null} curl -X PATCH https://api.social-api.ai/v1/posts/7c9e6679-7425-40de-944b-e07fc1f90ae7 \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{"scheduled_at": "2026-10-21T14:00:00Z"}' ``` The response is HTTP 200 with the updated post. SocialAPI sends `post.updated` with `changed_fields`. An edit to the content or targets runs validation again. A post in any status other than `draft`, `scheduled`, or `failed` returns `409 post.state_invalid`. [Publish a post](/guides/publishing#edit-a-post-before-it-publishes) lists every editable field. ## Publish now `POST /v1/posts/{pid}/publish` sends a scheduled post immediately. It costs 1 post credit again, on top of the credit the scheduling cost. The response is HTTP 201 and the post is `publishing`. A post that is `published`, `partial`, or `failed` returns `409 post.state_invalid`. ## Cancel a scheduled post `DELETE /v1/posts/{pid}` cancels a `scheduled` post: SocialAPI removes the post and sends nothing. The response is HTTP 200 with `{"success": true, "deleted": true}`, and SocialAPI sends `post.deleted`. The post credit is not returned. Deleting a post that is already `publishing` returns `409 post.state_invalid`. See [Delete a post](/guides/delivery#delete-a-post). ## Posting caps can delay a scheduled post SocialAPI caps how many posts one account publishes per hour and per 24 hours, by platform. A scheduled post that reaches a cap is not failed. The target stays `pending`, and SocialAPI publishes it when the cap window clears, so the post goes out later than `scheduled_at`. The caps and their values are in [Rate limits](/guides/rate-limits#posting-velocity-caps). Immediate publishes that reach a cap return `429 publishing.velocity_limit` instead. ## When a scheduled post fails SocialAPI makes one delivery attempt per target at the scheduled time. A failed attempt is final: the target becomes `failed` with its `error`, the post becomes `failed` or `partial`, and SocialAPI sends one `post.failed` or `post.partial` event. Nothing is re-sent automatically. A scheduled post's credit is not returned when it fails. Common causes are a disconnected account (`publishing.delivery.account_load_failed`), an account that lost the `posts.write` [permission](/guides/permissions) (`account.permission_missing`), content that stopped passing validation (`validation.publish`), and a platform refusal (`platform.{platform}.*`). The causes are in [Track post delivery](/guides/delivery#target-errors). To send a failed post again, fix the cause, optionally edit the post, and call `POST /v1/posts/{pid}/retry`. It re-sends only the failed targets and costs 1 post credit. A scheduled post still publishes when your SocialAPI account is past due. See [Past-due accounts](/guides/plans#past-due-accounts). ## YouTube has 2 schedulers On YouTube, `scheduled_at` makes SocialAPI upload the video at that time, and `platform_data.youtube.publish_at` uploads it now and lets YouTube make it public later. See [Schedule a YouTube post](/platforms/youtube/publishing#schedule-a-post). ## Read scheduling support from the API `GET /v1/posts/validate` returns `scheduling.supported` for each platform it covers. It is `true` for each of them. See [Publish a post](/guides/publishing#validate-before-you-publish). ## Scopes | Scope | Grants | | - | - | | `posts:read` | List scheduled posts | | `posts:write` | Schedule, edit, publish now, and cancel posts | ## Use from an AI agent | Tool | What it does | | - | - | | `social_api_create_post` | Schedules a post with `scheduled_at`, optionally spreading the targets with `stagger` | | `social_api_list_posts` | Lists posts, including `status=scheduled` | | `social_api_update_post` | Changes the time or content of a scheduled post | | `social_api_publish_post` | Publishes a scheduled post now | | `social_api_delete_post` | Cancels a scheduled post | ## Related * [Publish a post](/guides/publishing): fields every platform shares * [Track post delivery](/guides/delivery): post statuses, target errors, and retry * [Rate limits](/guides/rate-limits): the posting caps that hold a scheduled post * [Plans and usage](/guides/plans): what scheduling costs * [Webhook events](/guides/webhook-events): `post.scheduled` and the settle events # Scoped API keys Source: https://docs.social-api.ai/guides/scoped-keys Restrict a SocialAPI key to specific scopes and brands for least-privilege automations, AI agents, and agency client isolation. A scoped API key is a SocialAPI API key restricted to some actions (its scopes), some brands, or both. A key with no restriction has full access: every action on every [brand](/guides/concepts) of your SocialAPI account. Two uses are common. A bot that only answers comments holds a key with `inbox:read` and `inbox:write`, and nothing else. An agency gives each client's automation a key limited to that client's brand, so a leaked key cannot reach another client. | Restriction | Field | Controls | | - | - | - | | Scopes | `scopes` | Which actions the key can perform | | Brands | `brand_ids` | Which brands' accounts, posts, conversations, and Automations the key can see and change | > [!NOTE] > > An empty `scopes` array grants every scope, and an empty `brand_ids` array grants every brand. A key is restricted when at least one of the two arrays holds a value. ## Create a scoped key Pass `scopes`, `brand_ids`, or both to `POST /v1/keys`: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/keys \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme client bot", "scopes": ["inbox:read", "inbox:write", "dms:send"], "brand_ids": ["1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f"] }' ``` Response (HTTP 201): ```json theme={null} { "id": "5b0e6f3a-8c2d-4e1f-9a7b-3c4d5e6f7a8b", "name": "Acme client bot", "raw_key": "sapi_key_cd27569b0823266d6431e5431c09fd746edd48faa40aeb43e5e7ccd9a34fee", "message": "Store this key securely. It will not be shown again.", "scopes": ["inbox:read", "inbox:write", "dms:send"], "brand_ids": ["1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f"] } ``` `raw_key` is shown once. Creating keys needs full access (a full-access key, an OAuth access token, or the dashboard), so a scoped key can never create a key with more access than it has. * An unknown scope returns `400 validation.scope_invalid`, and the message names it. * A brand ID that does not exist, is not a UUID, or belongs to another SocialAPI account returns `400 validation.brand_invalid`. ## Scopes Scopes have the form `resource:action`. A scope-restricted key can perform an operation only when it holds the operation's scope. | Scope | Grants | | - | - | | `accounts:read` | List brands and connected accounts. Read account Pages, creator info, usage limits, Pinterest boards and Pins, account and brand analytics, platform account summaries, Google Business Profile media, insights, and search keywords, and the Pages, locations, or profiles of a Facebook, Google, or LinkedIn login | | `accounts:manage` | Connect, reconnect, and disconnect accounts, change an account's [permissions](/guides/permissions), finish pending connections, assign Facebook Pages, Google locations, and LinkedIn member profiles and company Pages, change account Page settings, re-sync an account's posts, edit a Google Business Profile and its media, and create, edit, and delete Pinterest boards | | `posts:read` | List and read posts, their metrics and analytics, and Instagram collaborators | | `posts:write` | Create, edit, publish, retry, unpublish, validate (`POST /v1/posts/validate`), and delete posts, edit YouTube video details, and accept or decline Instagram collab invites | | `media:read` | List the media library | | `media:write` | Get an upload URL, upload, verify, and delete media | | `inbox:read` | Read comments and their replies, reviews, mentions, and Instagram stories | | `inbox:write` | Reply to comments, reviews, and mentions, send private replies, hide or unhide comments, delete comments, like or unlike comments, and moderate or edit YouTube comments | | `dms:read` | Read DM conversations and messages, read Instagram participant profiles, and start or check an inbox sync | | `dms:send` | Send DMs and add or remove a reaction on a message | | `automations:read` | Read [Automations](/guides/automations) and their runs | | `automations:write` | Create, edit, pause, resume, and delete Automations. Each action also needs its own scope, see [below](#automations-need-their-actions-scopes) | | `webhooks:manage` | Manage webhook endpoints, read and retry deliveries, and send test events. Refused to brand-restricted keys | | `analytics:read` | Read the events log and its summary, create and read exports, read LinkedIn insights (company Page and member profile) and Threads insights, and look up Threads profiles. Refused to brand-restricted keys | `GET /v1/keys/scopes` returns this catalog with a `group` and a `description` per scope, so your app can render a scope picker from it: ```bash theme={null} curl https://api.social-api.ai/v1/keys/scopes \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200, first entry shown): ```json theme={null} { "data": [ { "name": "accounts:read", "description": "List brands, connected accounts, and their metadata", "group": "Accounts" } ], "count": 14 } ``` ### Automations need their actions' scopes An [Automation](/guides/automations) carries out its actions long after the request that configured it. A key can configure only actions it can perform itself: besides `automations:write`, it needs the scope of each action. | Action | Also needs | | - | - | | `hide_comment`, `reply_comment`, `private_reply` | `inbox:write` | | `send_dm` | `dms:send` | A key that holds only `automations:write` and creates an Automation with a `send_dm` action gets `403 auth.insufficient_scope` with `meta.required_scope` set to `dms:send`. The check runs on every change that can make the Automation send more: creating it, resuming it, or changing its `steps`, `post_ids`, or `once_per_person`. Renaming, pausing, and deleting need only `automations:write`, so a key can always stop an Automation it can see. ### Always available Any valid key can call these endpoints, whatever its scopes and brands: * `GET /v1/users/me` * `GET /v1/usage` * `GET /v1/billing/plans` * `GET /v1/media/storage` * `GET /v1/posts/validate` * `GET /v1/keys/scopes` * `GET /v1/webhooks/events` * `GET /v1/announcements` and `POST /v1/announcements/{id}/seen` ### Full-access operations A restricted key can never perform these operations. They need full access: a full-access key, an OAuth access token, or the dashboard. * Managing API keys: list, create, edit, rotate, and revoke, including the key itself * Editing or deleting your SocialAPI account (`PATCH` and `DELETE /v1/users/me`) * Creating, editing, or deleting brands. Listing brands needs `accounts:read` * Listing, creating, and revoking invites A key that can create keys can create itself an unrestricted one, so every key operation needs full access. > [!WARNING] > > A restricted key cannot rotate or revoke itself. Rotate or revoke a leaked scoped key with a full-access key or from the dashboard. ## Brand restrictions A brand-restricted key behaves as if its brands are the only brands in your SocialAPI account. It never learns that other resources exist: * **Reading** an account, brand, or other resource outside the key's brands returns the same `404` as a resource that does not exist, for example `404 account.not_found` or `404 brand.not_found`. * **Lists** (accounts, brands, posts, conversations, comments, mentions, Automations) return only resources inside the key's brands. `GET /v1/brands` returns only the key's brands. An Automation belongs to the brand of its account. * **Connecting** an account needs a `brand_id` from the key's list. A connection without `brand_id` creates a new brand that the key cannot see. See [Connect a social account](/guides/connect-accounts). ### Posts A post belongs to the brands of its target accounts: * **Read.** A post is visible when at least one of its target accounts is in the key's brands. A draft with no target is not visible to a brand-restricted key. A post the key cannot see returns `404 resource.not_found`. * **Change** (edit, publish, retry, unpublish, delete). Every target account must be in the key's brands. Otherwise the post returns `404 resource.not_found`. * **Create or retarget.** Every account in the request must be in the key's brands. Otherwise the request returns `404 account.not_found`. ### Media is shared `media:read` and `media:write` work on the whole media library, whatever the key's brands. A file in the library reaches a platform only when a post uses it, and creating or changing a post is limited to the key's brands. ### Webhooks and analytics need every brand Webhook endpoints, their deliveries, the events log, and exports cover every brand of your SocialAPI account. A brand-restricted key is refused `webhooks:manage` and `analytics:read` operations even when it holds the scope, with `403 auth.insufficient_scope`. A key restricted only by scopes can hold and use them. ### Deleting a brand never widens a key When you delete a brand that a key lists in `brand_ids`, its ID stays in the array and matches nothing. The key does not become unrestricted, even when that brand was its only one. ## Read a key's restrictions `GET /v1/keys` returns `scopes` and `brand_ids` on every key. Both arrays are always present, and two empty arrays mean full access: ```json theme={null} { "id": "5b0e6f3a-8c2d-4e1f-9a7b-3c4d5e6f7a8b", "name": "Acme client bot", "preview": "sapi_key_cd275...", "is_active": true, "last_used_at": "2026-10-12T16:04:00Z", "created_at": "2026-09-30T08:15:00Z", "scopes": ["inbox:read", "inbox:write", "dms:send"], "brand_ids": ["1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f"] } ``` ## Change the restrictions on a key `PATCH /v1/keys/{id}` replaces the key's `name`, `scopes`, and `brand_ids` together. The secret does not change, so services using the key keep working: ```bash theme={null} curl -X PATCH https://api.social-api.ai/v1/keys/5b0e6f3a-8c2d-4e1f-9a7b-3c4d5e6f7a8b \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme client bot", "scopes": ["inbox:read", "inbox:write"], "brand_ids": ["1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f"] }' ``` Response (HTTP 200): ```json theme={null} { "id": "5b0e6f3a-8c2d-4e1f-9a7b-3c4d5e6f7a8b", "name": "Acme client bot", "scopes": ["inbox:read", "inbox:write"], "brand_ids": ["1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f"] } ``` > [!WARNING] > > Send all three fields. `name` is required, or the request returns `400 validation.field_required`. A `scopes` or `brand_ids` you leave out is saved as empty, which removes that restriction. Editing a key needs full access. Editing a revoked key, or an ID that is not one of your keys, returns `404 resource.not_found`. To issue a new secret with the same restrictions, rotate the key: see [Authentication](/guides/authentication). ## Errors A restricted key that is refused gets `403 auth.insufficient_scope`. `meta.required_scope` tells you what is missing: * **The key lacks the scope.** `meta.required_scope` names the missing scope. * **A `webhooks:manage` or `analytics:read` operation with a brand-restricted key.** `meta.required_scope` names that scope. Brand-restricted keys are refused it even when they hold it. * **A full-access operation.** `meta.required_scope` is absent. * **An operation no scope covers.** `meta.required_scope` is absent. ```json theme={null} { "error": { "code": "auth.insufficient_scope", "message": "This API key does not have the required scope: posts:write", "meta": { "required_scope": "posts:write" } }, "request_id": "a1b2c3d4" } ``` A resource outside the key's brands returns its ordinary `404`, never a `403`. Full-access keys and [OAuth 2.1 access tokens](/integrations/oauth) are never restricted. ## Related * [Authentication](/guides/authentication) for creating, rotating, and revoking keys * [Connect a social account](/guides/connect-accounts) for connecting accounts with a brand-restricted key * [Automations](/guides/automations) for the scopes each action needs * [Account permissions](/guides/permissions) for limiting what a connected account can do, which differs from a key's scopes * [The MCP server](/integrations/overview) for giving an AI agent a scoped key * [Errors](/guides/errors) for every `auth.*` code # Use cases Source: https://docs.social-api.ai/guides/use-cases See how developers and AI agents use the SocialAPI REST API and MCP server for an inbox, comment-to-DM Automations, publishing, scheduling, and agents. SocialAPI is a REST API and MCP server for the comments, direct messages (DMs), mentions, reviews, and posts of your connected social accounts. These are 5 common ways to use it, each with the guide that has the details. ## Read and answer an inbox Read the comments, DMs, mentions, and reviews of every connected account through one set of endpoints, and answer them. Each area has its own guide: [Comments](/guides/comments), [Direct messages](/guides/direct-messages), [Reviews](/guides/reviews), and [Mentions](/guides/mentions). ```bash theme={null} curl https://api.social-api.ai/v1/inbox/conversations \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Start with [Unified inbox](/guides/inbox). Receive new items as they arrive with [Webhooks](/guides/webhooks). ## Answer comments and DMs automatically An Automation answers Instagram and Facebook comments and DMs that match keywords: a private reply with a link, a public reply, a hidden comment, or a button menu. SocialAPI runs it, so you run no server. On Instagram, a Follow gate sends the link only to people who follow the account. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/automations \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "name": "Send the link", "trigger": "comment", "steps": [{ "key": "start", "filters": [{ "type": "keyword", "keyword": { "keywords": ["link"], "match": "contains" } }], "actions": [{ "type": "private_reply", "private_reply": { "texts": ["Here is the link: https://example.com"] } }] }] }' ``` See [Automations](/guides/automations). ## Publish to many platforms Send one post to 1 or more connected accounts with one request, and read the status of each platform's copy. Add media with [Media](/guides/media). Required fields differ per platform: Instagram, for example, needs an image or video ([Instagram publishing](/platforms/instagram/publishing)). ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Hello from SocialAPI!", "media": [{ "source_type": "url", "source": "https://cdn.example.com/launch.jpg" }], "platform_data": { "instagram": { "content_type": "feed" } }, "publish_now": true, "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }, { "account_id": "acc_01HZ9X4T6B8C2D5F7G9H1J3K" } ] }' ``` See [Publishing](/guides/publishing) and [Track post delivery](/guides/delivery). ## Schedule posts Set `scheduled_at` instead of `publish_now` to publish at a time you choose. Edit, reschedule, or cancel the post before that time. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Our spring sale starts today.", "media": [{ "source_type": "url", "source": "https://cdn.example.com/launch.jpg" }], "platform_data": { "instagram": { "content_type": "feed" } }, "scheduled_at": "2026-10-20T09:00:00Z", "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }] }' ``` See [Scheduling](/guides/scheduling). ## Give an AI agent access The MCP server gives an agent in Claude, ChatGPT, or Cursor tools to read the inbox, reply, publish, and manage Automations and accounts. The operations listed under [What tools do not cover](/integrations/tools#what-tools-do-not-cover) are REST only. Give an agent a [scoped API key](/guides/scoped-keys) to limit what it can do. See [MCP server](/integrations/overview) and [MCP tools reference](/integrations/tools). ## Related * [Quickstart](/quickstart) to connect an account and make your first calls * [Core concepts](/guides/concepts) for the objects behind these use cases * [Supported platforms](/platforms/overview) for what each platform allows * [Plans and usage](/guides/plans) for what each request costs # Webhook events Source: https://docs.social-api.ai/guides/webhook-events Every webhook event SocialAPI sends: when it fires, which platforms emit it, and its payload and fields, across posts, inbox, and accounts. A webhook event is one JSON body that SocialAPI sends to your [webhook endpoint](/guides/webhooks) when something happens on a connected account or on a post. This page lists every event you can subscribe to, when it fires, which platforms emit it, and its payload. Match on the `event` field, never on text inside `data`. `GET /v1/webhooks/events` returns the same list of event names with a description and a `category` for each (`posts`, `inbox`, or `accounts`). You subscribe to events by name when you [register an endpoint](/guides/webhooks#register-an-endpoint). ## Event envelope Every delivery is a JSON object with an `event` name, a `data` value (an object, except an array on a `mention.received` event sent when you list mentions), and, for events SocialAPI receives from a platform, a `raw_payload`: ```json theme={null} { "event": "comment.received", "data": { "id": "sapi_cmt_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "platform": "instagram", "type": "comment", "author": { "id": "17841400008460056", "name": "janesmith" }, "content": { "text": "Love this product!" }, "metadata": null, "received_at": "2026-10-13T14:30:00Z", "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "9b2f6c1e-3a4d-4e8b-b7c5-2f1a8d6e0c43", "platform_post_id": "17895695668004550" }, "raw_payload": { "field": "comments", "value": { "from": { "id": "17841400008460056", "username": "janesmith" }, "media": { "id": "17895695668004550", "media_product_type": "FEED" }, "id": "17865799348089039", "text": "Love this product!" } } } ``` | Field | Description | | - | - | | `event` | The event name. The same value is in the `X-SocialAPI-Event` header | | `data` | The event payload. Its shape depends on the event, described per event below | | `raw_payload` | The platform's own notification for this one event, unchanged. Present on `comment.received`, `mention.received`, `dm.received`, `dm.sent`, `dm.referral`, `dm.postback`, and `dm.deleted` when the platform sent the event to SocialAPI. Absent on every other event. Its shape belongs to the platform and changes with the platform's API versions, so read `data` when it has the field you need | The body carries no event ID and no sent-at time. The `X-SocialAPI-Delivery` header identifies the delivery, and `X-SocialAPI-Timestamp` holds the time of the attempt. SocialAPI delivers each event at least once, and events can arrive in a different order than they happened. [Deduplicate and order](/guides/webhooks#respond-to-a-webhook) on your side. ## Route events by brand Webhook events carry `brand_id`, the [brand](/guides/concepts#brand) of the connected account, next to `account_id`. One endpoint receives the events of all your brands, so when you serve more than one customer from one SocialAPI account, route each event to its customer with `brand_id`. | Location of `brand_id` | Events | | - | - | | `data.brand_id` | `comment.received`, `dm.received`, `dm.sent`, `dm.referral`, `dm.postback`, `dm.deleted`, `dm.status.sent`, `dm.status.delivered`, `dm.status.read`, `dm.status.failed`, `mention.received` that a platform notification triggers, `review.received`, `review.updated`, `account.connected`, `account.disconnected`, `account.updated`, `page.removed`, `post.unpublished` | | `data[].brand_id` | `mention.received` that [listing mentions](#mentions-you-list) triggers | | `data.targets[].brand_id` | `post.scheduled` | | `data.results[].brand_id` | `post.published`, `post.partial`, `post.failed` | A post can target accounts of different brands, so `post.scheduled`, `post.published`, `post.partial`, and `post.failed` carry one `brand_id` per target. `post.updated`, `post.deleted`, and `post.retried` name no account and carry no `brand_id`. To route `post.updated` and `post.retried`, read `targets[].account_id` from `GET /v1/posts/{pid}`, then each account's `brand_id` from `GET /v1/accounts`. `post.deleted` fires after the post is deleted, so read `targets[].account_id` from `GET /v1/posts/{pid}` before you delete the post, or record each post's brands when you create it. Treat `brand_id` as optional: an event can arrive without it. For example, these cases always arrive without it: * `post.unpublished` with `account_id` set to `null`. Route it like `post.updated`, with `GET /v1/posts/{pid}`. * `comment.received` and `mention.received` from Threads, when the same Threads account is connected to more than one of your brands. Their `account_id` is the Threads user ID, which is the `platform_user_id` of each of those accounts in its `account.connected` event, so you choose which of those brands handles the event. ## Platform coverage Which platforms emit an event is stated under each event as **Emitted for**. These groups hold across the page: * **Instagram, Facebook, Threads, WhatsApp, Telegram, Zalo, and Google Business Profile** send comments, direct messages, mentions, or reviews to SocialAPI as they happen, so those events arrive as webhook events. Each platform's page has a Capabilities table, and [Supported platforms](/platforms/overview) compares them. * **X / Twitter, YouTube, Bluesky, Pinterest, LinkedIn, and App Store Connect** push nothing to SocialAPI. They emit no comment, DM, or review events, and no mention event from the platform: read those with the inbox endpoints. Listing mentions on X, Bluesky, and a LinkedIn company Page sends `mention.received` (see [Mentions you list](#mentions-you-list)). * **TikTok** sends new comments to SocialAPI, so it emits `comment.received`. It has no DM events or review events. Listing TikTok mentions sends `mention.received` (see [Mentions you list](#mentions-you-list)). TikTok accounts also emit the `post.*` events for posts you publish through SocialAPI, `account.connected` when you connect an account, and `account.disconnected` when SocialAPI flags the account for reconnection. A TikTok [creator connection](/platforms/tiktok#creator-connection) emits no comment or mention events, and also emits `account.disconnected` when the person removes SocialAPI's app. * **Trustpilot** is not available, so it emits no events. ## Posts Post events carry the post's SocialAPI ID, which is a UUID (see [IDs](/guides/ids)). They report the state changes of posts you create through SocialAPI. The `post.published`, `post.partial`, and `post.failed` events fire once when the post settles, including a scheduled post and a post on a platform that finishes publishing later. **Emitted for:** every platform you can publish to through SocialAPI, for every event in this section. ### `post.scheduled` Fires when you create a post with a future `scheduled_at` and it enters the `scheduled` state. ```json theme={null} { "event": "post.scheduled", "data": { "post_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "status": "scheduled", "scheduled_at": "2026-10-20T14:00:00Z", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "9b2f6c1e-3a4d-4e8b-b7c5-2f1a8d6e0c43", "platform": "instagram" }, { "account_id": "acc_02HZ9X3Q4R5M6N7P8V2K0W1K", "brand_id": "4d8a2c7e-91b3-4f6a-a2d5-7e0c3b9f1a64", "platform": "facebook" } ] } } ``` | Field | Description | | - | - | | `post_id` | The post's ID | | `status` | `scheduled` | | `scheduled_at` | When the post publishes, in ISO 8601 UTC | | `targets` | One item per destination, each with `account_id`, `brand_id`, and `platform` | ### `post.published`, `post.partial`, and `post.failed` `post.published` fires when the post published on every target. `post.partial` fires when it published on at least one target and failed on at least one other. `post.failed` fires when it failed on every target. All 3 share one payload. ```json theme={null} { "event": "post.partial", "data": { "post_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "status": "partial", "results": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "9b2f6c1e-3a4d-4e8b-b7c5-2f1a8d6e0c43", "platform": "instagram", "status": "published", "platform_post_id": "17895695668004550", "permalink": "https://www.instagram.com/p/C8xY2AbC/" }, { "account_id": "acc_02HZ9X3Q4R5M6N7P8V2K0W1K", "brand_id": "4d8a2c7e-91b3-4f6a-a2d5-7e0c3b9f1a64", "platform": "facebook", "status": "failed", "error": { "code": "platform.facebook.publish_failed", "message": "Facebook rejected the post." } } ] } } ``` | Field | Description | | - | - | | `post_id` | The post's ID | | `status` | `published`, `partial`, or `failed`, matching the event name | | `results` | One item per target | | `results[].account_id` | The connected account | | `results[].brand_id` | The brand of the connected account | | `results[].page_id` | The SocialAPI page ID, when the target is a page of the account. Omitted otherwise | | `results[].platform` | The platform slug | | `results[].status` | `published` or `failed` | | `results[].platform_post_id` | The platform's ID of the published post. Omitted when the target failed | | `results[].permalink` | The public URL. Omitted when the platform returns none | | `results[].error` | On a failed target: `code` and `message` only. The same failure on `GET /v1/posts/{pid}` is in `targets[].error`, which also holds `category` and `caused_by`. Codes are described on [Track post delivery](/guides/delivery#target-errors) | ### `post.updated` Fires when you edit a draft, scheduled, or failed post. ```json theme={null} { "event": "post.updated", "data": { "post_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "status": "scheduled", "changed_fields": ["text", "scheduled_at"] } } ``` | Field | Description | | - | - | | `status` | The post's status after the edit | | `changed_fields` | The fields the edit changed: `text`, `title`, `visibility`, `media`, `scheduled_at`, `hidden`, `targets`, `first_comment`, `segments`. It never lists `platform_data`, even when the edit changed it | ### `post.deleted` Fires when you delete a post. ```json theme={null} { "event": "post.deleted", "data": { "post_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "was_status": "published" } } ``` `was_status` is the post's status before deletion. A published or partial post is removed from the platforms on a best-effort basis and stays in SocialAPI while any target is still published (see [Delete a post](/guides/delivery#delete-a-post)). In that case this event does not fire. ### `post.unpublished` Fires when `POST /v1/posts/{pid}/unpublish` removes at least one target of a published post from its platform. ```json theme={null} { "event": "post.unpublished", "data": { "post_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "9b2f6c1e-3a4d-4e8b-b7c5-2f1a8d6e0c43", "unpublished_count": 1 } } ``` `account_id` is the account the request limited the removal to, and `brand_id` is that account's brand. `account_id` is `null` when the request did not name an account, and `brand_id` is then absent. `unpublished_count` is the number of targets removed from their platform. ### `post.retried` Fires when a failed or partial post is retried with `POST /v1/posts/{pid}/retry`. A `post.published`, `post.partial`, or `post.failed` event follows when the retry settles. ```json theme={null} { "event": "post.retried", "data": { "post_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "retried_count": 2 } } ``` `retried_count` is the number of failed targets sent again. ## Comments ### `comment.received` Fires when a person comments on a post of a connected account. Facebook sends the event again when the person edits the comment. **Emitted for:** Instagram, Facebook, Threads (replies to the account's posts), and TikTok. The example is the Instagram comment shown in the [envelope](#event-envelope). The `data` object is the shared shape in [Interaction payload](#interaction-payload): nested `author` and `content` objects. It is not the flat shape (`author_id`, `text`) that `GET /v1/inbox/comments/{postId}` returns. Per platform: | Platform | What is in `data` | | - | - | | Instagram | `author.id` and `author.name` (the person's username), and `author.avatar_url` when SocialAPI can read it. `platform_post_id` is the media ID. `metadata` is `null` | | Facebook | `author.id`, `author.name`, and `page_id` (the SocialAPI page that received the comment). `platform_post_id` is the Facebook post ID | | Threads | `author.username` only. `platform_post_id` is the root post's ID, `permalink` is the reply's URL, and `metadata` holds `parent_id` (the post or reply being answered) and `is_verified`. `account_id` is the Threads user ID, not an `acc_` ID. `raw_payload` is the `values` object of the Threads event | | TikTok | `author.id` (TikTok's unique identifier for the person), `author.name`, `author.username`, and `author.avatar_url`. `platform_post_id` is the TikTok video ID. A reply under a comment is also `comment.received`. `post_id` and `permalink` are not set, and `metadata` is `null`. Comments written by the connected account are not sent. `raw_payload` is TikTok's notification: see [TikTok](/platforms/tiktok#webhook-events) | `post_id` is set on Instagram and Facebook when the post was published through SocialAPI. ## Direct messages Direct message events share the [interaction payload](#interaction-payload). A DM event carries the SocialAPI `conversation_id` and, except on Zalo, the `platform_id` of the message. Join `platform_id` to the `platform_id` of a row from `GET /v1/inbox/conversations/{id}/messages`. Do not join on `id`: the message endpoint returns its own record ID, which differs from the event's interaction ID. ### `dm.received` Fires when a person sends a message to a connected account. **Emitted for:** Instagram, Facebook, WhatsApp, Telegram, and Zalo. Zalo delivers text messages only. ```json theme={null} { "event": "dm.received", "data": { "id": "sapi_dm_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "platform": "instagram", "type": "dm.received", "author": { "id": "17846744073097661", "name": "Jane Smith" }, "recipient": { "id": "17841405793187218" }, "content": { "text": "Is this still in stock?" }, "metadata": null, "received_at": "2026-10-13T14:30:00Z", "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "9b2f6c1e-3a4d-4e8b-b7c5-2f1a8d6e0c43", "conversation_id": "7f1c4e2b-2841-41a8-85d7-27b0f37ac067", "platform_id": "aWdfZAG1faXRlbToxOklHTWVzc2FnZAUlEOjE3ODQxNDA1" }, "raw_payload": { "sender": { "id": "17846744073097661" }, "recipient": { "id": "17841405793187218" }, "timestamp": 1792161000000, "message": { "mid": "aWdfZAG1faXRlbToxOklHTWVzc2FnZAUlEOjE3ODQxNDA1", "text": "Is this still in stock?" } } } ``` What differs per platform: | Platform | Differences | | - | - | | Instagram | `recipient` is the connected account. `story` is set when the message replies to one of the account's stories. A story mention arrives as `dm.received` with `metadata.attachments[].type` of `story_mention`, not as `mention.received`. A message shared from a post carries the post in `metadata.attachments` (`media_id`, and `post_id` when the post was published through SocialAPI) and sets `platform_post_id` | | Facebook | Adds `page_id`. Has no profile picture for the sender: `author.avatar_url` is absent | | WhatsApp | `type` is `dm`. `author.id` is the sender's phone number, or their business-scoped user ID (for example `CO.1648230076976170`) when the sender uses a WhatsApp username and hides their number. `author.name` is the contact name WhatsApp reports, or the username when there is no name. `metadata` holds `platform_id` and `phone_number_id`. A tap on a reply button or list item also sets `metadata.interactive_type` and `metadata.payload` | | Telegram | `type` is `dm`. `metadata` holds `platform_id` and `chat_id` | | Zalo | `author.id` only. No `conversation_id` and no `platform_id` | On Telegram, Instagram, and Facebook a message that replies to an earlier one carries `reply_to`, for example `"reply_to": { "platform_id": "20" }`, with the replied-to message's `platform_id`. `reply_to` is absent when the message is not a reply. ### `dm.sent` Fires when a connected account sent a message. The sender is `author` and the person who received it is `recipient`. **Emitted for:** Instagram, Facebook, and WhatsApp. On Instagram and Facebook the event fires for every message the platform reports as sent from the account: messages sent through the API, replies typed in the platform's own app or inbox, and DMs your [Automations](/guides/automations) send. The event carries nothing that separates an Automation's DM from one you sent. On Facebook, `raw_payload` carries `app_id`, the ID of the app that sent the message, which tells API sends from replies typed in the Page inbox. On WhatsApp there is no message echo. The event fires once per message, when WhatsApp reports it as `sent`, for messages sent with `POST /v1/inbox/conversations/{id}/messages` or the `send-template` endpoint. It also fires for each message sent from the phone on a number that stays on the WhatsApp Business app (see [WhatsApp](/platforms/whatsapp)). A redelivered receipt does not raise it again, and neither do the later `delivered` and `read` receipts. `received_at` is WhatsApp's send time, `author.id` is the sending phone number ID, and `recipient.id` is the contact's WhatsApp ID. ```json theme={null} { "event": "dm.sent", "data": { "id": "sapi_dm_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1J", "platform": "instagram", "type": "dm.sent", "author": { "id": "17841405793187218" }, "recipient": { "id": "17846744073097661", "name": "Jane Smith" }, "content": { "text": "Yes, 3 left." }, "metadata": null, "received_at": "2026-10-13T14:31:00Z", "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "9b2f6c1e-3a4d-4e8b-b7c5-2f1a8d6e0c43", "conversation_id": "7f1c4e2b-2841-41a8-85d7-27b0f37ac067", "platform_id": "aWdfZAG1faXRlbToxOklHTWVzc2FnZAUlEOjE3ODQxNDA2" } } ``` `dm.status.sent` fires alongside `dm.sent` for the same message. ### `dm.referral` Fires when a person opens a conversation from an ad or referral link without sending a message. `content.text` is empty and `metadata.referral` is always present. The event has no `platform_id`. **Emitted for:** Instagram and Facebook. ```json theme={null} { "event": "dm.referral", "data": { "id": "sapi_dm_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1K", "platform": "instagram", "type": "dm.referral", "author": { "id": "17846744073097661" }, "recipient": { "id": "17841405793187218" }, "content": { "text": "" }, "metadata": { "referral": { "ad_id": "120210572164750432", "source": "ADS", "type": "OPEN_THREAD", "ads_context_data": { "ad_title": "Spring Sale 25% Off", "photo_url": "https://example.com/ads/spring-sale.jpg" } } }, "received_at": "2026-10-13T14:32:00Z", "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "9b2f6c1e-3a4d-4e8b-b7c5-2f1a8d6e0c43", "conversation_id": "7f1c4e2b-2841-41a8-85d7-27b0f37ac067" } } ``` A person who opens the conversation from an ad and sends a message produces `dm.received` with the same `metadata.referral`. The `referral` object holds only the fields the platform sent: `ad_id`, `ref`, `source`, `type`, and `ads_context_data` (`ad_title`, `photo_url`, `video_url`). ### `dm.postback` Fires when a person taps a quick reply or a `postback` button in a DM. `metadata.payload` is the payload you set when you sent the button and `metadata.title` is the button's title. When the tap arrives through an ig.me link or an icebreaker, `metadata.referral` is also set. URL buttons open the link in the person's browser and raise no event. **Emitted for:** Instagram and Facebook. WhatsApp sends a tap as `dm.received` with `metadata.interactive_type` and `metadata.payload`. ```json theme={null} { "event": "dm.postback", "data": { "id": "sapi_dm_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1L", "platform": "instagram", "type": "dm.postback", "author": { "id": "17846744073097661" }, "recipient": { "id": "17841405793187218" }, "content": { "text": "" }, "metadata": { "title": "Track my order", "payload": "TRACK_ORDER" }, "received_at": "2026-10-13T14:33:00Z", "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "9b2f6c1e-3a4d-4e8b-b7c5-2f1a8d6e0c43", "conversation_id": "7f1c4e2b-2841-41a8-85d7-27b0f37ac067", "platform_id": "aWdfZAG1faXRlbToxOklHTWVzc2FnZAUlEOjE3ODQxNDA3" } } ``` Taps on buttons an Automation sends also arrive as `dm.postback`. Their `metadata.payload` starts with `sa1.` and the Automation answers the tap itself, so skip payloads that start with `sa1.`. Do not start the payloads of your own buttons with `sa1.`. Buttons are described in [Direct messages](/guides/direct-messages). ### `dm.deleted` Fires when the sender unsends a message. `platform_id` names the removed message, which no longer appears in the conversation. `content.text` is empty. **Emitted for:** Instagram. ```json theme={null} { "event": "dm.deleted", "data": { "id": "sapi_dm_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "platform": "instagram", "type": "dm.deleted", "author": { "id": "17846744073097661" }, "recipient": { "id": "17841405793187218" }, "content": { "text": "" }, "metadata": null, "received_at": "2026-10-13T14:34:00Z", "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "9b2f6c1e-3a4d-4e8b-b7c5-2f1a8d6e0c43", "conversation_id": "7f1c4e2b-2841-41a8-85d7-27b0f37ac067", "platform_id": "aWdfZAG1faXRlbToxOklHTWVzc2FnZAUlEOjE3ODQxNDA1" } } ``` ### `dm.status.sent`, `dm.status.delivered`, `dm.status.read`, and `dm.status.failed` These events report the progress of a message you sent. Their `data` is a short status object, not an interaction: match the message by its IDs. `account_id` and `brand_id` name the account that sent it. | Event | Fires when | Emitted for | | - | - | - | | `dm.status.sent` | The platform accepted the message | Instagram, Facebook, WhatsApp | | `dm.status.delivered` | The message reached the recipient | Facebook, WhatsApp. Instagram sends no delivery receipts | | `dm.status.read` | The recipient read the message | Instagram, Facebook, WhatsApp | | `dm.status.failed` | The platform could not deliver the message | WhatsApp | On Instagram and Facebook: ```json theme={null} { "event": "dm.status.read", "data": { "status": "read", "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "9b2f6c1e-3a4d-4e8b-b7c5-2f1a8d6e0c43", "mids": ["aWdfZAG1faXRlbToxOklHTWVzc2FnZAUlEOjE3ODQxNDA2"], "recipient_id": "17846744073097661" } } ``` | Field | Description | | - | - | | `status` | `sent`, `delivered`, or `read` | | `account_id` | The connected account that sent the message | | `brand_id` | The brand of that account | | `mids` | The platform message IDs the receipt covers | | `recipient_id` | The platform ID of the person who received or read the message | | `page_id` | Facebook only: the SocialAPI page ID | | `raw_payload` | The platform's notification, inside `data`. Present on Instagram `read` and on Facebook `delivered` and `read`. Absent on `sent` | On WhatsApp: ```json theme={null} { "event": "dm.status.failed", "data": { "id": "sapi_dm_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1J", "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "9b2f6c1e-3a4d-4e8b-b7c5-2f1a8d6e0c43", "message_id": "wamid.HBgLMTU1NTAxMjM0NTYVAgARGBI5QTNDQTVCM0Q0Q0Q2RkE4RjUA", "recipient_id": "15550123456", "status": "failed", "error_code": 131026, "error_title": "Message undeliverable", "error_message": "Unable to deliver the message. The recipient may not be a WhatsApp user." } } ``` | Field | Description | | - | - | | `id` | The SocialAPI message ID, the same value the send response returned. Empty when WhatsApp sent no `recipient_id` | | `account_id` | The connected WhatsApp account that sent the message | | `brand_id` | The brand of that account | | `message_id` | The WhatsApp message ID | | `recipient_id` | The recipient's WhatsApp ID | | `status` | `sent`, `delivered`, `read`, or `failed` | | `error_code`, `error_title`, `error_message` | WhatsApp's error. Present only when WhatsApp reports one, which is how `failed` explains itself, because the send had already succeeded | ## Mentions ### `mention.received` Fires in two cases: the platform tells SocialAPI that a connected account was @mentioned in a comment or in a post caption, or you list mentions with `GET /v1/accounts/{id}/mentions` (see [Mentions you list](#mentions-you-list)). **Emitted for:** Instagram, Facebook, and Threads when the platform reports a mention, and every platform that lists mentions when you list them. Instagram story mentions are not `mention.received`: they arrive as `dm.received` (see above). ```json theme={null} { "event": "mention.received", "data": { "id": "sapi_mnt_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "platform": "instagram", "type": "mention", "author": { "id": "17846744073097661", "name": "Jane Smith", "username": "janesmith" }, "content": { "text": "Loving the new range from @acmecorp" }, "metadata": { "mention_type": "caption", "permalink": "https://www.instagram.com/p/C8xY2AbC/" }, "received_at": "2026-10-13T14:35:00Z", "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "9b2f6c1e-3a4d-4e8b-b7c5-2f1a8d6e0c43", "platform_post_id": "17895695668004550" }, "raw_payload": { "field": "mentions", "value": { "media_id": "17895695668004550" } } } ``` SocialAPI reads the mention's text and author from the platform before sending. When that read fails, the event is still delivered with `content`, `author`, and `metadata.permalink` missing or empty. | Platform | What is in `data` | | - | - | | Instagram | `metadata.mention_type` is `comment` or `caption`. `platform_post_id` is the media ID | | Facebook | `metadata.mention_type` is `comment` or `post`. Adds `page_id` | | Threads | `author.username` only. `platform_post_id` is the mentioning post's ID, `permalink` is its URL, and there is no `mention_type`. `account_id` is the Threads user ID, not an `acc_` ID | On Threads, `GET /v1/accounts/{id}/mentions` returns the same mention with the same `id`, so the event and the list can be matched. Mentions are described on [Mentions](/guides/mentions). #### Mentions you list Every `GET /v1/accounts/{id}/mentions` call that returns at least one item also sends one `mention.received` event, on every platform that lists mentions. A call that returns no items sends none. SocialAPI sends the event on every such call, including a repeated call that returns the same items. For this event, `data` is an array: the items of that response's `data`, in the same order, each with the fields of the item in the list plus `brand_id`. The event has no `raw_payload`. The event a platform notification triggers has an object in `data`, so a handler for `mention.received` must accept both an object and an array. ```json theme={null} { "event": "mention.received", "data": [ { "id": "sapi_mnt_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "platform": "instagram", "type": "mention", "author": { "id": "17846744073097661", "name": "Jane Smith", "username": "janesmith" }, "content": { "text": "Loving the new range from @acmecorp" }, "metadata": { "mention_type": "caption", "permalink": "https://www.instagram.com/p/C8xY2AbC/" }, "received_at": "2026-10-13T14:35:00Z", "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "9b2f6c1e-3a4d-4e8b-b7c5-2f1a8d6e0c43", "platform_post_id": "17895695668004550" } ] } ``` Repeated list calls send the same mention again, so deduplicate on `data[].id`. ## Reviews Review events do not follow the interaction payload. Google sends only the name of the changed review, so `data` holds identifiers and a `review` object that SocialAPI reads for you. The `review` object has the same fields as an item of `GET /v1/inbox/reviews/{account_id}`, so one parser handles both. **Emitted for both review events:** Google Business Profile. ### `review.received` Fires when a customer posts a new review on a connected location. ```json theme={null} { "event": "review.received", "data": { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "9b2f6c1e-3a4d-4e8b-b7c5-2f1a8d6e0c43", "review_id": "sapi_rev_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "change": "new_review", "review": { "id": "sapi_rev_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "rating": 4, "text": "Friendly staff, quick service.", "author": { "name": "Jane Smith", "avatar_url": "https://lh3.googleusercontent.com/a/abc" }, "created_at": "2026-10-13T14:30:00Z", "updated_at": "2026-10-13T14:30:00Z" } } } ``` | Field | Description | | - | - | | `account_id` | The connected Google Business Profile account | | `brand_id` | The brand of that account | | `review_id` | The review's interaction ID | | `change` | `new_review` | | `review` | The review. Omitted when SocialAPI could not read it (the review was deleted in the meantime, or the account's access was revoked). Fall back to `GET /v1/inbox/reviews/{account_id}` | | `review.rating` | Integer from 1 to 5. 0 when the platform gave no rating | | `review.text` | The review text. Empty for a rating-only review | | `review.reply` | Your reply, with `text`, `updated_at`, `state` (`PENDING`, `APPROVED`, or `REJECTED`), and `policy_violation` on a rejected reply. Omitted when there is no reply | | `review.reply_url` | A Google-hosted URL for replying to the review. Omitted when empty | | `review.media` | Photos or videos the reviewer attached, each with `thumbnail_url` and optionally `thumbnail_label` and `video_url`. Omitted when there are none | When more than 1 SocialAPI account has connected the same Google location, each of them receives the event. ### `review.updated` Fires when a review changes. Branch on `change`, because one event covers every kind of change. | `change` | Meaning | Extra fields | | - | - | - | | `review_edited` | The reviewer edited their review | None | | `reply_approved` | A reply you posted passed Google's moderation and is public | `state`: `APPROVED` | | `reply_rejected` | Google rejected the reply and it will never publish | `state`: `REJECTED`. `review.reply.policy_violation` holds Google's reason, so you can rewrite and send again without another call | ```json theme={null} { "event": "review.updated", "data": { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "9b2f6c1e-3a4d-4e8b-b7c5-2f1a8d6e0c43", "review_id": "sapi_rev_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "change": "reply_approved", "state": "APPROVED", "review": { "id": "sapi_rev_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "rating": 4, "text": "Friendly staff, quick service.", "author": { "name": "Jane Smith", "avatar_url": "https://lh3.googleusercontent.com/a/abc" }, "reply": { "text": "Thank you, Jane.", "updated_at": "2026-10-13T15:00:00Z", "state": "APPROVED" }, "created_at": "2026-10-13T14:30:00Z", "updated_at": "2026-10-13T14:30:00Z" } } } ``` The fields of `review` are in the `review.received` table. Reviews are described on [Reviews](/guides/reviews). ## Accounts ### `account.connected` Fires when an account finishes connecting, including when you reconnect it. Use it to show the connected profile or start work that needs the account. **Emitted for:** every platform. ```json theme={null} { "event": "account.connected", "data": { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "9b2f6c1e-3a4d-4e8b-b7c5-2f1a8d6e0c43", "platform": "instagram", "platform_user_id": "17841405793187218", "username": "acmecorp", "display_name": "ACME Corp", "profile_picture_url": "https://example.com/avatars/acmecorp.jpg", "bio": "Official ACME account", "profile_url": "https://www.instagram.com/acmecorp/", "user_email": "owner@acme.com", "connected_at": "2026-10-13T14:30:00Z" } } ``` | Field | Description | | - | - | | `account_id` | The connected account's ID | | `brand_id` | The brand the account belongs to | | `platform` | The platform slug | | `platform_user_id` | The platform's ID for the account | | `username`, `display_name`, `profile_picture_url`, `bio` | The profile as the platform reports it. Empty strings when the platform gives none | | `profile_url` | The public profile URL. Built from `username` on Instagram, Threads, TikTok, and X, and from `platform_user_id` on Facebook, LinkedIn, YouTube, and Google Business Profile. Empty on every other platform | | `user_email` | The email address of your SocialAPI account | | `connected_at` | When the connection finished, in ISO 8601 UTC | ### `account.disconnected` Fires when a connected account is removed or needs reconnecting. It has 2 forms. **The person revoked access on the platform.** The account is deleted from SocialAPI. The payload has no `reason` and no `reconnect_required`. ```json theme={null} { "event": "account.disconnected", "data": { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "9b2f6c1e-3a4d-4e8b-b7c5-2f1a8d6e0c43", "platform": "instagram", "platform_user_id": "17841405793187218" } } ``` **Emitted for:** Instagram, Facebook, Threads, and a TikTok [creator connection](/platforms/tiktok#creator-connection), when the person removes the app in the platform's settings. The default TikTok connection gets no notice of a removal: see [When TikTok authorization ends](/platforms/tiktok#when-tiktok-authorization-ends). **SocialAPI flagged the account for reconnection.** The account still exists. Its `status` changes so that calls on it signal that it needs reconnecting (see [Connect accounts](/guides/connect-accounts)). The payload has `reconnect_required: true` and a `reason`. ```json theme={null} { "event": "account.disconnected", "data": { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "9b2f6c1e-3a4d-4e8b-b7c5-2f1a8d6e0c43", "platform": "facebook", "reason": "all_pages_removed", "reconnect_required": true } } ``` | `reason` | Platforms | Meaning | | - | - | - | | `all_pages_removed` | Facebook | The last remaining Page of the account was removed | | `access_revoked` | Instagram, Facebook, Threads, WhatsApp | The account's token or scopes were revoked, which fails every page at once. On WhatsApp it also means Meta no longer serves the number | | `page_access_lost` | Facebook | A Page assigned to the account is no longer available after a reconnect | | `number_banned` | WhatsApp | Meta banned the number. Reconnecting is refused until the ban is lifted on appeal | | `posts_sync_auth_failed` | Every platform except Pinterest, LinkedIn member profiles, WhatsApp, X, Telegram, and App Store Connect | The platform refused a read of the account's posts because the login needs renewing | | `youtube_account_suspended` | YouTube | YouTube suspended or closed the account or its channel. Reconnecting does not clear it | **Emitted for:** the platforms listed in the table. The event's `reason` can differ from the account's `reconnect_reason`. When a token check finds a revoked Instagram, Facebook, or Threads login, the event says `access_revoked` while `reconnect_reason` holds the specific cause, such as `password_changed`. On WhatsApp, `reconnect_reason` is `whatsapp_number_unavailable` or `whatsapp_number_banned` where the event says `access_revoked` or `number_banned`. Treat `reason` as the event's own value. ### `account.updated` Fires when something changes on the account. Branch on `change`. **Emitted for:** every platform for `permissions_reduced`. Google Business Profile for every other `change`. | `change` | Meaning | Extra fields | | - | - | - | | `customer_media_added` | A customer added media to the location | `media_id`: the media item's Google resource name | | `google_update_pending` | Google has updates to the location waiting for review | None | | `voice_of_merchant_changed` | The location's Voice of Merchant status changed | None | | `duplicate_location` | Google flagged the location as a duplicate | None | | `permissions_reduced` | The account lost [permissions](/guides/permissions) that you did not remove yourself | `permissions`: the list of permissions it lost | ```json theme={null} { "event": "account.updated", "data": { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "9b2f6c1e-3a4d-4e8b-b7c5-2f1a8d6e0c43", "change": "customer_media_added", "media_id": "accounts/123456789/locations/987654321/media/customers/AF1QipN" } } ``` `permissions_reduced` fires when X refused a media upload because the account's access lacks `media.write`, or when a reconnect granted less than the account had. It does not fire when you remove a permission with `PATCH /v1/accounts/{id}`. Reconnect the account to ask for the permissions again. ```json theme={null} { "event": "account.updated", "data": { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "9b2f6c1e-3a4d-4e8b-b7c5-2f1a8d6e0c43", "change": "permissions_reduced", "permissions": ["messages.read", "messages.write"] } } ``` ### `page.removed` Fires when a Page of a multi-page account is found removed. SocialAPI deactivates the page, and it stops appearing in page listings. Other pages of the account stay active. **Emitted for:** Facebook. ```json theme={null} { "event": "page.removed", "data": { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "9b2f6c1e-3a4d-4e8b-b7c5-2f1a8d6e0c43", "platform_page_id": "102938475610293", "name": "ACME Storefront", "platform": "facebook", "reason": "page_not_found" } } ``` | Field | Description | | - | - | | `platform_page_id` | The platform's Page ID | | `name` | The Page's name | | `reason` | `page_not_found` (SocialAPI found the Page gone during its periodic refresh) or `page_access_lost` (a reconnect no longer has access to the Page) | ## Interaction payload Comments, direct messages, and mentions share one `data` shape. Fields marked optional are omitted when empty. | Field | Description | | - | - | | `id` | The SocialAPI interaction ID, with the `sapi_cmt_`, `sapi_dm_`, or `sapi_mnt_` prefix. See [IDs](/guides/ids) | | `platform` | The platform slug | | `type` | The interaction type, in the table below | | `account_id` | The connected account that received or sent the interaction | | `brand_id` | The brand of that account. See [Route events by brand](#route-events-by-brand) | | `page_id` | Optional. Facebook only: the SocialAPI page ID of the Page that received the event | | `author` | Who wrote it: `id`, `name`, `username`, and `avatar_url`, each omitted when the platform gives none | | `recipient` | Optional. The other party of a DM event on Instagram, Facebook, and WhatsApp, in the shape of `author` | | `content` | `text` (empty for attachment-only messages and referrals) and `media`, a list of `url` and `type` items, omitted when there is none | | `metadata` | Platform-specific keys, described per event. `null` when the event has none | | `received_at` | When the interaction happened in ISO 8601 UTC. On Instagram comments and on Instagram and Facebook mentions it is the time SocialAPI received the event | | `post_id` | Optional. The SocialAPI post ID, when the post was published through SocialAPI | | `platform_post_id` | Optional. The platform's ID of the post the interaction belongs to | | `permalink` | Optional. Threads replies and mentions, and TikTok mentions: the URL of the reply or mention | | `conversation_id` | Optional. The SocialAPI conversation ID of a DM on Instagram, Facebook, WhatsApp, and Telegram | | `platform_id` | Optional. The platform's message ID of a DM. Not set on `dm.referral` and on Zalo | | `story` | Optional. Instagram only: the story a message replies to, with `id`, `media_url`, and `expires_at` | | `reply_to` | Optional. The earlier message a DM replies to, as `{ "platform_id": "..." }` | | Event | `type` value | | - | - | | `comment.received` | `comment` | | `mention.received` | `mention` | | `dm.received` | `dm.received` on Instagram, Facebook, and Zalo. `dm` on WhatsApp and Telegram | | `dm.sent`, `dm.referral`, `dm.postback`, `dm.deleted` | The event name | Match on `event`, not on `type`. ## Use events with Automations Every inbound Instagram and Facebook event goes both to the webhook endpoints subscribed to it and to your [Automations](/guides/automations). An Automation removes nothing from your webhook stream. A comment or DM that an Automation answers still arrives as `comment.received` or `dm.received`, and if your own handler also replies, the person gets both replies. No webhook event reports an Automation run: read an Automation's Firings instead. ## Related * [Webhooks](/guides/webhooks): register endpoints, verify signatures, and handle retries. * [Direct messages](/guides/direct-messages): send the messages and buttons that produce `dm.sent`, `dm.postback`, and `dm.status.*`. * [Comments](/guides/comments): read and answer the comments behind `comment.received`. * [Reviews](/guides/reviews): read and reply to the reviews behind `review.received`. * [Track post delivery](/guides/delivery): the post statuses and target errors behind the `post.*` events. * [Supported platforms](/platforms/overview): the capability matrix for each platform. # Webhooks Source: https://docs.social-api.ai/guides/webhooks Register an HTTPS endpoint, verify each request signature, and handle retries to receive real-time comments, DMs, mentions, reviews, and post events. A webhook endpoint is an HTTPS URL on your server. SocialAPI sends it a signed JSON `POST` each time an event you subscribed to occurs: a new comment, direct message (DM), mention, or review, a post changing state, or a connected account changing connection state. Webhooks replace polling each platform for new activity. Managing endpoints costs no credits. Each event, its platforms, and its payload are on [Webhook events](/guides/webhook-events). While your SocialAPI account is past due, SocialAPI delivers no events (see [Past-due accounts](/guides/plans#past-due-accounts)). ## Endpoints | Method | Path | Description | | - | - | - | | `GET` | `/v1/webhooks/events` | List the event names you can subscribe to | | `POST` | `/v1/webhooks` | Register an endpoint | | `GET` | `/v1/webhooks` | List your endpoints | | `GET` | `/v1/webhooks/{id}` | Get an endpoint with its delivery counts | | `PATCH` | `/v1/webhooks/{id}` | Change the URL, the events, or the active state | | `DELETE` | `/v1/webhooks/{id}` | Delete an endpoint | | `GET` | `/v1/webhooks/{id}/deliveries` | List an endpoint's deliveries | | `GET` | `/v1/webhooks/{id}/deliveries/{did}` | Get one delivery with its payload and attempts | | `POST` | `/v1/webhooks/{id}/deliveries/{did}/retry` | Send a delivery again | | `POST` | `/v1/webhooks/{id}/test` | Send a test payload to an endpoint | You can also manage endpoints in the dashboard under **Webhooks**. ## Register an endpoint Subscribe an endpoint to 3 events: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/webhooks \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://app.example.com/webhooks/socapi", "events": ["comment.received", "dm.received", "review.received"] }' ``` Response (HTTP 201): ```json theme={null} { "id": "3f6b0c1e-5a47-4d2b-9a0e-8c1d7b2e4f90", "url": "https://app.example.com/webhooks/socapi", "events": ["comment.received", "dm.received", "review.received"], "secret": "a3f8c2e1b9d4f7a6c5e0b3d2f1a8e7c4b9d6f3a2e5c8b1d4f7a0e3c6b9d2f5a8", "message": "Store the secret securely. It will not be shown again." } ``` | Field | Required | Description | | - | - | - | | `url` | Yes | The URL that receives events. It must use `https` | | `events` | Yes | A non-empty array of event names from [Webhook events](/guides/webhook-events) | The `secret` is a 64-character string. It is returned once, in this response. `GET /v1/webhooks/{id}` returns only a hint made of `whsec_...` and the last 4 characters. To replace a lost secret, register a new endpoint and delete the old one. SocialAPI does not call your URL when you register it, so registration succeeds even when the server is not running. Send a test delivery to check the endpoint. An invalid request returns `400 validation.field_required` (no `url`), `400 validation.field_invalid` (`url` is not an HTTPS URL, `events` is empty, or an event name is unknown), or `400 validation.body_invalid` (the body is not valid JSON). An endpoint receives events for every connected account and brand of your SocialAPI account. There is no filter by account or brand: route each event with the `brand_id` it carries (see [Route events by brand](/guides/webhook-events#route-events-by-brand)). ## What SocialAPI sends Every delivery is a `POST` with a JSON body that holds an `event` name and a `data` value: an object, except an array on a `mention.received` event sent when you list mentions (see the [event envelope](/guides/webhook-events#event-envelope)). These headers are on every request: | Header | Value | | - | - | | `Content-Type` | `application/json` | | `X-SocialAPI-Event` | The event name, for example `comment.received` | | `X-SocialAPI-Timestamp` | Unix time in seconds when SocialAPI sent this attempt | | `X-SocialAPI-Signature-V2` | `sha256=` followed by the hex HMAC-SHA256 of `.`. Use this one | | `X-SocialAPI-Signature` | `sha256=` followed by the hex HMAC-SHA256 of the raw body alone. It has no freshness check | | `X-SocialAPI-Delivery` | The delivery's ID. It is the same on every retry of the delivery. A test delivery has none | ## Verify signatures Verify `X-SocialAPI-Signature-V2` on every request before you act on it. The signature proves the request came from SocialAPI and that nobody changed the body. Follow these rules: 1. Read the raw request body. Do not parse it and serialize it again, because that changes the bytes. 2. Use the endpoint `secret` exactly as returned, as the key. Do not decode it from hex. 3. Compute the HMAC-SHA256 of the timestamp header value, a literal `.`, and the raw body. Prefix the hex digest with `sha256=`. 4. Compare it with `X-SocialAPI-Signature-V2` in constant time. 5. Reject the request when the timestamp is more than 5 minutes from your clock (see [Replay protection](#replay-protection)). ```javascript Node.js theme={null} import crypto from "node:crypto"; import express from "express"; const app = express(); function verify(secret, rawBody, headers, toleranceSeconds = 300) { const timestamp = headers["x-socialapi-timestamp"]; const received = headers["x-socialapi-signature-v2"]; if (!timestamp || !received) return false; const age = Math.abs(Date.now() / 1000 - Number(timestamp)); if (!Number.isFinite(age) || age > toleranceSeconds) return false; const expected = "sha256=" + crypto .createHmac("sha256", secret) .update(`${timestamp}.`) .update(rawBody) .digest("hex"); const a = Buffer.from(expected); const b = Buffer.from(received); return a.length === b.length && crypto.timingSafeEqual(a, b); } app.post("/webhooks/socapi", express.raw({ type: "application/json" }), (req, res) => { if (!verify(process.env.SOCAPI_WEBHOOK_SECRET, req.body, req.headers)) { return res.sendStatus(401); } const payload = JSON.parse(req.body.toString("utf8")); // Store payload, then process it after you respond. res.sendStatus(200); }); ``` ```python Python theme={null} import hashlib import hmac import os import time from flask import Flask, abort, request app = Flask(__name__) def verify(secret: str, raw_body: bytes, headers, tolerance_seconds: int = 300) -> bool: timestamp = headers.get("X-SocialAPI-Timestamp", "") received = headers.get("X-SocialAPI-Signature-V2", "") try: age = abs(time.time() - int(timestamp)) except ValueError: return False if age > tolerance_seconds: return False expected = "sha256=" + hmac.new( secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, received) @app.post("/webhooks/socapi") def webhook(): if not verify(os.environ["SOCAPI_WEBHOOK_SECRET"], request.get_data(), request.headers): abort(401) payload = request.get_json() # Store payload, then process it after you respond. return "", 200 ``` ```go Go theme={null} package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "io" "math" "net/http" "os" "strconv" "time" ) func verify(secret string, rawBody []byte, h http.Header) bool { timestamp := h.Get("X-SocialAPI-Timestamp") received := h.Get("X-SocialAPI-Signature-V2") sent, err := strconv.ParseInt(timestamp, 10, 64) if err != nil || math.Abs(float64(time.Now().Unix()-sent)) > 300 { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(timestamp + ".")) mac.Write(rawBody) expected := "sha256=" + hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(expected), []byte(received)) } func handler(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil || !verify(os.Getenv("SOCAPI_WEBHOOK_SECRET"), body, r.Header) { http.Error(w, "invalid signature", http.StatusUnauthorized) return } // Store body, then process it after you respond. w.WriteHeader(http.StatusOK) } func main() { http.HandleFunc("/webhooks/socapi", handler) http.ListenAndServe(":8080", nil) } ``` ```php PHP theme={null} $tolerance) { return false; } $expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret); return hash_equals($expected, $received); } $rawBody = file_get_contents('php://input'); if (!verify(getenv('SOCAPI_WEBHOOK_SECRET'), $rawBody, getallheaders())) { http_response_code(401); exit; } $payload = json_decode($rawBody, true); // Store $payload, then process it after you respond. http_response_code(200); ``` Use a constant-time comparison (`timingSafeEqual`, `compare_digest`, `hmac.Equal`, `hash_equals`). A plain string comparison leaks timing information. `X-SocialAPI-Signature` (v1) is the HMAC-SHA256 of the raw body alone, prefixed with `sha256=`. It is kept for existing integrations. It proves authenticity but not freshness, so a captured request still verifies later. ## Replay protection `X-SocialAPI-Signature-V2` covers the timestamp, so an attacker cannot move a captured request into a later time window by editing the header. Reject any request whose `X-SocialAPI-Timestamp` is more than 5 minutes from your clock. The timestamp is the time of the attempt, so a retry of an old event carries a new timestamp and passes the check. To process each event once, store the `X-SocialAPI-Delivery` IDs you handled and skip repeats. A manual retry (`POST .../retry`) creates a new delivery with a new ID, so also deduplicate on the event's own identity: `data.id` for comment and DM events, `data.id` or each `data[].id` for `mention.received`, and `data.post_id` with the event name for post events. ## Respond to a webhook Your endpoint must return an HTTP `2xx` status within 10 seconds. Any other status, a timeout, or a connection error counts as a failed attempt. Return `200` once you have stored the body, and do the real work afterward. SocialAPI delivers each event at least once. The same event can arrive more than once, and events can arrive in a different order than they happened. Make your handler idempotent, and read the current state from the API when order matters. While an endpoint's `is_active` is `false`, SocialAPI sends it nothing. It does not send those events later when you turn the endpoint on again. To recover what you missed, read `GET /v1/events` (within your plan's event history) or the inbox and post endpoints. ## Retries A failed attempt is retried until the delivery has used 10 attempts. The wait before each retry grows, so an endpoint that recovers within about 14 hours of the first attempt still receives the event: | Attempt | Sent | Time since attempt 1 | | - | - | - | | 1 | When the event occurs | | | 2 | 10 seconds after attempt 1 fails | about 10 seconds | | 3 | 1 minute after attempt 2 fails | about 1 minute | | 4 | 5 minutes after attempt 3 fails | about 6 minutes | | 5 | 15 minutes after attempt 4 fails | about 21 minutes | | 6 | 30 minutes after attempt 5 fails | about 51 minutes | | 7 | 1 hour after attempt 6 fails | about 2 hours | | 8 | 2 hours after attempt 7 fails | about 4 hours | | 9 | 4 hours after attempt 8 fails | about 8 hours | | 10 | 6 hours after attempt 9 fails | about 14 hours | Each wait varies by up to 10 percent either way. After the tenth failed attempt, about 14 hours after the first, the delivery's `status` becomes `failed` and SocialAPI makes no further attempts. Send a failed delivery again yourself with `POST /v1/webhooks/{id}/deliveries/{did}/retry`. A retried event can arrive hours after events that happened later. Order events by their own timestamps, not by arrival. When an attempt ends in a connection error, a `429`, or a `5xx` other than `501`, SocialAPI sends the same request one more time within the same attempt. Your endpoint can therefore receive the same delivery twice, seconds apart. Both requests carry the same `X-SocialAPI-Delivery`. ## Manage endpoints List your endpoints: ```bash theme={null} curl https://api.social-api.ai/v1/webhooks \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "id": "3f6b0c1e-5a47-4d2b-9a0e-8c1d7b2e4f90", "url": "https://app.example.com/webhooks/socapi", "events": ["comment.received", "dm.received", "review.received"], "is_active": true, "created_at": "2026-10-13T09:00:00Z" } ], "count": 1 } ``` `GET /v1/webhooks/{id}` returns the same fields plus `secret_hint` and delivery counts: ```json theme={null} { "id": "3f6b0c1e-5a47-4d2b-9a0e-8c1d7b2e4f90", "url": "https://app.example.com/webhooks/socapi", "events": ["comment.received", "dm.received", "review.received"], "is_active": true, "secret_hint": "whsec_...f5a8", "created_at": "2026-10-13T09:00:00Z", "stats": { "last_24h": { "delivered": 142, "failed": 3 }, "last_7d": { "delivered": 980, "failed": 11 } } } ``` Change an endpoint with `PATCH`. Send at least 1 of `url`, `events`, and `is_active`. `events` replaces the whole list. The `url` and `events` rules are the same as at registration, and a body with no fields returns `400 validation.body_invalid`. ```bash theme={null} curl -X PATCH https://api.social-api.ai/v1/webhooks/3f6b0c1e-5a47-4d2b-9a0e-8c1d7b2e4f90 \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{"is_active": false}' ``` The response (HTTP 200) is the updated endpoint, in the shape of the list items. Delete an endpoint: ```bash theme={null} curl -X DELETE https://api.social-api.ai/v1/webhooks/3f6b0c1e-5a47-4d2b-9a0e-8c1d7b2e4f90 \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "ok": true } ``` Deleting stops future deliveries. A delivery already in progress can still reach your URL once. An unknown endpoint ID returns `404 resource.not_found`. ## Monitor deliveries SocialAPI records every delivery with its status, the HTTP status of your response, and how long your endpoint took. A delivery's `status` is `pending` (not finished), `delivered` (your endpoint returned `2xx`), or `failed` (10 attempts used). ```bash theme={null} curl "https://api.social-api.ai/v1/webhooks/3f6b0c1e-5a47-4d2b-9a0e-8c1d7b2e4f90/deliveries?status=failed&limit=20" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "id": "b1d4e7a2-6c3f-4a90-8e15-2f7c9d0a3b68", "event_type": "comment.received", "status": "failed", "response_status_code": 500, "response_duration_ms": 412, "attempt_count": 5, "description": "Instagram comment received", "created_at": "2026-10-13T14:30:00Z" } ], "next_cursor": "2026-10-13T14:12:00Z", "count": 1 } ``` | Parameter | Description | | - | - | | `status` | Filter by `pending`, `delivered`, or `failed` | | `event_type` | Filter by event name | | `limit` | Page size from 1 to 100. The default is 50. A value outside the range uses 50 | | `cursor` | The `next_cursor` of the previous page. See [Pagination](/guides/pagination) | `count` is the number of deliveries that match the filters across all pages. `next_cursor` is absent on the last page. Deliveries are sorted newest first. `GET /v1/webhooks/{id}/deliveries/{did}` returns the delivery with the exact `payload` that was sent and every attempt: ```json theme={null} { "id": "b1d4e7a2-6c3f-4a90-8e15-2f7c9d0a3b68", "endpoint_id": "3f6b0c1e-5a47-4d2b-9a0e-8c1d7b2e4f90", "event_type": "comment.received", "status": "failed", "response_status_code": 500, "response_duration_ms": 412, "attempt_count": 5, "description": "Instagram comment received", "payload": { "event": "comment.received", "data": { "id": "sapi_cmt_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I" } }, "attempts": [ { "attempt_number": 1, "status_code": 500, "response_body": "Internal Server Error", "duration_ms": 412, "created_at": "2026-10-13T14:30:01Z" } ], "created_at": "2026-10-13T14:30:00Z" } ``` Each attempt holds the first 10,240 bytes of your response body. An attempt that ended in a connection error has an `error_message` in place of a response. The example `payload` is shortened. The real one is the full body that was sent. `POST /v1/webhooks/{id}/deliveries/{did}/retry` creates a new delivery with the same event and payload and the description `Retry of {did}`. It returns the new delivery with `status` `pending`. The new delivery gets 10 attempts of its own. The original delivery is unchanged. SocialAPI keeps deliveries for the number of days your plan allows (see [History and export allowances](/guides/plans#history-and-export-allowances)). ## Test an endpoint `POST /v1/webhooks/{id}/test` sends one request to the endpoint and returns the result. You choose the event name and the body: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/webhooks/3f6b0c1e-5a47-4d2b-9a0e-8c1d7b2e4f90/test \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "event_type": "comment.received", "payload": { "event": "comment.received", "data": { "text": "Test comment" } } }' ``` Response (HTTP 200): ```json theme={null} { "delivery_id": "c8a2f5d1-9e47-4b63-a1d0-7e3b5c2f8a94", "status": "delivered", "response_status_code": 200, "response_body": "ok", "response_duration_ms": 87 } ``` SocialAPI sends `payload` exactly as you give it, as the request body. It does not wrap it. The request carries the signature headers and `X-SocialAPI-Event` set to `event_type`, but no `X-SocialAPI-Delivery`. `status` is `delivered` for a `2xx` response and `failed` otherwise, and `response_status_code` is `0` when the connection failed. SocialAPI does not retry a failed test. The delivery appears in the delivery list with the description `Test delivery`. `event_type` and `payload` are both required (`400 validation.field_required`). `event_type` can be any string, including names that are not subscribable. ## Security * **Verify every signature** with `X-SocialAPI-Signature-V2` and reject stale timestamps. * **Keep the secret out of source control.** Store it in an environment variable or a secret manager. * **Use HTTPS only.** SocialAPI rejects any other scheme at registration. * **Return `200` fast** and process afterward, to avoid timeouts and duplicate retries. * **Make handlers idempotent.** SocialAPI delivers each event at least once. * **Rotate by replacement.** There is no rotate call: register a new endpoint, switch your handler to its secret, then delete the old endpoint. ## Platform support Which platforms emit each event is listed under the event on [Webhook events](/guides/webhook-events). Platforms that push nothing to SocialAPI have no comment, DM, mention, or review events: read those with the inbox endpoints. ## Limits | Limit | Value | | - | - | | Response time per request | 10 seconds | | Attempts per delivery | 10 | | Response body kept per attempt | 10,240 bytes | | Deliveries per list page | 1 to 100, default 50 | | Delivery history | Per plan: [History and export allowances](/guides/plans#history-and-export-allowances) | | Endpoint URL scheme | `https` only | ## Errors and debugging When an event does not arrive, check in this order: 1. **Endpoint state.** `GET /v1/webhooks/{id}` must show `is_active: true` and the event name in `events`. An event is delivered only to endpoints subscribed to it when it occurs. 2. **Past due.** While your SocialAPI account is past due, SocialAPI sends no events and records no deliveries. Calls to the API return `402 billing.past_due`. 3. **Platform support.** Check **Emitted for** under the event on [Webhook events](/guides/webhook-events). A platform that does not emit the event never sends it. 4. **Deliveries.** `GET /v1/webhooks/{id}/deliveries?status=failed` lists failures. Read the attempts of one delivery: `status_code` is your endpoint's response, and `error_message` explains a connection error or timeout. 5. **Signature.** When your handler returns `401` on every request, check that you hash the raw body, key the HMAC with the secret string as returned, and compare against `X-SocialAPI-Signature-V2`. 6. **Test.** Send `POST /v1/webhooks/{id}/test` to separate your endpoint's behavior from SocialAPI's event delivery. Other codes on these endpoints: `401 auth.invalid_key` and its siblings for a missing or invalid key, `403 auth.insufficient_scope` when the key lacks `webhooks:manage`, and `404 resource.not_found` for an unknown endpoint or delivery. Every code is in [Errors](/guides/errors). ## Scopes | Scope | Grants | | - | - | | `webhooks:manage` | Every endpoint on this page except `GET /v1/webhooks/events` | A brand-restricted key is refused these endpoints with `403 auth.insufficient_scope`, even when it holds `webhooks:manage`, because endpoints belong to your SocialAPI account and not to one brand. See [Scoped API keys](/guides/scoped-keys). ## Use from an AI agent | Tool | What it does | | - | - | | `social_api_list_webhooks` | List endpoints | | `social_api_create_webhook` | Register an endpoint. Returns the secret once | | `social_api_update_webhook` | Change the URL, events, or `is_active` | | `social_api_delete_webhook` | Delete an endpoint | | `social_api_get_webhook` | Get an endpoint with its delivery counts | | `social_api_list_webhook_deliveries` | List deliveries, with filters | | `social_api_get_webhook_delivery` | Get one delivery with its attempts | | `social_api_retry_webhook_delivery` | Send a delivery again | | `social_api_test_webhook` | Send a test payload | See [MCP tools](/integrations/tools) for the full list. ## Related * [Webhook events](/guides/webhook-events): every event, its platforms, and its payload. * [Track post delivery](/guides/delivery): the post statuses and target errors that the `post.*` events report. * [Scoped API keys](/guides/scoped-keys): restrict a key to `webhooks:manage` for a webhook-only integration. * [Plans and usage](/guides/plans): delivery history length, and what happens while an account is past due. * [Errors](/guides/errors): every error code and how to handle it. * [Automations](/guides/automations): answer Instagram and Facebook comments and DMs without running a webhook handler. # SocialAPI Source: https://docs.social-api.ai/index Read and answer comments, DMs, mentions, and reviews, publish posts, and automate replies across social platforms with one REST API and MCP server. SocialAPI is a REST API and MCP server for the social accounts you connect. One set of endpoints reads and answers comments, direct messages (DMs), mentions, and reviews, publishes and schedules posts, and runs Automations that reply for you. Developers call the REST API with an API key, and AI agents use the MCP server, whose tools cover the inbox, publishing, Automations, and accounts. SocialAPI's own approved apps connect most platforms, so you register no app with Meta, TikTok, or Google. X uses your own X developer app, Telegram uses your bot token, and Bluesky uses an app password. See [Platform apps](/guides/platform-credentials). Plans and prices are in [Plans and usage](/guides/plans). > [card] [Quickstart](/quickstart) > > Get an API key, connect an account, read, reply, and publish. > [card] [Core concepts](/guides/concepts) > > Brand, account, post, interaction, and capability. > [card] [Use cases](/guides/use-cases) > > Five common ways to use the API. > [card] [Supported platforms](/platforms/overview) > > What each platform supports. > [card] [AI agents](/integrations/overview) > > Connect Claude, ChatGPT, Cursor, and more. > [card] [API reference](/api-reference/introduction) > > Every endpoint, request, and response. ## Supported platforms Each platform page lists what that platform supports. [Supported platforms](/platforms/overview) compares them feature by feature. | Platform | Slug | Connects through | Status | | - | - | - | - | | [Instagram](/platforms/instagram) | `instagram` | SocialAPI's app | Available | | [Facebook](/platforms/facebook) | `facebook` | SocialAPI's app | Available | | [Threads](/platforms/threads) | `threads` | SocialAPI's app | Available. Profile discovery is in beta | | [Google Business Profile](/platforms/google) | `google` | SocialAPI's app | Available | | [TikTok](/platforms/tiktok) | `tiktok` | SocialAPI's app | Available | | [LinkedIn](/platforms/linkedin) | `linkedin` | SocialAPI's app | Available | | [X / Twitter](/platforms/twitter) | `twitter` | Your own X app | Available | | [YouTube](/platforms/youtube) | `youtube` | SocialAPI's app | Available | | [Pinterest](/platforms/pinterest) | `pinterest` | SocialAPI's app | Available | | [Zalo](/platforms/zalo) | `zalo` | SocialAPI's app | Available | | [WhatsApp](/platforms/whatsapp) | `whatsapp` | SocialAPI's app | Available | | [Telegram](/platforms/telegram) | `telegram` | Your bot token | Available | | [Bluesky](/platforms/bluesky) | `bluesky` | Your app password | Available | | [App Store Connect](/platforms/app-store-connect) | `app_store_connect` | Your App Store Connect API key | Available | | [Trustpilot](/platforms/trustpilot) | `trustpilot` | Not connectable | Not available | ## Inbox The inbox reads and answers comments, DMs, reviews, and mentions through `/v1/inbox`. This request replies to a comment. A public reply costs 1 interaction credit. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/inbox/comments/17895695668004550 \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "comment_id": "sapi_cmt_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "text": "Thank you, Jane!" }' ``` See [Unified inbox](/guides/inbox). ## Automations An Automation answers Instagram and Facebook comments and DMs that match keywords, with no code running on your side. This request sends a private reply with a link to anyone who comments "link". ```bash theme={null} curl -X POST https://api.social-api.ai/v1/automations \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "name": "Send the link", "trigger": "comment", "steps": [{ "key": "start", "filters": [{ "type": "keyword", "keyword": { "keywords": ["link"], "match": "contains" } }], "actions": [{ "type": "private_reply", "private_reply": { "texts": ["Here is the link: https://example.com"] } }] }] }' ``` See [Automations](/guides/automations). ## Publish One request publishes a post to one or more connected accounts. Set `publish_now` to publish at once, or `scheduled_at` to publish later. Without either, the request saves a draft. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Hello from SocialAPI!", "media": [{ "source_type": "url", "source": "https://cdn.example.com/launch.jpg" }], "platform_data": { "instagram": { "content_type": "feed" } }, "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }] }' ``` See [Publishing](/guides/publishing), [Scheduling](/guides/scheduling), and [Media](/guides/media). ## Webhooks SocialAPI sends new comments, DMs, reviews, and post results to your server as signed webhook events. This request registers an endpoint. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/webhooks \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://app.example.com/webhooks/socapi", "events": ["comment.received", "dm.received"] }' ``` See [Webhooks](/guides/webhooks) and [Webhook events](/guides/webhook-events). ## AI agents The MCP server at `https://api.social-api.ai/mcp` gives AI clients tools for the inbox, publishing, Automations, and accounts: see [MCP tools](/integrations/tools). This command adds it to Claude Code. ```bash theme={null} claude mcp add socialapi --transport http https://api.social-api.ai/mcp ``` See [MCP server](/integrations/overview) for every supported client. # ChatGPT Source: https://docs.social-api.ai/integrations/chatgpt Connect SocialAPI to ChatGPT on web, desktop, and mobile with a custom connector to manage comments, DMs, and posts in plain language. Connect SocialAPI to ChatGPT and ask it to list your connected accounts, reply to comments, send direct messages (DMs), and publish posts. ChatGPT connects to SocialAPI's MCP server at `https://api.social-api.ai/mcp` and signs in with OAuth 2.1, so you create no API key. ## Prerequisites * A SocialAPI account with at least one connected account. * A ChatGPT Plus, Pro, Business, Enterprise, or Education plan. Developer mode is not available on the Free plan. * A desktop browser at [chatgpt.com](https://chatgpt.com). You can create a connector only on the web. The desktop and mobile apps use a connector you created there. ## Set up on web **1. Turn on Developer mode** Go to **Settings** → **Security and login** and switch **Developer mode** on. The toggle is not under **Apps & Connectors** → **Advanced settings**. **2. Create the connector** Go to **Settings** → **Connectors** and click **Create**. The **Create** button appears only while Developer mode is on. Fill in: * **Name:** `SocialAPI` * **Description:** `Manage social media accounts, posts, comments, DMs, reviews, and mentions` * **Connector URL:** `https://api.social-api.ai/mcp` ChatGPT tests the connection and lists the available tools. ![ChatGPT create connector dialog](https://mintcdn.com/socialapi/LjBjrwhsSNWqN2AY/images/mcp-chatgpt-create.png?fit=max\&auto=format\&n=LjBjrwhsSNWqN2AY\&q=85\&s=fc2dfe732ec0cb5dc2768a14ac4c4ae1) **3. Authorize with your SocialAPI account** The first time you use a SocialAPI tool, a browser window opens on the SocialAPI authorization page. Log in and click **Authorize**. ChatGPT finds the authorization server on its own, so you enter no authentication settings. ![ChatGPT authorization screen for SocialAPI](https://mintcdn.com/socialapi/LjBjrwhsSNWqN2AY/images/mcp-chatgpt-add-server.png?fit=max\&auto=format\&n=LjBjrwhsSNWqN2AY\&q=85\&s=868766fc0b2ad08b0207252435093f07) **4. Turn the connector on in a conversation** Open a new conversation, click **+** near the message box, select **More**, and switch the SocialAPI connector on. ChatGPT shows connectors as apps in parts of its interface: they are the same thing. ## Use it on desktop and mobile A connector you created on chatgpt.com is also available in the ChatGPT desktop and mobile apps (iOS and Android). You cannot create a connector in those apps. **1. Create the connector on the web** Follow [Set up on web](#set-up-on-web). **2. Open the app** Open the ChatGPT desktop or mobile app. The connector is already there. **3. Turn the connector on in a conversation** Open the **+** menu and switch the SocialAPI connector on. ## Try it Ask ChatGPT: ```text theme={null} List my connected social accounts ``` ChatGPT calls `social_api_list_accounts` and answers with your connected accounts and their platforms. ## Troubleshooting > [accordion] There is no Developer mode toggle > > Developer mode needs a paid ChatGPT plan: Plus, Pro, Business, Enterprise, or Education. On those plans, look under **Settings** → **Security and login**. > [accordion] There is no Create button under Connectors > > **Create** appears only while Developer mode is on. Switch Developer mode on first. > [accordion] ChatGPT cannot reach the server > > Use the URL exactly as shown: `https://api.social-api.ai/mcp`. > [accordion] Tool calls fail with HTTP 401 > > The OAuth session expired. Remove the SocialAPI connector and create it again to sign in again. ## Related * [SocialAPI MCP server](/integrations/overview): the server URL and the other supported clients. * [MCP tools reference](/integrations/tools): every tool ChatGPT can call. * [OAuth for MCP clients](/integrations/oauth): how the sign-in works. * [Errors](/guides/errors): every error code a tool can return. # Claude Source: https://docs.social-api.ai/integrations/claude Connect SocialAPI to Claude on claude.ai, Claude Desktop, Claude Code, and mobile to manage comments, DMs, and posts in plain language. Connect SocialAPI to Claude and ask it to list your connected accounts, reply to comments, send direct messages (DMs), and publish posts. Claude connects to SocialAPI's MCP server at `https://api.social-api.ai/mcp` and signs in with OAuth 2.1, so you create no API key. ## Prerequisites * A SocialAPI account with at least one connected account. * For claude.ai, Claude Desktop, and mobile: a Claude Free, Pro, Max, Team, or Enterprise plan. * For Claude Code: [Claude Code](https://docs.claude.com/en/docs/claude-code) installed and signed in. Claude Desktop, claude.ai, and the mobile app share one account. A connector you add on one surface appears on the others. ## Set up claude.ai **1. Open Connectors** Click your profile icon, then go to **Settings** → **Connectors**. You can also open [claude.ai/customize/connectors](https://claude.ai/customize/connectors). ![Claude Settings page with the Connectors section](https://mintcdn.com/socialapi/dkXBPl2dMHhtYTy4/images/mcp-claude-settings.png?fit=max\&auto=format\&n=dkXBPl2dMHhtYTy4\&q=85\&s=30db5cedcb288a0af760b2f1a37231de) **2. Connect SocialAPI** SocialAPI is in Claude's connectors directory. Search for **SocialAPI** and click **Connect**. Claude already knows the server URL, so you type nothing. ![SocialAPI connector in the Claude connectors directory](https://mintcdn.com/socialapi/Nif1EwawS0vMng4O/images/mcp-claude-directory.png?fit=max\&auto=format\&n=Nif1EwawS0vMng4O\&q=85\&s=bb62f6be70f58d1f05157d56536d9469) On a Team plan, a member who cannot enable connectors sees **Request** instead of **Connect**. The request goes to an organization admin, who enables the connector in **Organization settings** → **Connectors**. The member then clicks **Connect** in **Settings** → **Connectors**. **3. Authorize with your SocialAPI account** A browser window opens on the SocialAPI authorization page. Log in and click **Authorize**. The window closes when the sign-in is done. ![SocialAPI authorization screen](https://mintcdn.com/socialapi/dkXBPl2dMHhtYTy4/images/mcp-claude-oauth.png?fit=max\&auto=format\&n=dkXBPl2dMHhtYTy4\&q=85\&s=e3d502da0cd17c1632a2ccda01c85335) **4. Turn the connector on in a conversation** Open a new conversation, click **+** at the bottom left of the input, select **Connectors**, and switch **SocialAPI** on. ![Claude conversation using SocialAPI tools to list accounts](https://mintcdn.com/socialapi/dkXBPl2dMHhtYTy4/images/mcp-claude-conversation.png?fit=max\&auto=format\&n=dkXBPl2dMHhtYTy4\&q=85\&s=66d8c7c104239deb3583877b8c4e0bab) ## Set up Claude Desktop Claude Desktop adds remote servers through **Settings** → **Connectors**, the same flow as claude.ai. **1. Open Connectors** In Claude Desktop ([download](https://claude.ai/download)), go to **Settings** → **Connectors**. **2. Add SocialAPI** Search the directory for **SocialAPI** and click **Connect**. To add it by URL instead, click **Add custom connector** (the Free plan allows 1 custom connector), enter `SocialAPI` as the name and `https://api.social-api.ai/mcp` as the URL. **3. Authorize** Complete the sign-in in the browser window that opens. > [!WARNING] > > `claude_desktop_config.json` holds local stdio servers only (`command` and `args`). A remote server such as SocialAPI cannot go in that file: add it in **Settings** → **Connectors**. ## Set up Claude Code **1. Add the server** ```bash theme={null} claude mcp add socialapi --transport http https://api.social-api.ai/mcp ``` By default the server is added to the local scope, which is the current project only. Add `--scope user` to use it in all projects, or `--scope project` to share it through `.mcp.json`: ```bash theme={null} claude mcp add socialapi --transport http --scope user https://api.social-api.ai/mcp ``` **2. Authorize** Run `/mcp` in Claude Code, select **SocialAPI**, and complete the sign-in in your browser. You can also run `claude mcp login socialapi`. Log in and click **Authorize**. On a machine without a browser, Claude Code prints the authorization URL. Open it on another machine, then paste the full redirect URL from that browser's address bar back at the prompt. **3. Check the server** ```bash theme={null} claude mcp list ``` To remove the server, run `claude mcp remove socialapi`. ## Set up mobile (iOS and Android) **1. Open Connectors** In the Claude app, tap your profile icon, then go to **Settings** → **Connectors**. **2. Connect SocialAPI** Search for **SocialAPI** and tap **Connect**. **3. Authorize** Complete the sign-in in the browser sheet that opens. **4. Turn the connector on in a conversation** Open the **+** menu, select **Connectors**, and switch **SocialAPI** on. ## Try it Ask Claude: ```text theme={null} List my connected social accounts ``` Claude calls `social_api_list_accounts` and answers with your connected accounts and their platforms. Then try "Show me the latest comments on my Instagram posts". ## Troubleshooting > [accordion] Connect shows Request instead > > Your Team plan does not let you enable connectors yourself. Send the request, and an organization admin enables SocialAPI in **Organization settings** → **Connectors**. > [accordion] Claude Code does not open a browser > > Claude Code uses your system default browser. Set one, or run `claude mcp login socialapi --no-browser` and open the authorization URL that Claude Code prints. > [accordion] SocialAPI tools are missing in a conversation > > Open **+** → **Connectors** and check that **SocialAPI** is on. A conversation does not use a connector that is off. > [accordion] Tool calls fail with HTTP 401 > > The OAuth session expired. Remove the SocialAPI connector and add it again to sign in again. ## Related * [SocialAPI MCP server](/integrations/overview): the server URL and the other supported clients. * [MCP tools reference](/integrations/tools): every tool Claude can call. * [OAuth for MCP clients](/integrations/oauth): how the sign-in works. * [Errors](/guides/errors): every error code a tool can return. # Codex Source: https://docs.social-api.ai/integrations/codex Connect SocialAPI to OpenAI Codex (CLI, IDE extension, and ChatGPT desktop app) with one entry in ~/.codex/config.toml. Connect SocialAPI to Codex and ask it to list your connected accounts, reply to comments, send direct messages (DMs), and publish posts. Codex reads MCP servers from `~/.codex/config.toml`. The Codex CLI, the Codex IDE extension, and the ChatGPT desktop app share that file, so one entry gives every Codex session the SocialAPI tools. The server is `https://api.social-api.ai/mcp`. ## Prerequisites * [Codex](https://developers.openai.com/codex) installed: CLI, IDE extension, or desktop app. * A SocialAPI account with at least one connected account. * For a machine without a browser: an [API key](/integrations/custom). ## Set up with the CLI **1. Add the server** ```bash theme={null} codex mcp add socialapi --url https://api.social-api.ai/mcp ``` Codex saves the server, detects that it uses OAuth 2.1 with PKCE, and opens your browser on the SocialAPI authorization page. Codex stores no API key and refreshes the token itself. **2. Authorize** Log in and click **Authorize**. The terminal prints `Successfully logged in to MCP server 'socialapi'`. **3. Check the server** ```bash theme={null} codex mcp list ``` `socialapi` is listed as `enabled` with its auth method. In the Codex TUI, type `/mcp` to see the connected servers and their tools. To sign in again on a new machine or after a revoked session, run `codex mcp login socialapi`. ## Set up with the config file **1. Add the entry** `codex mcp add` writes this entry to `~/.codex/config.toml`. You can also add it by hand, globally or in a trusted project's `.codex/config.toml`: ```toml theme={null} [mcp_servers.socialapi] url = "https://api.social-api.ai/mcp" ``` **2. Authorize** ```bash theme={null} codex mcp login socialapi ``` ## Set up a headless machine with an API key For CI jobs, containers, and any machine without a browser, skip OAuth and authenticate with an API key. Codex reads the key from an environment variable and sends it as `Authorization: Bearer`. **1. Reference the variable** ```toml theme={null} [mcp_servers.socialapi] url = "https://api.social-api.ai/mcp" bearer_token_env_var = "SOCAPI_KEY" ``` **2. Set the variable** ```bash theme={null} export SOCAPI_KEY=sapi_key_xxxxxxxxxxxxxxxx ``` > [!WARNING] > > Keep the key in the environment or a secret manager. Never paste it into `config.toml` or commit it. To limit what the agent can do, create a [scoped API key](/guides/scoped-keys). ## Limit the tools To give Codex fewer tools, list the ones to keep in `enabled_tools`, or the ones to hide in `disabled_tools`. Codex applies `disabled_tools` after `enabled_tools`: ```toml theme={null} [mcp_servers.socialapi] url = "https://api.social-api.ai/mcp" disabled_tools = [ "social_api_delete_post", "social_api_delete_comment", "social_api_disconnect_account", "social_api_revoke_key", ] ``` See [MCP tools reference](/integrations/tools) for every tool name. ## Try it Ask Codex: ```text theme={null} List my connected social accounts ``` Codex calls `social_api_list_accounts` and answers with your connected accounts and their platforms. ## Troubleshooting > [accordion] The browser does not open > > Codex prints the authorization URL (`Authorize socialapi by opening this URL in your browser`). Open it on the same machine: the callback goes to a local port that Codex listens on. > [accordion] HTTP 401 with an API key > > The variable named in `bearer_token_env_var` is not set in the shell that started Codex, or the key is revoked. Check the key in the [dashboard](https://app.social-api.ai/api-configuration?tab=keys). > [accordion] Tools are missing in the IDE extension or desktop app > > They read the same `~/.codex/config.toml`. Restart the app after you edit the file. ## Related * [SocialAPI MCP server](/integrations/overview): the server URL and the other supported clients. * [MCP tools reference](/integrations/tools): every tool Codex can call. * [Custom MCP clients](/integrations/custom): API key setup in detail. * [OAuth for MCP clients](/integrations/oauth): how the sign-in works. # Cursor Source: https://docs.social-api.ai/integrations/cursor Connect SocialAPI to Cursor with a config file or the Settings UI so Agent mode can manage comments, DMs, and posts in plain language. Connect SocialAPI to Cursor and ask Agent mode to list your connected accounts, reply to comments, send direct messages (DMs), and publish posts. Cursor connects to SocialAPI's MCP server at `https://api.social-api.ai/mcp` and signs in with OAuth 2.1, so you create no API key and put no credentials in the config. ## Prerequisites * [Cursor](https://cursor.com) installed. * A SocialAPI account with at least one connected account. ## Set up with the config file **1. Add the server** Add this to the project config `.cursor/mcp.json` or the global config `~/.cursor/mcp.json`: ```json theme={null} { "mcpServers": { "SocialAPI": { "url": "https://api.social-api.ai/mcp" } } } ``` **2. Authorize** The first time you use a SocialAPI tool, Cursor opens your browser on the SocialAPI authorization page. Log in and click **Authorize**. Cursor stores and refreshes the token. Cursor supports both the SSE and Streamable HTTP transports for remote servers. ## Set up with the Settings UI **1. Open MCP settings** Open **Cursor Settings** (gear icon) and go to **Tools & Integrations**. **2. Add the server** Under **MCP Tools**, click **Add Custom MCP** and enter `https://api.social-api.ai/mcp` as the URL. **3. Authorize** Click **Needs login** and authorize in the browser window that opens. ## Try it Open **Agent** mode and ask: ```text theme={null} List my connected social accounts ``` SocialAPI tools appear under **Available Tools**, and Cursor calls them when a request needs them. You can also name a tool in your prompt. Cursor calls `social_api_list_accounts` and answers with your connected accounts and their platforms. ## Troubleshooting > [accordion] The connection fails > > Open the **Output** panel (`Cmd+Shift+U` on macOS, `Ctrl+Shift+U` on Windows and Linux) and select **MCP Logs** in the dropdown. Check that the URL is exactly `https://api.social-api.ai/mcp`. > [accordion] The server shows Needs login > > Click **Needs login** to run the sign-in. Authorization has not happened yet, or the session expired. > [accordion] Tool calls fail with HTTP 401 > > The OAuth session expired. Remove the SocialAPI server from Cursor and add it again to sign in again. ## Related * [SocialAPI MCP server](/integrations/overview): the server URL and the other supported clients. * [MCP tools reference](/integrations/tools): every tool Cursor can call. * [OAuth for MCP clients](/integrations/oauth): how the sign-in works. * [Errors](/guides/errors): every error code a tool can return. # Custom MCP clients Source: https://docs.social-api.ai/integrations/custom Connect any MCP client to the SocialAPI MCP server with an API key for headless agents, server-side scripts, and CI jobs. Connect any MCP client to SocialAPI's MCP server at `https://api.social-api.ai/mcp` with an API key sent on the `Authorization: Bearer` header. Use this setup when your client does not implement OAuth 2.1, or when the agent runs without a browser: on a server, in a container, in a CI job, or on a schedule. ## Prerequisites * A SocialAPI account with at least one connected account. * An API key from the [dashboard](https://app.social-api.ai/api-configuration?tab=keys) or from `POST /v1/keys`. A key starts with `sapi_key_`. See [Authentication](/guides/authentication). > [!WARNING] > > An API key is a long-lived credential. Keep it in a secret manager or an environment variable, never in source control. ## Set up the config file **1. Add the server** Most MCP clients accept a custom `Authorization` header in the server config. Set it to your API key prefixed with `Bearer `: ```json theme={null} { "mcpServers": { "SocialAPI": { "url": "https://api.social-api.ai/mcp", "headers": { "Authorization": "Bearer sapi_key_xxxxxxxxxxxxxxxx" } } } } ``` **2. Match your client's field name** The field name differs by client. Some clients use `headers`, others use `requestInit.headers` or a top-level `auth` block. Check your client's MCP documentation. ## Call the server over HTTP The MCP endpoint speaks Streamable HTTP. Send each tool call as a JSON-RPC message in a `POST` request. The `Authorization` header is required on every request, and API key authentication has no `/oauth/authorize` redirect and no PKCE exchange. ```bash theme={null} curl https://api.social-api.ai/mcp \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }' ``` The response lists every tool with its input schema. Tool names start with `social_api_`: see [MCP tools reference](/integrations/tools). ## Limit what the agent can do A full-access API key can run every tool. To restrict an agent to some operations or some brands, issue it a [scoped API key](/guides/scoped-keys). A tool outside the key's scopes returns `auth.insufficient_scope`, with the missing scope in `required_scope`. `required_scope` is absent for tools that need a full-access key: see [Scoped API keys](/guides/scoped-keys). Plan allowances and platform support apply to a tool exactly as they do to its REST equivalent. ## Rotate a key Rotate a key with `POST /v1/keys/{id}/rotate`. It returns a new secret for the same key, keeps its scopes and brands, and ends the old secret at once. Update the agent's config with the new secret. See [Authentication](/guides/authentication#rotate-a-key). To revoke a key instead, use `DELETE /v1/keys/{id}` or the dashboard. A tool call that uses a revoked key returns HTTP 401. ## Try it Run the `tools/list` request above. A successful response is HTTP 200 and lists tools such as `social_api_list_accounts`. Then call that tool: ```bash theme={null} curl https://api.social-api.ai/mcp \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "social_api_list_accounts", "arguments": {} } }' ``` The result lists your connected accounts. ## Troubleshooting > [accordion] HTTP 401 with invalid or inactive API key > > A wrong, revoked, or rotated `sapi_key_` key returns `{"error": "invalid or inactive API key"}`. A token without the `sapi_key_` prefix that is not a valid OAuth 2.1 access token returns `{"error": "invalid token"}`. Check the key in the [dashboard](https://app.social-api.ai/api-configuration?tab=keys). > [accordion] HTTP 401 with missing Authorization header > > Your client is not sending the header. Check the field name in its MCP config, and check that the header reaches the server through any proxy in between. > [accordion] HTTP 402 on every call > > Every MCP request returns HTTP 402 `billing.past_due` until the invoice is paid. See [Plans](/guides/plans#past-due-accounts) for how access returns. > [accordion] A tool is not found > > Run `tools/list` to read the exact tool names. Every name starts with `social_api_`: see [MCP tools reference](/integrations/tools). ## Related * [SocialAPI MCP server](/integrations/overview): the server URL and the supported clients. * [MCP tools reference](/integrations/tools): every tool and the endpoint it runs. * [Authentication](/guides/authentication): create, rotate, and revoke API keys. * [Scoped API keys](/guides/scoped-keys): restrict an agent to some scopes and brands. * [Errors](/guides/errors): every error code a tool can return. # Hermes Agent Source: https://docs.social-api.ai/integrations/hermes Connect SocialAPI to Nous Research's Hermes Agent with one mcp_servers entry, then publish and reply from any chat Hermes is connected to. Connect SocialAPI to [Hermes Agent](https://hermes-agent.nousresearch.com) and ask it to list your connected accounts, reply to comments, send direct messages (DMs), and publish posts from `hermes chat` or from any messaging channel your Hermes gateway runs. Hermes reads MCP servers from `~/.hermes/config.yaml`. The server is `https://api.social-api.ai/mcp`. ## Prerequisites * Hermes Agent installed with MCP support. The standard install includes it: see [Troubleshooting](#troubleshooting) if you installed from PyPI. * A SocialAPI account with at least one connected account. * For a gateway with no browser: an [API key](/integrations/custom). ## Set up with OAuth **1. Add the server** Add this to `~/.hermes/config.yaml`: ```yaml theme={null} mcp_servers: socialapi: url: "https://api.social-api.ai/mcp" auth: oauth ``` **2. Sign in** ```bash theme={null} hermes mcp login socialapi ``` Hermes opens your browser on the SocialAPI authorization page. Log in and click **Authorize**. When the callback arrives, Hermes prints `Authenticated` with the number of tools available. Hermes caches the token in `~/.hermes/mcp-tokens/socialapi.json` and refreshes it automatically. If Hermes runs on a remote host, the browser callback cannot reach it. Paste the redirect URL into the prompt that `hermes mcp login` shows, or authorize from Hermes Desktop, which relays the callback. **3. Start using it** Start a new session with `hermes chat`, or type `/reload-mcp` in a running one. A running messaging gateway (`hermes gateway run`) watches `config.yaml` and connects the new server, so the same tools work from Telegram, Discord, or any other channel routed to Hermes. Hermes registers the tools with an `mcp_socialapi_` prefix, for example `mcp_socialapi_social_api_list_accounts`. You never need to call a tool by name. ## Set up a headless gateway with an API key For a gateway with no browser, authenticate with an API key instead of OAuth. **1. Store the key** Put the key in `~/.hermes/.env`: ```bash theme={null} SOCAPI_KEY=sapi_key_xxxxxxxxxxxxxxxx ``` **2. Reference the key** Reference it from `config.yaml`, without `auth: oauth`: ```yaml theme={null} mcp_servers: socialapi: url: "https://api.social-api.ai/mcp" headers: Authorization: "Bearer ${SOCAPI_KEY}" ``` Hermes substitutes `${SOCAPI_KEY}` when it runs, so the key never sits in `config.yaml`. To limit what the agent can do, create a [scoped API key](/guides/scoped-keys). ## Limit the tools Keep a subset of tools with `tools.include`, or hide some with `tools.exclude`: ```yaml theme={null} mcp_servers: socialapi: url: "https://api.social-api.ai/mcp" auth: oauth tools: exclude: - social_api_delete_post - social_api_delete_comment - social_api_disconnect_account - social_api_revoke_key ``` You can also switch tools on and off interactively with `hermes mcp configure socialapi`. See [MCP tools reference](/integrations/tools) for every tool name. ## Try it Run `hermes mcp test socialapi` to check that Hermes connects, authenticates, and counts the tools. Then ask Hermes: ```text theme={null} Reply to every unanswered Google review from this week with a short thank-you. ``` Hermes lists your Google Business Profile reviews, finds the ones without a reply, and posts a reply to each. ## Troubleshooting > [accordion] mcp.client.streamable\_http is not available > > Hermes was installed without its MCP dependencies, for example with `uv tool install hermes-agent`. Reinstall with the extra: `uv tool install --force 'hermes-agent[mcp]'`. > [accordion] non-interactive environment and no cached tokens found > > The first OAuth sign-in needs a terminal. Run `hermes mcp login socialapi` interactively once, or use the API key setup. > [accordion] 401 Unauthorized > > The token or key is wrong or revoked. Run `hermes mcp login socialapi` again, or check the key in the [dashboard](https://app.social-api.ai/api-configuration?tab=keys). ## Related * [SocialAPI MCP server](/integrations/overview): the server URL and the other supported clients. * [MCP tools reference](/integrations/tools): every tool Hermes can call. * [Custom MCP clients](/integrations/custom): API key setup in detail. * [Reviews](/guides/reviews): read and reply to Google Business Profile reviews. # Mistral / Le Chat Source: https://docs.social-api.ai/integrations/mistral Connect SocialAPI to Le Chat from Mistral on web and mobile with a custom MCP connector to manage comments, DMs, and posts. Connect SocialAPI to Le Chat and ask it to list your connected accounts, reply to comments, send direct messages (DMs), and publish posts. Le Chat connects to SocialAPI's MCP server at `https://api.social-api.ai/mcp` and signs in with OAuth 2.1, so you create no API key. ## Prerequisites * A SocialAPI account with at least one connected account. * A Le Chat account on any plan. MCP connectors are available on the Free plan. ## Set up on web **1. Open Connectors** Open [chat.mistral.ai](https://chat.mistral.ai), open the side panel, expand **Intelligence**, and click **Connectors**. **2. Add a custom connector** Click **+ Add Connector** on the right, then select the **Custom MCP Connector** tab. ![Le Chat custom MCP connector form](https://mintcdn.com/socialapi/nk4ZXwmImjgLZWZ4/images/mcp-lechat-settings.png?fit=max\&auto=format\&n=nk4ZXwmImjgLZWZ4\&q=85\&s=e11ae528c565dc2a979b0a4660e7a99e) **3. Fill in the form** * **Connector name:** `SocialAPI` * **Connection server:** `https://api.social-api.ai/mcp` * **Description** (optional): `Manage social media accounts, posts, comments, DMs, and more` Click **Connect**. **4. Authorize with your SocialAPI account** Le Chat detects the OAuth 2.1 sign-in and opens the SocialAPI authorization page. Log in and click **Authorize**. ![Le Chat authorization screen for SocialAPI](https://mintcdn.com/socialapi/IQn1fcUN4-PkmH54/images/mcp-lechat-oauth.png?fit=max\&auto=format\&n=IQn1fcUN4-PkmH54\&q=85\&s=5c7ceb5d33900cc25995c2dca64778b4) **5. Turn the connector on in a conversation** Click **Tools** (the four-squares icon) below the message box and check **SocialAPI**. You can also type `/SocialAPI` in the message box. Le Chat asks for approval before each tool call. Select **Always Allow** to skip the prompt for that tool in future. Le Chat labels the Connectors feature as Beta. ## Use it on mobile (iOS and Android) A connector you added on the web appears in the Le Chat mobile apps. **1. Add the connector on the web** Follow [Set up on web](#set-up-on-web). **2. Open the app** Open the Le Chat mobile app. The connector is already there. **3. Turn the connector on in a conversation** Open the **Tools** menu and check **SocialAPI**. ## Try it Ask Le Chat: ```text theme={null} List my connected social accounts ``` Le Chat calls `social_api_list_accounts` and answers with your connected accounts and their platforms. ![Le Chat conversation using SocialAPI tools](https://mintcdn.com/socialapi/IQn1fcUN4-PkmH54/images/mcp-lechat-conversation.png?fit=max\&auto=format\&n=IQn1fcUN4-PkmH54\&q=85\&s=3b197e62b8c43e9b120102520b0b5a0b) ## Troubleshooting > [accordion] Le Chat does not detect the sign-in > > Use the URL exactly as shown: `https://api.social-api.ai/mcp`. > [accordion] SocialAPI is missing from the Tools menu > > Check that the connector shows as connected in **Intelligence** → **Connectors**. Then check **SocialAPI** in **Tools** for the conversation. > [accordion] Tool calls fail with HTTP 401 > > The OAuth session expired. Remove the SocialAPI connector and add it again to sign in again. ## Related * [SocialAPI MCP server](/integrations/overview): the server URL and the other supported clients. * [MCP tools reference](/integrations/tools): every tool Le Chat can call. * [OAuth for MCP clients](/integrations/oauth): how the sign-in works. * [Errors](/guides/errors): every error code a tool can return. # OAuth for MCP clients Source: https://docs.social-api.ai/integrations/oauth Authenticate an MCP client or your own app against SocialAPI with OAuth 2.1, PKCE, and dynamic client registration, then call the API for a user. SocialAPI runs an OAuth 2.1 authorization server so that an MCP client or your own app can act for a SocialAPI user without handling an API key. The user approves the app on SocialAPI's consent screen, and the app receives an access token it sends as `Authorization: Bearer ` to the MCP server at `https://api.social-api.ai/mcp` and to every public `/v1` endpoint. Claude, ChatGPT, Cursor, and other MCP clients that support OAuth run this flow on their own once you add the server URL: see [the MCP server](/integrations/overview). Build the flow yourself only for a client you write. The server follows RFC 8414 (metadata), RFC 7591 (dynamic client registration), RFC 7636 (PKCE, required), RFC 7009 (revocation), and RFC 9728 (protected resource metadata). ## Endpoints | Method | Path | Description | | - | - | - | | `GET` | `/.well-known/oauth-protected-resource` | Protected resource metadata for the MCP server. Also served at `/.well-known/oauth-protected-resource/mcp` | | `GET` | `/.well-known/oauth-authorization-server` | Authorization server metadata | | `POST` | `/oauth/register` | Register a client | | `GET` | `/oauth/authorize` | Send the user to the consent screen | | `POST` | `/oauth/token` | Exchange a code for tokens, or refresh them | | `POST` | `/oauth/revoke` | Revoke a refresh token | | `GET` | `/.well-known/mcp/server-card.json` | The MCP server card, for directories that list MCP servers | Every path is on `https://api.social-api.ai`, with no `/v1` prefix and no API key. ## Discover the server The protected resource metadata names the MCP server and its authorization server: ```bash theme={null} curl https://api.social-api.ai/.well-known/oauth-protected-resource ``` Response (HTTP 200): ```json theme={null} { "resource": "https://api.social-api.ai/mcp", "authorization_servers": ["https://api.social-api.ai"], "bearer_methods_supported": ["header"], "scopes_supported": ["social:all"] } ``` The authorization server metadata lists every endpoint: ```bash theme={null} curl https://api.social-api.ai/.well-known/oauth-authorization-server ``` Response (HTTP 200): ```json theme={null} { "issuer": "https://api.social-api.ai", "authorization_endpoint": "https://api.social-api.ai/oauth/authorize", "token_endpoint": "https://api.social-api.ai/oauth/token", "registration_endpoint": "https://api.social-api.ai/oauth/register", "revocation_endpoint": "https://api.social-api.ai/oauth/revoke", "response_types_supported": ["code"], "grant_types_supported": ["authorization_code", "refresh_token"], "token_endpoint_auth_methods_supported": ["client_secret_post"], "code_challenge_methods_supported": ["S256"], "scopes_supported": ["social:all"] } ``` ## Run the flow **1. Register your client** Registration is open: any app can register, with no API key. ```bash theme={null} curl -X POST https://api.social-api.ai/oauth/register \ -H "Content-Type: application/json" \ -d '{ "client_name": "Acme Assistant", "redirect_uris": ["https://app.example.com/oauth/socialapi/callback"] }' ``` Response (HTTP 201): ```json theme={null} { "client_id": "9f2c4e6a8b1d3f5a7c9e1b3d5f7a9c2e", "client_secret": "c572b7d490c47214440a3f049e0582a4d03e012b0a48b9ef8d2aeaa7c5e36f8c", "client_id_issued_at": 1791997200, "client_secret_expires_at": 0, "client_name": "Acme Assistant", "redirect_uris": ["https://app.example.com/oauth/socialapi/callback"], "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "client_secret_post" } ``` `client_name` and at least one entry in `redirect_uris` are required, or the request returns `400 validation.field_required`. Each redirect URI must use `http` or `https` and must not contain a user name, a password, or a `#fragment`; otherwise the request returns `400 validation.field_invalid`. `client_secret` is shown once and never expires (`client_secret_expires_at` is `0`). **2. Send the user to the consent screen** Generate a PKCE code verifier, derive its `S256` challenge, and redirect the user's browser to: ```text theme={null} https://api.social-api.ai/oauth/authorize?client_id=9f2c4e6a8b1d3f5a7c9e1b3d5f7a9c2e&redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fsocialapi%2Fcallback&response_type=code&scope=social%3Aall&state=Zx81Kq&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256 ``` SocialAPI checks the request, then sends the user to the SocialAPI dashboard. The user signs in to SocialAPI, sees your `client_name`, and approves or denies. The consent screen accepts the request for 1 hour. | Parameter | Required | Description | | - | - | - | | `client_id` | Yes | From registration | | `redirect_uri` | Yes | Must equal one of the client's registered URIs, character for character | | `code_challenge` | Yes | The PKCE challenge | | `code_challenge_method` | Yes | Must be `S256` | | `response_type` | No | `code` | | `scope` | No | `social:all`, the default and the only scope | | `state` | No | Your value, returned unchanged with the code | A missing `client_id`, `redirect_uri`, or `code_challenge` returns `400 validation.field_required`. A `code_challenge_method` other than `S256` returns `400 validation.field_invalid`. An unknown `client_id` returns `400 auth.oauth.invalid_client`, and an unregistered `redirect_uri` returns `400 auth.oauth.invalid_redirect`. These errors are JSON responses to the browser, not redirects. **3. Receive the code** After approval, the browser lands on your redirect URI: ```text theme={null} https://app.example.com/oauth/socialapi/callback?code=f1f59823f4e9ff94e434e2c1a633d9c5830e62cdbd43eb8ef97ccabe15fbcf41&state=Zx81Kq ``` When the user denies, the redirect carries `error=access_denied` and your `state` instead. The code expires 10 minutes after it is issued and works once. **4. Exchange the code for tokens** ```bash theme={null} curl -X POST https://api.social-api.ai/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=f1f59823f4e9ff94e434e2c1a633d9c5830e62cdbd43eb8ef97ccabe15fbcf41" \ -d "client_id=9f2c4e6a8b1d3f5a7c9e1b3d5f7a9c2e" \ -d "client_secret=c572b7d490c47214440a3f049e0582a4d03e012b0a48b9ef8d2aeaa7c5e36f8c" \ -d "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" \ -d "redirect_uri=https://app.example.com/oauth/socialapi/callback" ``` Response (HTTP 200): ```json theme={null} { "access_token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTBlODQwMCJ9.c2lnbmF0dXJl", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "d3161495b7d3f064afd3b5aa7876503f0ff9273896ea7e9257bef82a809defcf", "scope": "social:all" } ``` `code`, `client_id`, and `code_verifier` are required. If you send `client_secret`, SocialAPI verifies it. If you omit it, PKCE alone protects the exchange. `redirect_uri` is checked when you send it, and must equal the one used in step 2. An exchange that passes the `client_id` and `client_secret` check still uses up the code when it fails because the code was issued to another client, `redirect_uri` does not match, or PKCE verification fails: send the user through step 2 again. **5. Call the API** Send the access token as the Bearer token: ```bash theme={null} curl https://api.social-api.ai/v1/accounts \ -H "Authorization: Bearer eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTBlODQwMCJ9.c2lnbmF0dXJl" ``` ## Tokens | Token | Lifetime | Notes | | - | - | - | | Access token | 1 hour (`expires_in: 3600`) | Full access to the SocialAPI account of the user who approved. Not limited by scopes or brands | | Refresh token | 30 days | Works once. Each refresh returns a new refresh token and ends the old one | | Authorization code | 10 minutes | Works once | An expired or invalid access token returns `401 auth.invalid_token` on `/v1` endpoints. An access token whose user no longer exists returns `401 auth.user_not_found`. For a key with narrower access, use a [scoped API key](/guides/scoped-keys) instead. ## Refresh the access token ```bash theme={null} curl -X POST https://api.social-api.ai/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=refresh_token" \ -d "refresh_token=d3161495b7d3f064afd3b5aa7876503f0ff9273896ea7e9257bef82a809defcf" \ -d "client_id=9f2c4e6a8b1d3f5a7c9e1b3d5f7a9c2e" \ -d "client_secret=c572b7d490c47214440a3f049e0582a4d03e012b0a48b9ef8d2aeaa7c5e36f8c" ``` The response (HTTP 200) has the same shape as the code exchange, with a new access token and a new refresh token. Store the new refresh token: the one you sent stops working. `refresh_token` and `client_id` are required. If you send `client_secret`, SocialAPI verifies it. If you omit it, the refresh token alone authorizes the refresh (PKCE does not apply to a refresh). ## Client authentication The token endpoint reads `client_id` and `client_secret` from the form body. It also accepts them with HTTP Basic authentication (`Authorization: Basic base64(client_id:client_secret)`); when both are present, the body wins. It accepts a JSON body when the request has no form-encoded `grant_type`. A client that cannot keep a secret, such as a desktop or browser app, can leave `client_secret` out: PKCE protects the code exchange. ## Revoke a refresh token ```bash theme={null} curl -X POST https://api.social-api.ai/oauth/revoke \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "token=d3161495b7d3f064afd3b5aa7876503f0ff9273896ea7e9257bef82a809defcf" \ -d "client_id=9f2c4e6a8b1d3f5a7c9e1b3d5f7a9c2e" ``` Response: HTTP 200 with no body, whether or not the token existed (RFC 7009). The refresh token stops working when it belongs to that `client_id`. Revocation applies to refresh tokens only: an access token already issued keeps working until it expires, at most 1 hour later. ## Token endpoint errors `/oauth/token` answers errors in the OAuth format (RFC 6749), not in the SocialAPI error envelope: ```json theme={null} { "error": "invalid_grant", "error_description": "invalid or expired authorization code" } ``` * `400 invalid_request`: `grant_type` is missing, or a required parameter of the grant is missing. * `400 unsupported_grant_type`: `grant_type` is not `authorization_code` or `refresh_token`. * `400 invalid_grant`: the code or refresh token is unknown, expired, already used, or issued to another client, or the PKCE verifier or `redirect_uri` does not match. On a refresh, an unknown `client_id` also returns `invalid_grant`. * `401 invalid_client`: on a code exchange, the `client_id` is unknown. On either grant, the `client_secret` you sent is wrong. ## Related * [SocialAPI MCP server](/integrations/overview): connect Claude, ChatGPT, Cursor, and other clients * [Custom MCP clients](/integrations/custom): connect your own client with an API key * [Authentication](/guides/authentication): API keys and the two token types * [Scoped API keys](/guides/scoped-keys): access narrower than a full-access token * [Errors](/guides/errors): the `auth.oauth.*` codes # OpenClaw Source: https://docs.social-api.ai/integrations/openclaw Connect SocialAPI to OpenClaw with one openclaw mcp add, so the agent behind every routed channel can publish and reply. Connect SocialAPI to [OpenClaw](https://docs.openclaw.ai) so the agents behind every channel you route through the OpenClaw gateway can list your connected accounts, reply to comments, send direct messages (DMs), and publish posts. OpenClaw keeps its MCP servers in `~/.openclaw/openclaw.json` under `mcp.servers`. The server is `https://api.social-api.ai/mcp`. ## Prerequisites * OpenClaw installed (`npm i -g openclaw`). * A SocialAPI account with at least one connected account. * For a gateway with no browser: an [API key](/integrations/custom). ## Set up with OAuth **1. Add the server** ```bash theme={null} openclaw mcp add socialapi \ --url https://api.social-api.ai/mcp \ --transport streamable-http \ --auth oauth ``` This saves the entry to `~/.openclaw/openclaw.json`: ```json theme={null} { "mcp": { "servers": { "socialapi": { "url": "https://api.social-api.ai/mcp", "transport": "streamable-http", "auth": "oauth" } } } } ``` Set `transport` to `streamable-http`. When you omit it, OpenClaw uses SSE. **2. Sign in** ```bash theme={null} openclaw mcp login socialapi ``` OpenClaw prints an authorization URL and waits for the browser callback on `127.0.0.1:8989`. Open the URL, log in to SocialAPI, and click **Authorize**. The terminal prints `MCP OAuth credentials saved for "socialapi"`. If the gateway runs on another machine and the callback cannot reach it, authorize in your browser, copy the `code` from the redirect URL, and finish with: ```bash theme={null} openclaw mcp login socialapi --code ``` **3. Check the server** ```bash theme={null} openclaw mcp probe socialapi ``` OpenClaw connects and reports the number of tools. If the gateway is already running, `openclaw mcp reload` picks up the new server without a restart. ## Choose shared or per-sender accounts By default, the credentials from `openclaw mcp login` are shared: every channel acts on the one SocialAPI account you authorized. Use this for a personal assistant or a team bot. To let each person who messages the agent connect their own SocialAPI account, set `oauth.identity` to `per-requester` and give the gateway a public HTTPS origin for the callback: ```json theme={null} { "gateway": { "publicOrigin": "https://gateway.example.com" }, "mcp": { "servers": { "socialapi": { "url": "https://api.social-api.ai/mcp", "transport": "streamable-http", "auth": "oauth", "oauth": { "identity": "per-requester" } } } } } ``` The first time a sender calls a SocialAPI tool, OpenClaw replies with a sign-in link for that sender. ## Set up a headless gateway with an API key OpenClaw ignores a static `Authorization` header while `auth` is `oauth`, so save the server without `auth`: **1. Set the variable** ```bash theme={null} export SOCAPI_KEY=sapi_key_xxxxxxxxxxxxxxxx ``` **2. Save the server** ```bash theme={null} openclaw mcp set socialapi '{"url":"https://api.social-api.ai/mcp","transport":"streamable-http","headers":{"Authorization":"Bearer ${SOCAPI_KEY}"}}' ``` OpenClaw resolves `${SOCAPI_KEY}` from the environment the gateway runs in, so the key never sits in `openclaw.json`. To limit what the agent can do, create a [scoped API key](/guides/scoped-keys). ## Limit the tools Hide the tools that a channel agent must never call: ```bash theme={null} openclaw mcp tools socialapi --exclude 'social_api_delete_*,social_api_disconnect_account,social_api_revoke_key' ``` Use `--include` to keep only a subset instead, and `--clear` to remove the filter. See [MCP tools reference](/integrations/tools) for every tool name. ## Try it Send the agent a message on any routed channel: ```text theme={null} List my connected social accounts ``` The agent calls `social_api_list_accounts` and answers with your connected accounts and their platforms. ## Troubleshooting > [accordion] listen EADDRINUSE: address already in use 127.0.0.1:8989 > > Another `openclaw mcp login` is still waiting. Stop it, or finish with `--code ` as shown above. > [accordion] requires OAuth authorization > > The server is saved but not signed in. Run `openclaw mcp login socialapi`. > [accordion] Streamable HTTP error with an API key > > The key is wrong or revoked, or the variable is not set where the gateway runs. Check the key in the [dashboard](https://app.social-api.ai/api-configuration?tab=keys). ## Related * [SocialAPI MCP server](/integrations/overview): the server URL and the other supported clients. * [MCP tools reference](/integrations/tools): every tool the agent can call. * [Custom MCP clients](/integrations/custom): API key setup in detail. * [OAuth for MCP clients](/integrations/oauth): how the sign-in works. # SocialAPI MCP server Source: https://docs.social-api.ai/integrations/overview Connect Claude, ChatGPT, Cursor, and other AI clients to the SocialAPI MCP server to read comments, reply, send DMs, and publish posts. The SocialAPI MCP server lets an AI client run SocialAPI operations on your connected accounts through the [Model Context Protocol](https://modelcontextprotocol.io) (MCP). Once a client is connected, you can ask it in plain language to list your connected accounts, read and reply to comments, send direct messages (DMs), answer reviews, and publish or schedule posts. The server URL is the same for every client: ```text theme={null} https://api.social-api.ai/mcp ``` The server uses the Streamable HTTP transport. Every tool runs the same operation as its REST equivalent, with the same scopes, plan allowances, and platform support: see [MCP tools reference](/integrations/tools). ## Authentication An MCP client authenticates with a bearer token on the `Authorization` header. SocialAPI accepts the same two token types as the REST API: | Token | Use it for | How the client gets it | | - | - | - | | OAuth 2.1 access token | Interactive clients with a built-in sign-in: Claude, ChatGPT, Le Chat, Cursor, VS Code, Codex, Hermes Agent, OpenClaw | You add the server URL. The client opens a SocialAPI sign-in page, you approve, and the client keeps and refreshes the token. See [OAuth for MCP clients](/integrations/oauth) | | API key | Headless agents, servers, CI jobs, and clients without OAuth | You create a key in the dashboard and put it in the client's `Authorization` header. See [Custom MCP clients](/integrations/custom) | A full-access API key and an OAuth access token can run every tool. To give an agent less, connect it with a [scoped API key](/guides/scoped-keys): a tool outside the key's scopes returns a result with `isError: true` and `code` `auth.insufficient_scope`. `required_scope` is absent for tools that need a full-access key. See [Scoped API keys](/guides/scoped-keys) and [Authentication](/guides/authentication). ## Connect a client | Client | Surfaces | Sign-in | Setup | | - | - | - | - | | Claude | claude.ai, Claude Desktop, Claude Code, mobile | OAuth | [Claude](/integrations/claude) | | ChatGPT | Web, desktop, mobile | OAuth | [ChatGPT](/integrations/chatgpt) | | Le Chat (Mistral) | Web, mobile | OAuth | [Mistral / Le Chat](/integrations/mistral) | | Cursor | Config file, Settings | OAuth | [Cursor](/integrations/cursor) | | VS Code with GitHub Copilot | Config file, Command Palette | OAuth | [VS Code](/integrations/vscode) | | Codex | CLI, IDE extension, desktop app | OAuth or API key | [Codex](/integrations/codex) | | Hermes Agent | Chat and messaging gateway | OAuth or API key | [Hermes Agent](/integrations/hermes) | | OpenClaw | Every routed channel | OAuth or API key | [OpenClaw](/integrations/openclaw) | | Any other MCP client | Any | API key | [Custom MCP clients](/integrations/custom) | ## What the agent can do The agent gets one tool per operation: accounts and brands, comments, DMs, reviews, mentions, Automations, posts, media, analytics, exports, webhooks, events, usage, and API keys. [MCP tools reference](/integrations/tools) lists every tool by area with the REST endpoint it runs. ## Not the documentation MCP server This documentation site also exposes its own MCP server, which only searches these docs. It cannot read or change your accounts. To manage your SocialAPI accounts from an AI client, add the server at `https://api.social-api.ai/mcp`. ## Troubleshooting > [accordion] The sign-in window does not open or loops > > Allow pop-ups for the client's site. Ad blockers and privacy extensions can block the redirect back to the client: pause them and retry. > [accordion] Tool calls fail with HTTP 401 > > The client's OAuth session expired or its API key is wrong. Remove the SocialAPI server from the client and add it again to sign in again. For an API key, check that it is active in the [dashboard](https://app.social-api.ai/api-configuration?tab=keys). The `401` body is `{"error": ""}`. > [accordion] The client reports that the server is not responding > > Use the URL exactly as shown: `https://api.social-api.ai/mcp`. Check [status.social-api.ai](https://status.social-api.ai) for an incident. > [accordion] A tool returns an error result > > A failed tool returns `isError: true` with the same `code` the REST API returns. Look the code up in [Errors](/guides/errors). ## Related * [MCP tools reference](/integrations/tools): every tool and the endpoint it runs. * [OAuth for MCP clients](/integrations/oauth): how the sign-in works, and how to build it for your own client. * [Custom MCP clients](/integrations/custom): connect with an API key. * [Scoped API keys](/guides/scoped-keys): limit which tools an agent can run. * [Errors](/guides/errors): every error code a tool can return. # MCP tools reference Source: https://docs.social-api.ai/integrations/tools Look up every SocialAPI MCP tool by area: what each tool does and the REST endpoint that runs the same operation. The [SocialAPI MCP server](/integrations/overview) gives an AI agent one tool per operation it can run on your SocialAPI account. Every tool name starts with `social_api_`. A tool runs the same operation as its REST equivalent, so the same [scopes](/guides/scoped-keys), plan allowances, platform support, and error codes apply. An agent calls a tool with a JSON-RPC `tools/call` request. The arguments are the tool's input fields: ```json theme={null} { "jsonrpc": "2.0", "id": 7, "method": "tools/call", "params": { "name": "social_api_reply_to_comment", "arguments": { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "post_id": "17895695668004550", "comment_id": "17858893269000001", "text": "Thanks for watching! The full recipe is in our bio." } } } ``` A tool that fails returns a result with `isError: true`. When the failure comes from the operation itself, the text is a JSON object with the same `code` the REST endpoint returns, the message in `error`, and any `meta` fields at the top level. This result comes from an API key without the `inbox:write` scope: ```json theme={null} { "jsonrpc": "2.0", "id": 7, "result": { "content": [ { "type": "text", "text": "{\"code\":\"auth.insufficient_scope\",\"error\":\"This API key does not have the required scope: inbox:write\",\"required_scope\":\"inbox:write\"}" } ], "isError": true } } ``` A missing or malformed argument returns a plain-text message instead, for example `account_id is required`. Every tool except `social_api_report_missing_capability` also accepts an optional `context` string argument: the goal the call serves, in one short sentence. Describe people by role, never by name, email, or ID. A call without `context` runs the same way as a call with it. ## Accounts and brands | Tool | What it does | REST equivalent | | - | - | - | | `social_api_list_accounts` | List your connected accounts, optionally for one brand. Returns the `id` that every other tool takes as `account_id` | `GET /v1/accounts` | | `social_api_connect_account` | Start connecting an account. On OAuth platforms it returns the `auth_url` the person opens to authorize access. On Telegram, Bluesky, and App Store Connect it connects the account at once and returns its `account_id`. Accepts `permissions`: see [Account permissions](/guides/permissions) | `POST /v1/accounts/connect` | | `social_api_list_pending_profiles` | List the Google Business Profiles, LinkedIn member profiles, or LinkedIn company Pages a pending connection can connect | `GET /v1/accounts/pending/{connection_id}` | | `social_api_select_pending_profile` | Connect the chosen Google Business Profile, LinkedIn member profile, or LinkedIn company Page from a pending connection | `POST /v1/accounts/pending/{connection_id}/select` | | `social_api_update_account_permissions` | Replace a connected account's permissions, or send `unrestricted: true` to give it every permission. Removing a permission deletes the data it covered: see [Account permissions](/guides/permissions#removing-a-permission-deletes-data) | `PATCH /v1/accounts/{id}` | | `social_api_disconnect_account` | Disconnect a connected account | `DELETE /v1/accounts/{id}` | | `social_api_get_creator_info` | Get a TikTok account's posting options: whether it can post, the allowed privacy level, maximum video duration, and comment, duet, and stitch settings | `GET /v1/accounts/{id}/creator-info` | | `social_api_list_account_pages` | List the pages of a connected account, such as Facebook Pages | `GET /v1/accounts/{id}/pages` | | `social_api_update_account_page` | Set a page as the default, or deactivate it | `PATCH /v1/accounts/{id}/pages/{pageId}` | | `social_api_list_pinterest_boards` | List the boards of a Pinterest account | `GET /v1/accounts/{id}/boards` | | `social_api_list_brands` | List your brands | `GET /v1/brands` | | `social_api_create_brand` | Create a brand | `POST /v1/brands` | | `social_api_update_brand` | Rename a brand | `PATCH /v1/brands/{id}` | | `social_api_delete_brand` | Delete a brand and disconnect its accounts | `DELETE /v1/brands/{id}` | | `social_api_create_invite` | Create an invite link that lets someone connect an account to a brand. Accepts `permissions` | `POST /v1/invites` | | `social_api_list_invites` | List a brand's invites, active, expired, and used | `GET /v1/invites` | | `social_api_revoke_invite` | Revoke an invite link | `DELETE /v1/invites/{id}` | ## Comments | Tool | What it does | REST equivalent | | - | - | - | | `social_api_list_inbox_posts` | List your posts with their comment counts | `GET /v1/inbox/comments` | | `social_api_list_post_comments` | List the comments on one post | `GET /v1/inbox/comments/{postId}` | | `social_api_get_comment_replies` | List the replies to one comment | `GET /v1/inbox/comments/{postId}/{commentId}/replies` | | `social_api_reply_to_comment` | Reply publicly to a post or a comment. The text is 2,200 bytes at most | `POST /v1/inbox/comments/{postId}` | | `social_api_private_reply_to_comment` | Send a private reply to the person who wrote a comment. The text is 1,000 bytes at most | `POST /v1/inbox/comments/{postId}/{commentId}/private-reply` | | `social_api_hide_comment` | Hide a comment from public view | `POST /v1/inbox/comments/{postId}/{commentId}/hide` | | `social_api_unhide_comment` | Show a hidden comment again | `DELETE /v1/inbox/comments/{postId}/{commentId}/hide` | | `social_api_like_comment` | Like a comment as the connected account | `POST /v1/inbox/comments/{postId}/{commentId}/like` | | `social_api_unlike_comment` | Remove the connected account's like from a comment | `DELETE /v1/inbox/comments/{postId}/{commentId}/like` | | `social_api_delete_comment` | Delete a comment from the platform | `DELETE /v1/inbox/comments/{postId}/{commentId}` | | `social_api_moderate_comment` | Hide, unhide, or delete a comment by its `sapi_cmt_` ID, with `action` set to `hide`, `unhide`, or `delete` | The hide, unhide, and delete endpoints above | See [Comments](/guides/comments) for platform support and limits. ## Direct messages | Tool | What it does | REST equivalent | | - | - | - | | `social_api_list_conversations` | List DM conversations, ordered by their latest activity, newest first | `GET /v1/inbox/conversations` | | `social_api_get_conversation` | Get one conversation | `GET /v1/inbox/conversations/{id}` | | `social_api_list_messages` | List the messages in a conversation, newest first | `GET /v1/inbox/conversations/{id}/messages` | | `social_api_send_dm` | Send a DM in an existing conversation. The text is 1,000 bytes at most. Instagram also accepts `quick_replies` and `buttons`: see [Direct messages](/guides/direct-messages) | `POST /v1/inbox/conversations/{id}/messages` | | `social_api_update_conversation` | Archive a conversation, or restore it to `active` | `PATCH /v1/inbox/conversations/{id}` | | `social_api_mark_conversation_read` | Mark every message in a conversation as read | `POST /v1/inbox/conversations/{id}/read` | See [Direct messages](/guides/direct-messages) for platform support and the messaging window. ## Reviews | Tool | What it does | REST equivalent | | - | - | - | | `social_api_list_review_accounts` | List the connected accounts that support reviews, each with its rating summary | `GET /v1/inbox/reviews` | | `social_api_get_reviews` | List the reviews of one connected account | `GET /v1/inbox/reviews/{account_id}` | | `social_api_reply_to_review` | Reply to a review as the business. On Google Business Profile and App Store Connect, replying again replaces the existing reply | `POST /v1/inbox/reviews/{id}/reply` | See [Reviews](/guides/reviews). ## Mentions | Tool | What it does | REST equivalent | | - | - | - | | `social_api_get_mentions` | List the posts and comments that mention or tag a connected account | `GET /v1/accounts/{id}/mentions` | | `social_api_reply_to_mention` | Reply publicly to a mention, including on a post the account does not own | `POST /v1/accounts/{id}/mentions/reply` | See [Mentions](/guides/mentions). ## Automations | Tool | What it does | REST equivalent | | - | - | - | | `social_api_list_automations` | List your Automations, optionally for one account | `GET /v1/automations` | | `social_api_get_automation` | Get one Automation with its steps and counters | `GET /v1/automations/{id}` | | `social_api_create_automation` | Create an Automation, with `steps` or `follow_gate` | `POST /v1/automations` | | `social_api_update_automation` | Change an Automation's fields, or pause and resume it | `PATCH /v1/automations/{id}` | | `social_api_delete_automation` | Delete an Automation and its run history | `DELETE /v1/automations/{id}` | | `social_api_list_automation_firings` | List an Automation's runs from the last 30 days, with each action's outcome | `GET /v1/automations/{id}/firings` | See [Automations](/guides/automations). ## Posts | Tool | What it does | REST equivalent | | - | - | - | | `social_api_list_posts` | List your posts (drafts, scheduled, published, and failed), with filters | `GET /v1/posts` | | `social_api_get_post` | Get one post with its per-platform delivery details | `GET /v1/posts/{pid}` | | `social_api_create_post` | Create a draft, schedule a post with `scheduled_at`, publish it with `publish_now`, or spread the targets over time with `stagger` | `POST /v1/posts` | | `social_api_update_post` | Update a draft, scheduled, or failed post | `PATCH /v1/posts/{pid}` | | `social_api_publish_post` | Publish a draft or scheduled post immediately | `POST /v1/posts/{pid}/publish` | | `social_api_retry_post` | Retry the failed platform deliveries of a `failed` or `partial` post | `POST /v1/posts/{pid}/retry` | | `social_api_unpublish_post` | Remove a published post from one platform or from all, and keep the post in SocialAPI. Posts on TikTok, and on Instagram accounts connected through Instagram Login, stay on the platform | `POST /v1/posts/{pid}/unpublish` | | `social_api_delete_post` | Delete a post. A published post is also removed from each platform, except TikTok posts and posts on Instagram accounts connected through Instagram Login | `DELETE /v1/posts/{pid}` | | `social_api_validate_post` | Check a post against each platform's rules before you create it | `POST /v1/posts/validate` | | `social_api_get_constraints` | Get each platform's content rules: text length, media count, media types, file sizes | `GET /v1/posts/validate` | | `social_api_get_post_metrics` | Fetch a published post's current engagement from each platform | `GET /v1/posts/{pid}/metrics` | | `social_api_list_instagram_stories` | List the active Stories of an Instagram account | `GET /v1/platforms/instagram/accounts/{id}/stories` | See [Publishing](/guides/publishing). ## Media | Tool | What it does | REST equivalent | | - | - | - | | `social_api_get_media_upload_url` | Get an upload URL and a `media_id` for one file | `GET /v1/media/upload-url` | | `social_api_verify_media_upload` | Confirm that the file reached the upload URL, so the `media_id` can be attached to a post | `POST /v1/media/{id}/verify` | | `social_api_list_media` | List the files in your media library | `GET /v1/media` | | `social_api_delete_media` | Delete a file from your media library | `DELETE /v1/media/{id}` | | `social_api_get_storage_usage` | Get your media storage use and quota | `GET /v1/media/storage` | See [Media](/guides/media) for the upload flow. ## Analytics and exports | Tool | What it does | REST equivalent | | | | | | - | - | - | - | - | - | - | | `social_api_get_account_analytics` | Get one connected account's daily metric history | `GET /v1/accounts/{id}/analytics` | | | | | | `social_api_get_brand_analytics` | Get a brand's daily metric history, summed across its accounts | `GET /v1/brands/{id}/analytics` | | | | | | `social_api_get_post_analytics` | Get one published post's daily engagement history | `GET /v1/posts/{pid}/analytics` | | `social_api_create_export` | Start an analytics export for a connected account | `POST /v1/accounts/{id}/export` | | `social_api_get_export` | Get an export's status and, once it completes, its download links | `GET /v1/exports/{id}` | | | | | | `social_api_list_exports` | List your 50 most recent exports, newest first | `GET /v1/exports` | | | | | | `social_api_get_export_videos` | Get the per-video data of a completed export | `GET /v1/exports/{id}/videos` | | | | | See [Analytics](/guides/analytics). ## Webhooks | Tool | What it does | REST equivalent | | - | - | - | | `social_api_list_webhooks` | List your webhook endpoints | `GET /v1/webhooks` | | `social_api_get_webhook` | Get one webhook endpoint with its delivery counts | `GET /v1/webhooks/{id}` | | `social_api_create_webhook` | Register a webhook endpoint for a list of events | `POST /v1/webhooks` | | `social_api_update_webhook` | Change an endpoint's URL or events, or turn it on or off | `PATCH /v1/webhooks/{id}` | | `social_api_delete_webhook` | Delete a webhook endpoint | `DELETE /v1/webhooks/{id}` | | `social_api_test_webhook` | Send a test event to an endpoint and return the HTTP status it answered with | `POST /v1/webhooks/{id}/test` | | `social_api_list_webhook_deliveries` | List an endpoint's deliveries | `GET /v1/webhooks/{id}/deliveries` | | `social_api_get_webhook_delivery` | Get one delivery with its payload and each attempt's response | `GET /v1/webhooks/{id}/deliveries/{did}` | | `social_api_retry_webhook_delivery` | Send a failed delivery again | `POST /v1/webhooks/{id}/deliveries/{did}/retry` | See [Webhooks](/guides/webhooks). ## Events, usage, and your SocialAPI account | Tool | What it does | REST equivalent | | - | - | - | | `social_api_list_events` | List the events log: publishing attempts, webhook deliveries, inbound platform events, API calls, and account changes | `GET /v1/events` | | `social_api_events_summary` | Get event and failure counts for a time window | `GET /v1/events/summary` | | `social_api_get_usage` | Get your usage and plan allowances for the current billing period | `GET /v1/usage` | | `social_api_get_account_limits` | Get a connected account's remaining quotas, as the platform reports them | `GET /v1/accounts/{id}/limits` | | `social_api_get_me` | Get your SocialAPI profile, including your plan | `GET /v1/users/me` | | `social_api_patch_me` | Update your profile settings | `PATCH /v1/users/me` | | `social_api_list_keys` | List your API keys, without their secrets | `GET /v1/keys` | | `social_api_revoke_key` | Revoke an API key | `DELETE /v1/keys/{id}` | | `social_api_report_missing_capability` | Report a capability that none of the tools provide. Its required `context` argument describes the capability and what it would accomplish. Returns `{"status": "received"}` and costs no credits | None | ## What tools do not cover No tool creates, rotates, or edits an API key, and no tool deletes your SocialAPI account. Create keys in the [dashboard](https://app.social-api.ai) or with `POST /v1/keys` (see [Authentication](/guides/authentication)). Among the operations available only through the REST API are: * Uploading a file in the request body (`POST /v1/media/upload`). An agent uploads with `social_api_get_media_upload_url` instead. * Inbox sync, inbound attachments, and message reactions (`/v1/inbox/sync`, `/v1/inbox/media/{id}`, `/v1/inbox/conversations/{id}/messages/{messageId}/reaction`). * Creating, editing, and deleting Pinterest boards. * The webhook event catalog (`GET /v1/webhooks/events`). The `social_api_create_webhook` description lists the event names. * Importing posts (`POST /v1/posts/import`), refreshing an account's posts (`POST /v1/accounts/{id}/sync`), announcements (`GET /v1/announcements`), and the plan catalog (`GET /v1/billing/plans`). * Every endpoint under `/v1/platforms/` except Instagram Stories: WhatsApp templates and phone numbers, Google Business Profile locations, platform summaries and insights. ## Errors and limits * **Timeout**: a request to the MCP server that runs longer than 5 minutes is stopped. * **Errors**: a failed tool returns `isError: true` with the same error code as its REST equivalent. Codes are listed in [Errors](/guides/errors). * **Scopes**: an agent connected with a scoped API key can run only the tools its scopes allow. A tool outside them returns `auth.insufficient_scope` with the missing scope in `required_scope`. `required_scope` is absent for tools that need a full-access key. See [Scoped API keys](/guides/scoped-keys). * **Cost**: a tool call consumes the same credits and plan allowances as its REST equivalent. See [Plans](/guides/plans). * **Rate limits**: tools that publish or send messages are subject to the same posting and messaging caps as the REST API. See [Rate limits](/guides/rate-limits). * **Annotations**: every tool declares `readOnlyHint`, `destructiveHint`, and `openWorldHint`. Clients read them to decide which calls need your confirmation. ## Related * [SocialAPI MCP server](/integrations/overview): connect an AI client to these tools. * [Custom MCP clients](/integrations/custom): call the tools over HTTP with an API key. * [Scoped API keys](/guides/scoped-keys): limit which tools an agent can run. * [API reference](/api-reference/introduction): request and response schemas for each REST equivalent. # VS Code + GitHub Copilot Source: https://docs.social-api.ai/integrations/vscode Connect SocialAPI to VS Code with GitHub Copilot using a config file or the Command Palette so Agent mode can manage comments, DMs, and posts. Connect SocialAPI to VS Code and ask Copilot Chat in Agent mode to list your connected accounts, reply to comments, send direct messages (DMs), and publish posts. VS Code connects to SocialAPI's MCP server at `https://api.social-api.ai/mcp` and signs in with OAuth 2.1, so you create no API key. ## Prerequisites * [VS Code](https://code.visualstudio.com) 1.101 or later with the [GitHub Copilot](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot) extension. * A GitHub Copilot subscription: Individual, Business, or Enterprise. * A SocialAPI account with at least one connected account. ## Set up with the config file **1. Add the server** Create or edit `.vscode/mcp.json` in your workspace, or add the entry to your user profile for all workspaces: ```json theme={null} { "servers": { "SocialAPI": { "type": "http", "url": "https://api.social-api.ai/mcp" } } } ``` **2. Authorize** VS Code shows a CodeLens above the server entry. Click **Auth** and authorize in the browser window that opens. ## Set up with the Command Palette **1. Add the server** Open the Command Palette (`Ctrl+Shift+P` on Windows and Linux, `Cmd+Shift+P` on macOS) and run **MCP: Add Server**. Select **HTTP** as the server type and enter `https://api.social-api.ai/mcp`. **2. Choose a scope** Choose **Global** for your user profile or **Workspace** for this workspace. **3. Authorize** Click the **Auth** CodeLens and authorize in the browser window that opens. ## Try it Open **Copilot Chat**, switch to Agent mode, and ask: ```text theme={null} List my connected social accounts ``` Copilot calls `social_api_list_accounts` and answers with your connected accounts and their platforms. ## Troubleshooting > [accordion] The Auth CodeLens does not appear > > Check that VS Code is version 1.101 or later and that the GitHub Copilot extension is installed and active. The CodeLens appears above the server entry in `.vscode/mcp.json`. > [accordion] The server does not connect > > Check that the `url` is exactly `https://api.social-api.ai/mcp` and that `type` is `http`. > [accordion] Tool calls fail with HTTP 401 > > The OAuth session expired. Click **Auth** above the server entry again to sign in again. ## Related * [SocialAPI MCP server](/integrations/overview): the server URL and the other supported clients. * [MCP tools reference](/integrations/tools): every tool Copilot can call. * [OAuth for MCP clients](/integrations/oauth): how the sign-in works. * [Errors](/guides/errors): every error code a tool can return. # App Store Connect API Source: https://docs.social-api.ai/platforms/app-store-connect Connect an App Store Connect API key to read your app's App Store reviews and post developer responses through one reviews API. SocialAPI connects an app in App Store Connect with your own App Store Connect API key. It lists the app's customer reviews and posts your developer response to a review. App Store Connect has no consent screen: you create an API key in App Store Connect, send it to SocialAPI in one request, and there is no OAuth redirect and no app to register. [Platform apps](/guides/platform-credentials) lists which app connects each platform. ## At a glance | Field | Value | | - | - | | Platform slug | `app_store_connect` | | Auth | Your App Store Connect API key. No OAuth | | Platform API | App Store Connect API | | Connected account | 1 app | | Status | Available on every plan | ## Capabilities | Feature | Status | Condition | | - | - | - | | List reviews | Yes | Written reviews of iOS, macOS, and visionOS apps. App Store Connect has no written reviews for tvOS apps | | Reply to a review | Yes | A review has 1 developer response. A new reply replaces it | | Edit or delete a reply | No | Send a new reply to change it. Delete a response in App Store Connect | | Review summary | Partial | `total_count` and `last_review_at`. `average_rating` is always `0`: App Store Connect reports no average rating | | Publishing | No | `POST /v1/posts` and `PATCH /v1/posts/{pid}` with an App Store Connect target return `422 account.platform_unsupported` | | Comments, DMs, mentions | No | `501 resource.not_supported` | | Analytics | No | No post metrics, account summary, or stored history | | Automations | No | Automations run on Instagram and Facebook accounts only. `422 automation.platform_unsupported` | | Platform quota | No | `GET /v1/accounts/{id}/limits` returns `501 resource.not_supported` | | Webhook events | Partial | Account events. No `review.received` or `review.updated`: read reviews with the list endpoint | The capability matrix for every platform is on [Supported platforms](/platforms/overview). ## Connect an account One connected account is one app. You send an API key and, when the key can see more than 1 app, the app to connect. **1. Create an API key** In App Store Connect, open **Users and Access**, then **Integrations**, then **App Store Connect API**. Generate a Team key with the **Customer Support** or **Admin** role. Generating a Team key needs the Account Holder or Admin role. An Individual key from your user profile also works when your own role is Customer Support, Admin, or Account Holder, and your user has the **Generate Individual API Keys** permission. Replying to reviews needs one of these roles. **2. Download the key and note its IDs** Download the private key file. App Store Connect lets you download it once. Note the key's **Key ID**, and for a Team key the **Issuer ID** shown on the **App Store Connect API** page. **3. Connect the app** Send the contents of the private key file as `metadata.private_key`, with line breaks escaped as `\n`: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/connect \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "app_store_connect", "metadata": { "key_id": "2X9R4HXF34", "issuer_id": "57246542-96fe-1a63-e053-0824d011072a", "private_key": "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49...\n-----END PRIVATE KEY-----", "app_id": "1234567890" } }' ``` Response (HTTP 201): ```json theme={null} { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f", "platform": "app_store_connect", "username": "com.example.myapp", "display_name": "My App" } ``` | Field | Required | Description | | - | - | - | | `metadata.key_id` | Yes | The API key's Key ID | | `metadata.private_key` | Yes | The full contents of the private key file, from `-----BEGIN PRIVATE KEY-----` to `-----END PRIVATE KEY-----` | | `metadata.issuer_id` | Team keys | The Issuer ID. Omit it for an Individual key | | `metadata.app_id` | When the key sees more than 1 app | The app's ID in App Store Connect. A request without it lists the IDs the key can see in `meta.apps`: see [Connect more than one app](#connect-more-than-one-app) | Every field is a JSON string. Send `app_id` in quotes: a number is read as missing. `username` is the app's bundle ID and `display_name` is its name. Without `brand_id` in the request, SocialAPI creates a brand for the app. SocialAPI also sends `account.connected` to your webhook endpoints. ### Connect more than one app A brand holds 1 account per platform, so each app goes into its own brand. Send one connect request per app, each with its `app_id` and a different `brand_id`, or with no `brand_id`. Connecting a second app into a brand that already holds one returns `409 account.already_linked`. Each brand counts toward your plan's brand allowance: see [Plans](/guides/plans). When the key can see more than 1 app and the request has no `app_id`, the call returns `400 validation.field_required` with `meta.field` set to `app_id` and `meta.apps` listing up to 200 of the apps the key can see: ```json theme={null} { "error": { "code": "validation.field_required", "message": "This API key can access several apps. Pass app_id to choose one; each app is a separate account.", "meta": { "field": "app_id", "apps": [ { "app_id": "1234567890", "name": "My App" }, { "app_id": "1234567891", "name": "My Other App" } ] } } } ``` ### Connect errors | HTTP | Code | When it occurs | | - | - | - | | 400 | `validation.field_required` | `key_id` or `private_key` is missing, or `app_id` is missing while the key sees more than 1 app. `meta.field` names it | | 400 | `validation.field_invalid` | `private_key` is not the contents of an App Store Connect private key file, the key can see no app with that `app_id`, or the key can see no app. `meta.field` names the field | | 400 | `validation.app_store_connect_rejected` | App Store Connect refused the app lookup as invalid. `error.message` carries App Store Connect's reason | | 401 | `platform.app_store_connect.auth` | App Store Connect rejected the key: check the Key ID, the Issuer ID, and that the key is not revoked | | 403 | `platform.app_store_connect.forbidden` | App Store Connect refused the request for the key's role. Check the role the key was created with | | 409 | `account.already_linked` | The brand already holds an App Store Connect app | | 429 | `platform.app_store_connect.rate_limit` | The key's hourly request limit on App Store Connect is used up. Wait, then retry | | 502 | `platform.app_store_connect.` | App Store Connect returned an error SocialAPI does not map. `` is App Store Connect's error code, or `api_error` when it sends none. `error.message` carries App Store Connect's reason. Retry later | ### Disconnect and revoke Disconnecting the account (`DELETE /v1/accounts/{account_id}`) deletes the API key from SocialAPI. App Store Connect offers no way to revoke a key from outside, so the key stays valid. To cut off access completely, revoke the key in App Store Connect: a Team key under **Users and Access**, **Integrations**, and an Individual key in your user profile, under **Individual API Key**. ## Reviews You list an app's reviews with `GET /v1/inbox/reviews/{account_id}` and reply with `POST /v1/inbox/reviews/{id}/reply`. The parameters, response fields, and cost are on [Reviews](/guides/reviews). These rules are specific to App Store Connect: * **Reviews carry a title.** Each review has `title`, the reviewer's headline, next to `text`, the review body. * **The author is a nickname.** `author.name` is the reviewer's App Store nickname. `author.avatar_url` is absent. * **No edit time.** `updated_at` is the same as `created_at`. * **No average rating.** `average_rating` is `0` in the list of review accounts and in the `summary` of a reviews page. `total_count` is the app's number of written reviews. * **No reply state.** `reply` has `text` and `updated_at`, and no `state`. A reply response has no `state` either. * **A reply replaces the previous one.** A review has 1 developer response. Replying again replaces it. * **Replies are not visible at once.** A response can take up to 24 hours to appear on the App Store. ```json theme={null} { "id": "sapi_rev_KQ2M7XW4RT9BCDFHJLNPSV3YZ5G8A6E1", "rating": 5, "title": "Love it", "text": "Does exactly what I need, every day.", "author": { "name": "happycamper42" }, "reply": { "text": "Thank you! Glad it helps.", "updated_at": "2026-10-02T09:15:00Z" }, "created_at": "2026-10-01T17:00:00Z", "updated_at": "2026-10-01T17:00:00Z" } ``` ### Review errors | HTTP | Code | When it occurs | | - | - | - | | 400 | `validation.app_store_connect_rejected` | App Store Connect refused the request as invalid: a reply, or a reviews page request such as one with a `cursor` it does not accept. `error.message` carries App Store Connect's reason. On a reply, the interaction credit is refunded | | 401 | `platform.app_store_connect.unauthorized` | App Store Connect rejected the key. If it repeats, check the key in App Store Connect, then connect the app again with a working key and the account's `brand_id` (see [Reconnect an account](/guides/connect-accounts#reconnect-an-account)) | | 403 | `platform.app_store_connect.forbidden` | App Store Connect refused the key's role. Replies need the Customer Support, Admin, or Account Holder role | | 404 | `resource.not_found` | The app or the review no longer exists, or the key cannot see it | | 429 | `platform.app_store_connect.rate_limit` | The key's hourly request limit on App Store Connect is used up. Wait, then retry | | 502 | `platform.app_store_connect.` | App Store Connect returned an error SocialAPI does not map. `` is App Store Connect's error code, or `api_error` when it sends none. `error.message` carries App Store Connect's reason. Retry later | A reply that fails with a `validation.*` code gets its interaction credit back. See [When a credit comes back](/guides/plans#when-a-credit-comes-back) for the other cases. ## Limits | Limit | Value | | - | - | | Requests per API key (App Store Connect) | An hourly limit set by Apple, shared by every app the key can see and by every other tool that uses the key. Apple's example is 3,500 requests per rolling hour, and Apple states the actual limit can vary | | Reviews per page | 1 to 50, the same as every platform: see [Reviews](/guides/reviews) | | Reply text | SocialAPI sets no length limit on `POST /v1/inbox/reviews/{id}/reply`. A reply App Store Connect refuses returns `400 validation.app_store_connect_rejected`. The MCP tool `social_api_reply_to_review` accepts up to 2,200 bytes | ## Scopes App Store Connect has no OAuth scopes. The key's role sets what SocialAPI can do: replying needs the Customer Support, Admin, or Account Holder role, and a call the role does not allow returns `403 platform.app_store_connect.forbidden`. The account supports the `reviews.read` and `reviews.write` [permissions](/guides/permissions), which SocialAPI alone enforces. ## Related * [Reviews](/guides/reviews): list reviews and reply, across platforms * [Supported platforms](/platforms/overview): what each platform supports * [Connect accounts](/guides/connect-accounts): the connect flow and the account object * [Platform apps](/guides/platform-credentials): which app connects each platform # Bluesky API Source: https://docs.social-api.ai/platforms/bluesky Connect a Bluesky account with an app password to publish posts, read and reply to comments and mentions, and send direct messages through one API. SocialAPI connects a Bluesky account with an app password. It publishes posts with up to 4 images, reads and answers replies and mentions, hides replies on your posts, and reads and sends direct messages (DMs) when the app password allows it. Bluesky has no consent screen: you create an app password in Bluesky, send it to SocialAPI in one request, and there is no OAuth redirect and no app to register. [Platform apps](/guides/platform-credentials) lists which app connects each platform. ## At a glance | Field | Value | | - | - | | Platform slug | `bluesky` | | Auth | App password. No OAuth | | Platform API | AT Protocol (Bluesky) | | Status | Available on every plan | ## Capabilities An unsupported call returns `501 resource.not_supported`. Read the `capabilities` object on each comment instead of assuming (`can_reply`, `can_delete`, `can_hide`). | Feature | Status | Condition | | - | - | - | | Publish a text post | Yes | Up to 300 characters. See [Bluesky publishing](/platforms/bluesky/publishing) | | Publish a post with images | Yes | 1 to 4 images, each at most 2 MB | | Publish a video | No | A `media_id` video returns `400 publishing.validation_failed`. A video sent as a `url` is skipped and the post publishes without it | | Schedule a post | Yes | `scheduled_at` on the post | | First comment on a post | No | `first_comment` is ignored | | Update a published post | No | `PATCH /v1/posts/{pid}` returns `409 post.state_invalid` for a published post. Delete the post and publish a new one | | Delete a published post | Yes | | | List posts | Yes | | | List comments and replies | Yes | Replies up to 6 levels below the post, as one flat list with no cursor | | Reply to a comment | Yes | | | Hide, unhide a comment | Yes | Only replies on a thread you started. At most 300 hidden replies per thread | | Delete a comment | Conditional | Only your own replies. A reply from another account cannot be deleted: hide it | | Like a comment | No | `501 resource.not_supported`. `can_like` is `false` on every comment | | Private reply to a comment | No | `501 resource.not_supported`. Bluesky has no comment-to-DM path | | Mentions | Yes | Posts that mention the account | | Reply to a mention | Yes | | | DMs: list, read, send | Partial | Text only. Needs an app password with direct message access: see [Direct messages](#direct-messages) | | React to a DM | No | `501 resource.not_supported` | | Reviews | No | Bluesky has no reviews. `GET /v1/inbox/reviews/{account_id}` returns `422 account.platform_unsupported` | | Automations | No | Automations run on Instagram and Facebook accounts only. `422 automation.platform_unsupported` | | Platform quota | Yes | `GET /v1/accounts/{id}/limits` returns `write_points_per_hour` and `write_points_per_day`: see [Rate limits](/guides/rate-limits#read-a-platforms-remaining-quota) | | Webhook events | Partial | No comment or DM events. `mention.received` only when you list mentions. See [Webhook events](#webhook-events) | The capability matrix for every platform is on [Supported platforms](/platforms/overview). ## Connect an account A Bluesky account connects in one request: you send its handle and an app password, and SocialAPI returns the connected account. **1. Create an app password** Sign in to Bluesky in a browser and open [App passwords](https://bsky.app/settings/app-passwords) in **Settings**. Click **Add app password** and name it, for example `socialapi`. Never send your account password. **2. Allow direct messages (optional)** Tick **Allow access to your direct messages** before you create the password if SocialAPI must read and send DMs. You can set this option only when you create the app password. To add DM access later, create a second app password with the option on and connect again with it. **3. Copy the password** Bluesky shows the app password once, in the form `xxxx-xxxx-xxxx-xxxx`. Copy it. **4. Connect the account** Send the handle as `metadata.identifier` and the password as `metadata.app_password`: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/connect \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "bluesky", "metadata": { "identifier": "yourhandle.bsky.social", "app_password": "xxxx-xxxx-xxxx-xxxx" } }' ``` Response (HTTP 201): ```json theme={null} { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f", "platform": "bluesky", "username": "yourhandle.bsky.social", "display_name": "Your Name" } ``` `identifier` is the account's handle or its DID. Without `brand_id` in the request, SocialAPI creates a brand for the account. SocialAPI also sends `account.connected` to your webhook endpoints. A custom-domain handle works the same way. A missing `identifier` or `app_password` returns `400 validation.field_required`, with `meta.field` naming it. A handle Bluesky cannot resolve, or an app password Bluesky rejects, returns `400 validation.field_invalid`. ### Two-factor accounts When the account has email two-factor authentication on, the first connect request returns `400 platform.bluesky.auth_factor_required` and Bluesky emails a confirmation code. Send the same request again with that code as `metadata.auth_factor_token`. ### Disconnect and revoke Disconnecting the account (`DELETE /v1/accounts/{account_id}`) deletes the app password from SocialAPI. Bluesky offers no way to revoke an app password from outside, so the password stays valid on Bluesky. To cut off access completely, delete the app password in **Settings** on Bluesky. ## Direct messages DMs work only with an app password that has direct message access. SocialAPI checks this when you connect. Without it, every DM call returns `403 platform.bluesky.dm_scope_missing`, and posts, comments, and mentions keep working. The fix is a new app password with **Allow access to your direct messages** on, then a new connect request. Reconnecting with the old password does not help. You list conversations with `GET /v1/inbox/conversations`, read a conversation with `GET /v1/inbox/conversations/{conversation_id}/messages`, and reply with `POST /v1/inbox/conversations/{conversation_id}/messages`. The request fields are on [Direct messages](/guides/direct-messages). These rules are specific to Bluesky: * **Text only.** A Bluesky message carries text. SocialAPI sends only the `text` of a send request. It does not send attachments, `buttons`, `quick_replies`, `cards`, or `list_sections`. * **No `message_tag`.** Bluesky has no message tags: `message_tag` is ignored. ## Comments and replies You read the replies to a post with `GET /v1/inbox/comments/{postId}` and answer with the reply endpoints on [Comments](/guides/comments). * **One flat list.** Bluesky returns a thread as a tree with no cursor. SocialAPI flattens it, the pagination object is always empty, and replies deeper than 6 levels below the post are not returned. A very large thread can be cut off. * **Hide, not delete.** Only the author of the original post can hide a reply to it: another account gets `403 platform.bluesky.not_thread_owner`. A thread holds at most 300 hidden replies: the 301st returns `422 platform.bluesky.hidden_replies_full`. A hidden reply is not removed: Bluesky no longer shows it by default. * **Delete only your own replies.** `platform.bluesky.not_own_record` is returned when you try to delete a reply from another account. * **No likes and no private replies.** `can_like` and `can_private_reply` are `false` on every comment. ## Mentions `GET /v1/accounts/{account_id}/mentions` returns the posts that mention the account, from Bluesky's mention notifications. It covers the whole account, not one post. You answer a mention with a reply to that post. See [Mentions](/guides/mentions). ## Webhook events Bluesky sends SocialAPI no notification of its own, so there is no `comment.received` or `dm.received` event: read comments and DMs with the inbox endpoints. Listing mentions with `GET /v1/accounts/{id}/mentions` sends a `mention.received` event for the listed items: see [Mentions you list](/guides/webhook-events#mentions-you-list). Bluesky does emit the `post.*` events for posts you publish through SocialAPI, `account.connected` when you connect the account, `account.disconnected` when SocialAPI flags the account for reconnection, and `account.updated` when the account loses [permissions](/guides/permissions) you did not remove yourself. See [Webhook events](/guides/webhook-events). ## Limits | Limit | Value | | - | - | | Post text | 300 characters | | Images per post | 4 | | Image size | 2 MB | | Replies read per post | 6 levels deep | | Hidden replies per thread | 300 | | Writes per account (Bluesky) | 5,000 points per hour and 35,000 per day. A new post or reply costs 3 points, an update 2, a delete 1 | | Sign-ins per account (Bluesky) | 30 per 5 minutes | | Posts (SocialAPI) | 25 per hour and 50 per 24 hours for each account: see [Posting velocity caps](/guides/rate-limits#posting-velocity-caps) | When Bluesky's own limit is used up, the call fails with `429 platform.bluesky.rate_limit`. Wait, then retry. `GET /v1/accounts/{id}/limits` returns the 2 write budgets. ## Scopes Bluesky has no OAuth scopes. An app password gives SocialAPI the access of the account itself, except direct messages, which need the option you tick when you create the password. SocialAPI uses it to publish posts, read and reply to comments and mentions, and read and send DMs. The account supports the `posts.read`, `posts.write`, `comments.read`, `comments.write`, `messages.read`, and `messages.write` [permissions](/guides/permissions), which SocialAPI alone enforces. ## Related * [Bluesky publishing](/platforms/bluesky/publishing): text, images, scheduling, and deleting posts * [Direct messages](/guides/direct-messages): the request fields for sending DMs * [Comments](/guides/comments): reading, replying to, and hiding comments * [Platform apps](/guides/platform-credentials): which app connects each platform * [Errors](/guides/errors): every Bluesky error code * [Supported platforms](/platforms/overview): what each platform supports # Bluesky publishing Source: https://docs.social-api.ai/platforms/bluesky/publishing Publish and schedule Bluesky posts with text and up to 4 images through SocialAPI, with media limits, link and hashtag handling, and delete. ## 1. Overview A Bluesky post is published once Bluesky has accepted it. Publishing goes through the same `POST /v1/posts` endpoint as every other platform. A post holds text, up to 4 images, or both. Bluesky has no video publishing through SocialAPI and no way to edit a published post. | Field | Value | | - | - | | Platform slug | `bluesky` | | Auth type | App password | | Platform API | AT Protocol (Bluesky) | | Create post | Yes | | Update post | Not after publishing (`409 post.state_invalid`). `draft`, `scheduled`, and `failed` posts can be edited | | Delete post | Yes | | Schedule | Yes (deferred publish via `scheduled_at`) | | First comment | No (`first_comment` is not published on Bluesky) | | Posting caps | 25 posts per hour and 50 per 24 hours for each account: see [Posting velocity caps](/guides/rate-limits#posting-velocity-caps) | *** ## 2. Supported media | Content type | How to trigger | Notes | | - | - | - | | Text only | Omit `media` | Up to 300 characters | | Single image | One `media` entry that is an image | JPEG, PNG, GIF, or WebP, at most 2 MB | | Multiple images | 2 to 4 `media` entries, all images | The images appear together in one post | | Video | Not supported | See "Video" in section 7 | **Text limit:** 300 characters, counted as Unicode characters. More returns `400 publishing.validation_failed`. **Image limit:** 4 images per post. More than 4 returns `400 publishing.validation_failed`. **File limits** apply to media you upload to SocialAPI (`source_type: "media_id"`). A file outside them fails validation with `400 publishing.validation_failed`. For a `url` item, SocialAPI checks the file type against these limits, and the size when the host reports it. When the host does not report the size, SocialAPI downloads the image when it publishes, and an image over 2 MB fails the target with `400 validation.field_invalid`, with `meta.field` set to `media`. **Alt text:** SocialAPI publishes every image with empty alt text. There is no field to set it. A `platform_attachment_id` item fails the Bluesky target with `422 publishing.media.unsupported_source`: Bluesky has no uploaded media to reference by ID. Send images as `url` or `media_id`. *** ## 3. Create post Use `POST /v1/posts` with `targets` that name a Bluesky account. `publish_now: true` publishes immediately. Without it and without `scheduled_at`, SocialAPI saves a draft. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Our spring release is live. Details: https://example.com/spring #release", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }] }' ``` Response (HTTP 201, delivery continues after the response): ```json theme={null} { "id": "6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "text": "Our spring release is live. Details: https://example.com/spring #release", "status": "publishing", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "bluesky", "status": "publishing" } ] } ``` SocialAPI turns every `http://` or `https://` address in the text into a link and every `#tag` into a hashtag. It does not turn `@handle` into a mention: the text publishes as plain text and the mentioned account is not notified. SocialAPI attaches no link preview card. ### Platform data fields Bluesky has no `platform_data` fields. SocialAPI forwards any additional keys in `platform_data.bluesky` only when a table in this section lists them. None does, so SocialAPI sends no key to Bluesky and returns no warning for an unlisted key. ### Scheduling Set `scheduled_at` to an ISO 8601 timestamp (UTC) to defer publishing: ```json theme={null} { "text": "Big announcement coming tomorrow.", "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "scheduled_at": "2026-10-17T10:00:00Z" } ``` See [Scheduling](/guides/scheduling) for how scheduled posts settle. *** ## 4. Update post Bluesky has no way to edit a published post. `PATCH /v1/posts/{pid}` edits only a post in `draft`, `scheduled`, or `failed` status, and returns `409 post.state_invalid` for a published post. To correct a post, delete it and publish a new one. *** ## 5. Delete post ```bash theme={null} curl -X DELETE https://api.social-api.ai/v1/posts/6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d \ -H "Authorization: Bearer $SOCAPI_KEY" ``` To remove only the Bluesky target of a cross-platform post, unpublish it by `account_id`. `DELETE /v1/posts/{pid}` always removes every target. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts/6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d/unpublish \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{"account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J"}' ``` See [Delete a post](/guides/delivery#delete-a-post) for how a cross-platform post is handled. *** ## 6. Retrieve posts Use `GET /v1/posts` to list posts. Filter by platform or account: ```bash theme={null} curl "https://api.social-api.ai/v1/posts?platform=bluesky&account_ids=acc_01HZ9X3Q4R5M6N7P8V2K0W1J&limit=20" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Each post includes a `targets` array with the per-platform status and engagement metrics: ```json theme={null} { "id": "6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "text": "Our spring release is live. Details: https://example.com/spring #release", "status": "published", "targets": [ { "platform": "bluesky", "platform_post_id": "at://did:plc:abc123xyz/app.bsky.feed.post/3kq2example", "status": "published", "permalink": "https://bsky.app/profile/did:plc:abc123xyz/post/3kq2example", "metrics": { "likes": 12, "comments": 3, "shares": 2, "saves": 0, "extra": null }, "metrics_synced_at": "2026-10-17T10:30:00Z" } ] } ``` **Metrics notes:** `likes` is Bluesky's like count, `comments` its reply count, and `shares` its repost count. Bluesky reports no saves, so `saves` stays `0`. `metrics_synced_at` is when SocialAPI last read the counts from Bluesky. **Posts published outside SocialAPI:** SocialAPI imports the 25 most recent posts of the account when you connect it and when you call `POST /v1/accounts/{id}/sync`. *** ## 7. Quirks, errors, and recovery ### No post editing Bluesky has no way to edit a published post. `PATCH /v1/posts/{pid}` returns `409 post.state_invalid` for a published post. Delete the post and publish a new one. ### Video Bluesky video is not supported. A video uploaded to SocialAPI as `media_id` fails validation with `400 publishing.validation_failed`. A video sent as a `url` that SocialAPI recognizes as a video is skipped, and the post publishes without it. ### Mentions are plain text An `@handle` in the text is not a mention. The mentioned account is not notified. ### Image download failure SocialAPI downloads each `url` image when it publishes. When the download fails, the target fails with `500 system.internal`. Check that the URL is public and returns the image, then send `POST /v1/posts/{pid}/retry`. ### Posting caps SocialAPI refuses a publish that would pass 25 posts in an hour or 50 posts in 24 hours for one account, with `429 publishing.velocity_limit`. A scheduled post is held and published when the window clears. Bluesky also limits writes per account to 5,000 points per hour and 35,000 per day, where a new post costs 3 points. When Bluesky refuses first, the target fails with `429 platform.bluesky.rate_limit`. See [Posting velocity caps](/guides/rate-limits#posting-velocity-caps). ### Errors on a failed target A publish that fails after the request returned puts a structured error on the target, in `targets[].error`: ```json theme={null} { "code": "platform.bluesky.rate_limit", "message": "Platform rate limit exceeded", "category": "rate_limit", "caused_by": "platform" } ``` * `401 platform.bluesky.auth`: Bluesky rejected the saved credential. Connect the account again. * `403 platform.bluesky.account_takedown`: Bluesky suspended the account. Connecting again does not restore access. * `429 platform.bluesky.rate_limit`: Bluesky's quota is used up. Wait, then retry. * `400 validation.platform_param`: Bluesky rejected a parameter. `error.message` carries Bluesky's reason. * `400 validation.field_invalid`: an image sent as a `url` is larger than 2 MB. Every code is in [Errors](/guides/errors). ### Recovery * **Auth error.** Connect the account again with a working app password: `POST /v1/accounts/connect` with the same `platform` and `brand_id`. See [Connect accounts](/guides/connect-accounts). * **Rate limit.** Wait for the `Retry-After` header, then send `POST /v1/posts/{pid}/retry`. See [Delivery](/guides/delivery). * **Image too large.** Resize the image to 2 MB or less, upload it again, and publish a new post. *** ## 8. Full worked example The following example uploads an image, publishes a post with it, then checks its status. **Step 1: Upload media** ```bash theme={null} # Get a presigned upload URL curl "https://api.social-api.ai/v1/media/upload-url?filename=release.jpg&media_type=image/jpeg" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response: ```json theme={null} { "upload_url": "https://upload.example.com/media/release.jpg?X-Amz-Signature=abc123", "media_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479" } ``` Upload the file directly to the presigned URL, then verify: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/media/f47ac10b-58cc-4372-a567-0e02b2c3d479/verify \ -H "Authorization: Bearer $SOCAPI_KEY" ``` **Step 2: Publish the post** ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Our spring release is live. Details: https://example.com/spring #release", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }] }' ``` Response (HTTP 201, delivery continues after the response): ```json theme={null} { "id": "6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "status": "publishing", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "bluesky", "status": "publishing" } ] } ``` **Step 3: Check the status** ```bash theme={null} curl "https://api.social-api.ai/v1/posts/6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200) once published: ```json theme={null} { "id": "6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "text": "Our spring release is live. Details: https://example.com/spring #release", "status": "published", "targets": [ { "platform": "bluesky", "platform_post_id": "at://did:plc:abc123xyz/app.bsky.feed.post/3kq2example", "status": "published", "permalink": "https://bsky.app/profile/did:plc:abc123xyz/post/3kq2example" } ], "created_at": "2026-10-16T09:00:00Z", "published_at": "2026-10-16T09:00:03Z" } ``` *** ## 9. Required OAuth scopes Bluesky has no OAuth scopes. The app password gives SocialAPI the access it needs to publish, with no extra option. Direct messages are the only feature that needs an option on the app password: see [Bluesky scopes](/platforms/bluesky#scopes). The scopes a scoped API key needs, the credit cost of a post, and the MCP tools that publish are on [Publishing](/guides/publishing). # Facebook API Source: https://docs.social-api.ai/platforms/facebook Connect Facebook Pages to publish posts and Reels, moderate comments, send private replies and Messenger DMs, and read mentions through one API. The Facebook API in SocialAPI connects Facebook Pages. It publishes text, link, photo, video, album, Reel, and Story posts, reads and moderates comments, sends private replies and Messenger direct messages (DMs), and reads mentions. SocialAPI's own Meta app connects the Pages, so you register nothing: see [Platform apps](/guides/platform-credentials). A Facebook profile is not supported: only Pages. ## At a glance | Field | Value | | - | - | | Platform slug | `facebook` | | Auth | OAuth 2.0 (Meta), one login for every Page the person manages | | Platform API | Meta Graph API (Facebook Pages and Messenger) | | Status | Available | ## Capabilities An unsupported call returns `501 resource.not_supported`. | Feature | Status | Condition | | - | - | - | | Publish text, link, photo, video, multi-photo album, Reel, Story | Yes | See [Facebook publishing](/platforms/facebook/publishing) | | Schedule a post | Yes | `scheduled_at` on the post | | First comment on a post | Yes | Not on a Story | | Update a published post | No | `PATCH /v1/posts/{pid}` returns `409 post.state_invalid` for a published post. A `draft`, `scheduled`, or `failed` post can be edited before it publishes. To correct it, delete the post with `DELETE /v1/posts/{pid}` and publish a new one | | Delete a published post | Yes | | | List posts | Yes | Posts in the Page feed. List posts does not return a Reel, even when `share_to_feed` is `true` | | List comments and replies | Yes | Comments are read per post | | Reply to a comment | Yes | | | Hide, unhide, delete a comment | Yes | | | Like or unlike a comment | Yes | The Page likes the comment | | Private reply to a comment | Yes | One per comment, within 7 days of the comment | | DMs: list, read, send | Yes | Inside the 24-hour messaging window. Text and attachments. A send ignores `quick_replies`, `buttons`, and `cards` | | React to a DM | Yes | | | Mentions | Partial | `GET /v1/accounts/{id}/mentions` returns posts that tag the Page. A mention in a comment arrives as a `mention.received` event | | Reply to a mention | Yes | A public comment on the mention | | Reviews | No | SocialAPI does not read Facebook reviews: Meta deprecated Page recommendations in Graph API v22.0. `GET /v1/inbox/reviews/{account_id}` returns `422 account.platform_unsupported` | | Automations | Yes | Comment and DM triggers, buttons on the first private reply, multi-step flows. A `send_dm` action delivers no buttons. No `follows_account` filter and no Follow gate: `400 validation.filter_unsupported` | | Analytics | Yes | See [Analytics](/guides/analytics) | | Page insights | Yes | `GET /v1/platforms/facebook/accounts/{id}/summary` (trailing 30 days) and `GET /v1/platforms/facebook/accounts/{id}/insights/daily`. Needs `read_insights`: see [Page insights](#page-insights) | | Webhook events | Yes | See [Webhook events](#webhook-events) | The cross-platform matrix is on [Platforms](/platforms/overview). ## Connect an account Connect with `POST /v1/accounts/connect` and `"platform": "facebook"`. The shared flow, request fields, and result are in [Connect accounts](/guides/connect-accounts), and the redirect parameters are in [Redirect parameters](/guides/connect-accounts#redirect-parameters). ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/connect \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "facebook", "redirect_uri": "https://app.example.com/oauth/callback", "state": "session_abc123" }' ``` Response (HTTP 202): ```json theme={null} { "auth_url": "https://www.facebook.com/v25.0/dialog/oauth?client_id=...&state=...", "state": "4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2", "message": "Open this link to connect your facebook account. Once you've authorized, the account will be connected automatically." } ``` After the person authorizes, the redirect carries `status=selection_required` and a `connection_id`: choose the Pages that join the brand as described in [Choose Facebook Pages](/guides/connect-accounts#choose-facebook-pages). When `facebook_page_selection` is `false` on your SocialAPI account (`GET /v1/users/me`), or the connection started from an invite link, the redirect carries `status=success` and the brand's Facebook account instead. That account holds every Page the login granted that none of your other brands already holds. A Page that another of your brands holds stays with that brand. * A login that grants no Page ends with `status=error` and `error=platform.facebook.no_pages` on your `redirect_uri`. * Without a selection step, a login whose every Page belongs to another of your brands ends with `status=error` and `error=platform.facebook.no_assignable_pages`. * A brand whose Facebook account is connected through a different login ends with `status=error` and `error=account.different_login`. Disconnect the brand's Facebook account first. * Reconnecting the same login refreshes the access for every brand that uses it, not only the brand you reconnected from. ### Pages of an account List the Pages of a connected account with `GET /v1/accounts/{id}/pages`. Each Page has at least an `id` (`sapi_page_` prefix), `platform_page_id`, `name`, `is_default`, and `is_active`. A Page also carries `profile_picture_url`, `category`, `followers_count`, `username`, `link`, `verification_status`, `cover_photo_url`, `about`, and `enriched_at`, each omitted when it has no value. Publish to a Page with `page_id` on the post target: see [Delivery](/guides/delivery). `PATCH /v1/accounts/{id}/pages/{pageId}` sets `is_default` or `is_active`. ### One login, many brands A Facebook login is a shared credential. One login can serve many brands, and each Page belongs to exactly one brand. List the Pages a login can manage, live from Facebook, with the brand that holds each: ```bash theme={null} curl "https://api.social-api.ai/v1/platforms/facebook/logins/cred_01J8X3Q4R5M6N7P8V2K0W1J/pages" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "login_id": "cred_01J8X3Q4R5M6N7P8V2K0W1J", "pages": [ { "platform_page_id": "104857392011223", "name": "Acme Bakery", "assignable": true } ], "count": 1 } ``` | Field | Description | | - | - | | `login_id` | The login you asked about | | `pages[].platform_page_id`, `name` | Facebook's Page ID and the Page's name | | `pages[].picture_url`, `category` | The Page's picture and category. Each is omitted when Facebook returns none | | `pages[].assignable` | `true` when no brand holds the Page. `false` when a brand holds it: `assigned_brand_id`, `assigned_brand_name`, and `assigned_account_id` then name it | | `count` | The number of Pages in `pages` | `login_id` is on each connected account. Move a Page to a brand with `POST /v1/platforms/facebook/logins/{loginId}/pages/{pageId}/assign`. A Page that another of your brands holds moves to the target brand. The call needs the `accounts:manage` scope. ```bash theme={null} curl -X POST "https://api.social-api.ai/v1/platforms/facebook/logins/cred_01J8X3Q4R5M6N7P8V2K0W1J/pages/104857392011223/assign" \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{"brand_id": "7d2e4b1a-9c3f-4e5d-8a6b-1f2e3d4c5b6a"}' ``` Response (HTTP 201): ```json theme={null} { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "7d2e4b1a-9c3f-4e5d-8a6b-1f2e3d4c5b6a", "platform_page_id": "104857392011223" } ``` | HTTP | Code | When it occurs | | - | - | - | | 400 | `validation.field_required` | The body has no `brand_id` | | 400 | `validation.page_not_granted` | The Page is not accessible through this login | | 403 | `platform.facebook.page_cap` | `facebook_page_selection` is `true` on your SocialAPI account and the brand already holds as many Facebook Pages as your plan allows. See [Facebook Pages per brand](/guides/plans#facebook-pages-per-brand) | | 404 | `resource.not_found` | The login does not exist | | 404 | `brand.not_found` | The brand does not exist or your key cannot reach it | | 409 | `account.different_login` | The brand's Facebook account uses a different login | ## Comments List comments on a post with `GET /v1/inbox/comments/{postId}`, where `postId` is the post's platform ID. Reply, hide, unhide, delete, like, and unlike work as described in [Comments](/guides/comments). * **Edited comments.** Facebook sends an edited comment again, as another `comment.received` event. Automations ignore edits and answer new comments only. * **Page admins.** An [Automation](/guides/automations) skips comments and messages from the Page itself. A Page admin who comments from a personal profile is a commenter like any other, so the comment triggers the Automation. ## Private replies A private reply sends a one-time DM to the person who commented. Send it with `POST /v1/inbox/comments/{postId}/{commentId}/private-reply`. Facebook allows one private reply per comment, within 7 days of the comment. A second reply, or a comment older than 7 days, returns `400 platform.facebook.private_reply_ineligible`. ## Direct messages and the messaging window A Page can message a person only inside the 24-hour messaging window that the person's last message opened. Outside it, the send returns `400 platform.facebook.window_closed`. * **Message types.** Text and attachments. A DM send ignores `quick_replies`, `buttons`, and `cards`: it delivers only the text. Buttons work on a [private reply](/guides/comments#send-a-private-reply). Each attachment in `attachments[]` has a `source_type` (`url`, `media_id`, or `platform_attachment_id`) and a `type`. `type` is required for `platform_attachment_id`, or the request returns `400 validation.attachment_type_required`. Field rules are in [Direct messages](/guides/direct-messages). * **Reactions.** `POST /v1/inbox/conversations/{id}/messages/{messageId}/reaction` reacts to a message, and `DELETE` on the same path removes the reaction. * **Delivery status.** `dm.status.sent`, `dm.status.delivered`, and `dm.status.read` events follow each DM you send. * **No message tags.** A `message_tag` on a Facebook DM, including `HUMAN_AGENT`, is not sent: the DM goes out untagged and the 24-hour window applies. ## Mentions `GET /v1/accounts/{id}/mentions` returns the posts that tag the Page. A mention inside a comment is not in that list: it arrives as a `mention.received` event. Reply to a mention with `POST /v1/accounts/{id}/mentions/reply`, which posts a public comment. Details are in [Mentions](/guides/mentions). ## Page insights `GET /v1/platforms/facebook/accounts/{id}/summary` returns the Page's followers, views, reach, post engagements, and net follower change over the trailing 30 days. ```bash theme={null} curl "https://api.social-api.ai/v1/platforms/facebook/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/summary" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "follower_count": 12840, "views": 48210, "reach": 31975, "interactions": 2216, "net_follows": 154, "period_start": "2026-09-17T09:00:00Z", "period_end": "2026-10-17T09:00:00Z" } ``` `interactions` counts post engagements, and `net_follows` is new follows minus unfollows over the period. `GET /v1/platforms/facebook/accounts/{id}/insights/daily` returns one point per day, oldest first. `days` sets how many trailing days, from 1 to 365 (default 365). A day with no data is left out. ```bash theme={null} curl "https://api.social-api.ai/v1/platforms/facebook/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/insights/daily?days=2" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ {"date": "2026-10-15", "followers": 12831, "views": 1620, "reach": 1104, "interactions": 71}, {"date": "2026-10-16", "followers": 12840, "views": 1755, "reach": 1187, "interactions": 83} ] } ``` `followers` is the count at the end of the day. `views`, `reach`, and `interactions` are that day's totals. Both routes take an optional `page_id`, the `id` from `GET /v1/accounts/{id}/pages`. Without it they report on the account's default Page. Both are free, need the `accounts:read` scope on a scoped API key, and have no MCP tool. Both need the `read_insights` scope, requested since 2026-10-17 with the `analytics.read` permission. Post metrics in [Analytics](/guides/analytics) do not need it. | HTTP | Code | When it occurs | | - | - | - | | 400 | `validation.days_invalid` | `days` is below 1 or above 365 | | 403 | `platform.facebook.insights_scope_missing` | The Page was connected before 2026-10-17, its login declined `read_insights`, or the person who connected it does not hold the Analyze task on the Page. A reconnect fixes the first two; for the third, grant the Analyze task on Facebook, then reconnect | | 403 | `account.permission_missing` | The account does not have the `analytics.read` [permission](/guides/permissions) | | 404 | `account.not_found` | The account does not exist | | 404 | `resource.not_found` | `page_id` is not an active Page of the account, or the account has no default Page | | 501 | `resource.not_supported` | `{id}` is not a Facebook account | | 502 | `platform.facebook.` or `platform.facebook.api_error` | Facebook returned an error, where `` is Meta's error code, or failed with no code: see [Errors](/guides/errors) | ## Reviews SocialAPI does not read Facebook reviews: Meta deprecated Page recommendations in Graph API v22.0. `GET /v1/inbox/reviews/{account_id}` for a Facebook account returns `422 account.platform_unsupported`, and no other call replaces it. ## Webhook events Facebook Pages emit these events to your webhook endpoints. Payloads are in [Webhook events](/guides/webhook-events). | Event | When it fires | | - | - | | `comment.received` | A person comments on a Page post, or edits a comment | | `mention.received` | The Page is tagged in a post or a comment | | `dm.received` | A person sends a Messenger message | | `dm.sent` | A message left the Page, including one sent from Meta Business Suite | | `dm.referral` | A person opened a conversation from an ad or referral link without sending a message | | `dm.postback` | A person tapped a button on a private reply | | `dm.status.sent`, `dm.status.delivered`, `dm.status.read` | Facebook accepted, delivered, or showed as read a DM you sent | | `account.connected` | You finished connecting or reconnecting the account | | `account.disconnected` | The person revoked access on Facebook, or SocialAPI flagged the account for reconnection (`reconnect_required: true`, see `reason`) | | `account.updated` | The account lost [permissions](/guides/permissions) you did not remove yourself (`change` is `permissions_reduced`) | | `page.removed` | A Page was removed from the login | | `post.published`, `post.partial`, `post.failed` | A post to this account finished: see [Delivery](/guides/delivery) | Every `post.*` event in [Webhook events](/guides/webhook-events#posts) is emitted for this platform. ## Limits | Limit | Value | | - | - | | Post text | 63,206 characters | | Photo | 10 MB: JPEG, PNG, GIF, or WebP | | Video | 4 GB and 240 minutes: MP4, QuickTime, or AVI | | Multi-photo album | 2 to 10 images | | Posts | 100 per 24 hours and 25 per hour for each Page: see [Posting velocity caps](/guides/rate-limits#posting-velocity-caps) | | Private reply | 1 per comment, within 7 days | | Facebook's own rate limit | `429 platform.facebook.rate_limit`. Credit rules: see [when a credit comes back](/guides/plans#when-a-credit-comes-back) | A Page access token does not expire on its own. Reconnect only when a call returns `401 platform.facebook.auth`, for example after the person revoked access. ## Scopes SocialAPI's Meta app requests these scopes at connect. You apply for no app review. Each scope belongs to the account [permissions](/guides/permissions) in the last column, and the consent screen asks for the scopes of the permissions you selected. The scopes marked Every account are always requested. | Scope | Used for | Permissions | | - | - | - | | `pages_show_list` | List the Pages the person manages | Every account | | `business_management` | List Pages owned through a Business Portfolio | Every account | | `pages_read_engagement` | Read likes, comments, and shares on Page posts | Every account | | `pages_read_user_content` | Read posts and comments that people make on the Page | `comments.read`, `comments.write` | | `pages_manage_engagement` | Reply to, hide, delete, and like comments | `comments.write` | | `pages_manage_metadata` | Subscribe the Page to webhook events | Every account | | `pages_messaging` | Send and receive Messenger DMs | `messages.read`, `messages.write` | | `pages_manage_posts` | Create, edit, and delete Page posts | `posts.write` | | `read_insights` | [Page insights](#page-insights). A login that declines it keeps `analytics.read` | `analytics.read` | | `public_profile` | Identify the person who connected | Every account | Facebook asks for messages as 1 scope, covering both reading and writing, so the consent screen shows the broader scope. The account stays limited to the permissions you selected: an account connected with `messages.read` alone cannot send messages. ## Related * [Facebook publishing](/platforms/facebook/publishing): fields, media rules, and quirks for publishing * [Connect accounts](/guides/connect-accounts): the shared connect flow * [Automations](/guides/automations): comment-to-DM flows * [Platforms](/platforms/overview): the capability matrix * [Errors](/guides/errors): every error code # Facebook publishing Source: https://docs.social-api.ai/platforms/facebook/publishing Publish text, link, photo, video, album, Reel, and Story posts to Facebook Pages through SocialAPI, with platform fields, limits, and error recovery. ## 1. Overview A Facebook post goes to one Facebook Page. SocialAPI picks the format from the `media` you send: no media gives a text or link post, one image a photo post, one video a video post, and 2 to 10 images a multi-photo album. Two richer formats are opt-in with `platform_data.content_type`: `"reel"` publishes a Reel, and `"stories"` publishes a 24-hour Story. Every format is described below. | Field | Value | | - | - | | Platform slug | `facebook` | | Auth type | OAuth 2.0 (Meta) | | Platform API | Meta Graph API (Facebook Pages) | | Create post | Yes | | Update post | Not after publishing (`409 post.state_invalid`). `draft`, `scheduled`, and `failed` posts can be edited | | Delete post | Yes | | Schedule | Yes (deferred publish via `scheduled_at`) | | First comment | Yes (best-effort; not on Stories) | | Posting caps | 100 posts per 24 hours and 25 per hour for each Page: see [Posting velocity caps](/guides/rate-limits#posting-velocity-caps) | | Carousel (multi-photo) | Yes (2 to 10 photos; images only) | | Reel | Yes (`content_type: "reel"`; one vertical video) | | Story | Yes (`content_type: "stories"`; one photo or video, expires after 24 hours) | *** ## 2. Supported media | Content type | How to trigger | Notes | | - | - | - | | Text only | No `media`; no `link` in `platform_data` | Publishes a plain text status update | | Link preview | No `media`; include `link` in the target's `platform_data` | Facebook generates an Open Graph preview card | | Single photo | One image entry in `media` | Publishes a Page photo; accepts `alt_text` and the targeting and place fields, see "Platform data fields" below | | Single video | One video entry in `media` | Publishes a Page video. SocialAPI treats a file whose name ends in `.mp4`, `.mov`, `.avi`, `.mkv`, or `.webm` as a video. Accepted formats for uploaded files are in File limits below. Accepts `title` and the video, targeting, and place fields, see "Platform data fields" below | | Reel | One vertical video entry in `media` plus `content_type: "reel"` in the target's `platform_data` | Publishes a Page Reel; accepts `content_type`, `share_to_feed` (default `true`), `title`, and `place` | | Story | One photo or video entry in `media` plus `content_type: "stories"` in the target's `platform_data` | Publishes a Page Story that expires after 24 hours. Captions, links, and audience targeting are ignored | | Multi-photo album | 2 to 10 image entries in `media` | Publishes one post containing every photo, with one caption. Accepts `link`, the CTA fields, age and country targeting, and `place`. | The format follows the `media` you pass: * `media` is empty: a text or link post. * `media` has one entry: a photo or a video, by the file extension. * `media` has 2 to 10 entries: a multi-photo album. Reels and Stories override that routing. Set `content_type` in the target's `platform_data` to `"reel"` (one vertical video) or `"stories"` (one photo or video), and that intent takes precedence over the rules above. The value is case-insensitive; any other value publishes a standard feed post and returns a warning naming it. **Text limit:** 63,206 characters. More returns `400 publishing.validation_failed`. **File limits for media you upload to SocialAPI** (`source_type: "media_id"`): images must be JPEG, PNG, GIF, or WebP and at most 10 MB. Videos must be MP4, QuickTime, or AVI, at most 4 GB, and at most 240 minutes. A file outside these limits fails validation with `400 publishing.validation_failed`. For a `url` item, SocialAPI checks the file type against these limits, and the size when the host reports it. The duration of a video sent by URL is not checked in advance. **Multi-photo album rules:** * 2 to 10 photos per post. Passing more than 10 entries returns a validation error. * Images only. Mixing images and videos in the same album is rejected with a validation error. Facebook's Graph API does not support mixed-media albums; publish separate posts. * If publishing the album fails, no partial post appears on the Page, and you can retry the whole post. * For best rendering, use consistent 1,080 by 1,080 px (1:1) images. *** ## 3. Create post Use `POST /v1/posts` with `targets` targeting a Facebook Page account. The `text` field becomes the post message. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Exciting news from our team. Read the full announcement on our website.", "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform_data": { "link": "https://example.com/announcement" } }] }' ``` Response (HTTP 201): ```json theme={null} { "id": "6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "text": "Exciting news from our team. Read the full announcement on our website.", "status": "publishing", "hidden": false, "retry_count": 0, "created_at": "2026-10-13T09:00:00Z", "updated_at": "2026-10-13T09:00:00Z", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "facebook", "status": "pending", "text": "Exciting news from our team. Read the full announcement on our website." } ] } ``` SocialAPI publishes in the background. The target moves from `pending` to `publishing`, then to `published` or `failed`. Poll `GET /v1/posts/{pid}` or subscribe to the `post.published`, `post.partial`, and `post.failed` [webhook events](/guides/webhook-events). Publishing or scheduling costs 1 post credit; a draft is free (see [Plans and credits](/guides/plans)). ### Platform data fields Send these keys flat in the target's `platform_data` (`targets[].platform_data`), or in `platform_data.facebook` on the post, which applies them to every Facebook target. When both set the same key, the target's value wins. Which keys apply depends on the content type, shown in the "Applies to" column. | Key | Type | Applies to | Notes | | - | - | - | - | | `content_type` | string | reel, stories | Set to `"reel"` or `"stories"` to publish those formats. Omit it for standard feed, photo, video, or album routing. Case-insensitive; any other value publishes a standard post and returns a warning naming it. | | `link` | string (URL) | text/link posts, multi-photo albums | Attaches a link preview card. Facebook generates the Open Graph preview. | | `cta_type` | string | text/link posts, multi-photo albums | Call-to-action button type, for example `SHOP_NOW` or `LEARN_MORE`. Must be set together with `cta_link`. | | `cta_link` | string (URL) | text/link posts, multi-photo albums | Destination URL for the call-to-action button. Must be set together with `cta_type`. | | `min_age` | number, 13 to 65 | text/link posts, photo, video, multi-photo albums | Minimum audience age. | | `max_age` | number, 13 to 65 | text/link posts, photo, video, multi-photo albums | Maximum audience age. Must be greater than or equal to `min_age`. | | `countries` | array of ISO 3166-1 alpha-2 codes | text/link posts, photo, video, multi-photo albums | Restricts the audience to these countries, for example `["FR", "BE"]`. | | `place` | string (numeric Page ID) | text/link posts, photo, video, multi-photo albums, reel | Tags a location on the post. | | `alt_text` | string | single photo | Accessibility text for the photo. | | `title` | string | single video, reel | Title shown with the video or Reel. | | `share_to_feed` | boolean | reel | Default `true`: the Reel is also shared to the Page feed on Facebook, not just the Reels tab. Send `false` to keep it in the Reels tab only. Either value, the Reel is not returned by List posts. | | `no_story` | boolean | single photo, single video | When `true`, the photo or video is added without posting a separate update to the Page feed. | | `content_category` | string | single video | One of Facebook's video categories: `BEAUTY_FASHION`, `BUSINESS`, `CARS_TRUCKS`, `COMEDY`, `CUTE_ANIMALS`, `ENTERTAINMENT`, `FAMILY`, `FOOD_HEALTH`, `HOME`, `LIFESTYLE`, `MUSIC`, `NEWS`, `POLITICS`, `SCIENCE`, `SPORTS`, `TECHNOLOGY`, `VIDEO_GAMING`, or `OTHER`. | | `content_tags` | array of numeric Facebook Page or profile IDs | single video | Tags Pages or profiles in the video. | | `embeddable` | boolean | single video | Allows the video to be embedded on other sites. | | `secret` | boolean | single video | Hides the video from the Page's public video list and search. | | `social_actions` | boolean | single video | Enables like, comment, and share actions on embeds of the video. | | `child_attachments` | array of 2 to 10 `{link, name?, description?, picture?}` | text/link posts with no media attached | Cards of a link carousel. Each card needs a `link`; `name`, `description`, and `picture` are optional. Returns an error when the post has media. See "Link carousel" below. | | `multi_share_optimized` | boolean | with `child_attachments` | When `true`, Facebook orders the cards for best performance instead of the order sent. | | `multi_share_end_card` | boolean | with `child_attachments` | When `true`, an end card promoting the Page appears after the last link card. | SocialAPI forwards any additional keys in `platform_data` that the table above lists for the post type you publish, and no others. Any other key, including a key listed for another post type, is not sent, and the response lists each one as a validation warning naming the key. For example, `link` on a single photo returns a warning and the photo publishes without it. Facebook has no AI-generated label: `is_ai_generated` returns a warning and is not sent. ### Photo post Provide a single image entry in `media`. Photo posts also accept `alt_text`, `min_age`, `max_age`, `countries`, `place`, and `no_story`; see the table above. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "Our new product is here.", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }] }' ``` ### Multi-photo album Provide 2 to 10 image entries in `media` to publish a multi-photo album. The entries are published as a single album post with one caption. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "Highlights from this week.", "media": [ { "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }, { "source_type": "media_id", "source": "8d5e9b2c-1a3f-4c6d-9e8b-2f7a4c1d6e0b" }, { "source_type": "media_id", "source": "3b1f7a90-6c2d-4e85-b9a1-0d4f8c3e2a17" } ] }' ``` Albums also accept `link`, `cta_type` plus `cta_link`, age and country targeting, and `place` in the target's `platform_data`: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Shop the spring collection.", "media": [ { "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }, { "source_type": "media_id", "source": "8d5e9b2c-1a3f-4c6d-9e8b-2f7a4c1d6e0b" }, { "source_type": "media_id", "source": "3b1f7a90-6c2d-4e85-b9a1-0d4f8c3e2a17" }, { "source_type": "media_id", "source": "5e2c8a14-7b9d-4f03-a6c1-9d8e0b3f4a26" } ], "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform_data": { "cta_type": "SHOP_NOW", "cta_link": "https://example.com/spring" } }] }' ``` **Limits:** * Minimum 2, maximum 10 photos per album. * Images only. Including a video entry (any of `.mp4`, `.mov`, `.avi`, `.mkv`, `.webm`) in a 2+ item `media` list returns a validation error before any upload to Facebook. ### Link carousel Publish 2 to 10 link cards in one post by setting `child_attachments` instead of `media` or `link`. This is different from a multi-photo album: no media is uploaded, and each card carries its own link, title, and description. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Top picks this week.", "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform_data": { "child_attachments": [ { "link": "https://example.com/product-1", "name": "Product one", "description": "Our best seller", "picture": "https://example.com/images/product-1.jpg" }, { "link": "https://example.com/product-2", "name": "Product two", "description": "New this week" } ], "multi_share_end_card": true } }] }' ``` Provide 2 to 10 cards. `child_attachments` on a post with `media` returns a validation error. `link` set together with `child_attachments` is not sent and returns a validation warning. ### Video post Provide a single video entry in `media`. It is published as a Page video. Video posts also accept `min_age`, `max_age`, `countries`, `place`, `no_story`, `content_category`, `content_tags`, `embeddable`, `secret`, and `social_actions`; see the table above. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "Watch our latest brand film.", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }] }' ``` ### Reel Publish a short-form vertical video as a Page Reel by setting `content_type: "reel"` in the target's `platform_data`. Provide exactly one vertical video entry in `media`. Facebook processes Reels before they go live. SocialAPI waits up to about 10 minutes for that to finish. After that, the target fails with `504 platform.facebook.reel_timeout`. If Facebook cannot process the video, the target fails with `422 platform.facebook.reel_processing_failed`. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Behind the scenes #reels", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform_data": { "content_type": "reel", "share_to_feed": true } }] }' ``` `share_to_feed` defaults to `true`, which shares the Reel to the Page feed on Facebook as well as the Reels tab. List posts does not return a Reel with either value. Reels also accept `title` and `place`; see the table above for every option. Requirements: exactly one video, vertical orientation (9:16), and a duration of 3 to 90 seconds. SocialAPI warns when the duration is outside that range; Facebook enforces the aspect ratio at publish time and rejects videos that do not comply. ### Story Publish a 24-hour Page Story by setting `content_type: "stories"` in the target's `platform_data`. Provide exactly one photo or video entry in `media`. A video Story waits up to about 10 minutes for Facebook to process it. After that, the target fails with `504 platform.facebook.story_timeout`. If Facebook cannot process the video, the target fails with `422 platform.facebook.story_processing_failed`. Stories carry no caption, link, call-to-action, or audience targeting. `text` is ignored, and any `platform_data` key other than `content_type` is not sent and returns a validation warning. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform_data": { "content_type": "stories" } }] }' ``` Set `content_type` to `"stories"`, plural, to publish a Story. The singular `"story"` is not recognized and publishes a standard feed post instead, with a validation warning. No other `platform_data` key applies to Stories. Requirements and limits: * Exactly one media item. Passing zero or more than one entry returns a validation error before any upload to Facebook. * Photos: JPEG, BMP, PNG, GIF, or TIFF, up to 10 MB (keep PNGs under 1 MB). * Videos: 9:16 vertical, 1080 by 1920 recommended (minimum 540 by 960), 3 to 90 seconds, 24 to 60 fps. * The photo or video must not have been used in a previously published post. Facebook rejects a Story whose media already appeared elsewhere, so upload a fresh asset for each Story. * Stories are a Pages-only format and expire after 24 hours (see [Stories expire](#stories-expire)). ### First comment Set `first_comment` to a string to post an automated comment immediately after publish. This is best-effort: if the comment fails (for example due to a temporary rate limit), the post is still considered published. Page Stories are the one exception: a Story has no comments edge on the Graph API, so `first_comment` is skipped for `content_type: "stories"` targets. Every other post type (text, link, single photo, multi-photo album, single video, Reel) accepts one. ```json theme={null} { "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "Exciting news from our team.", "first_comment": "Read the full announcement: https://example.com/blog" } ``` ### Scheduling Set `scheduled_at` to an ISO 8601 timestamp (UTC) to defer publishing: ```json theme={null} { "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "Scheduled announcement.", "scheduled_at": "2026-11-15T10:00:00Z" } ``` *** ## 4. Update post A published Facebook post cannot be edited through SocialAPI. `PATCH /v1/posts/{pid}` returns `409 post.state_invalid` for any post whose status is not `draft`, `scheduled`, or `failed`, with the message "Only draft, scheduled, or failed posts can be edited". A post in `draft`, `scheduled`, or `failed` status can be edited with `PATCH /v1/posts/{pid}` before it publishes. To correct a published post, delete it with `DELETE /v1/posts/{pid}` and publish a new post. *** ## 5. Delete post ```bash theme={null} curl -X DELETE https://api.social-api.ai/v1/posts/6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d \ -H "Authorization: Bearer $SOCAPI_KEY" ``` To remove only the Facebook target of a cross-platform post, unpublish it by `account_id`. `DELETE /v1/posts/{pid}` always removes every target. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts/6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d/unpublish \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{"account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J"}' ``` Deleting removes the post from the Page. See [Delete a post](/guides/delivery#delete-a-post) for how a cross-platform post is handled. *** ## 6. Retrieve posts Use `GET /v1/posts` to list posts. Filter by platform or account: ```bash theme={null} curl "https://api.social-api.ai/v1/posts?platform=facebook&account_ids=acc_01HZ9X3Q4R5M6N7P8V2K0W1J&limit=20" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Each post includes a `targets` array with per-platform status and engagement metrics: ```json theme={null} { "id": "6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "text": "Exciting news from our team.", "status": "published", "targets": [ { "platform": "facebook", "platform_post_id": "123456789012345_987654321098765", "status": "published", "permalink": "https://www.facebook.com/permalink/123456789012345", "platform_media": [ { "type": "image", "url": "https://scontent.xx.fbcdn.net/v/t39.30808-6/123_n.jpg" } ], "metrics": { "likes": 15, "comments": 3, "shares": 7, "saves": 0, "extra": null }, "metrics_synced_at": "2026-04-10T12:00:00Z" } ] } ``` **Metrics notes:** * `likes` and `comments` are synced from Facebook. * `shares` is populated for Facebook posts. * `saves` is not available on Facebook and is always `0`. * `metrics_synced_at` reflects when SocialAPI last refreshed the metrics from Facebook. * A Reel reports `likes`, `comments`, and a view count under `metrics.views` (also under `metrics.extra.views`). Its `shares` stays `0`, because Facebook does not report a Reel's share count to SocialAPI. * A target whose metrics cannot be read is returned with a structured `error` object rather than omitted from the response. An absent target therefore means the post was never published to that platform, not that the refresh failed. Check `error.code` to tell the cases apart: `resource.not_found` means the platform did not return the post in its recent listing, and `resource.not_supported` means the platform does not expose metrics for it. ### Settings on published posts A Facebook target can include a `settings` object with the options Facebook reports back for the published post: | Field | Type | Description | | - | - | - | | `place` | object | The tagged location: `id` and `name`. | | `countries` | array of strings | ISO 3166-1 alpha-2 countries the post is restricted to. | | `min_age` | number | Minimum audience age set on the post. | | `max_age` | number | Maximum audience age set on the post. | | `cta_type` | string | Call-to-action button type on the post. | | `cta_link` | string | Call-to-action destination URL. | | `child_attachments` | array | Cards of a link carousel, each with `link`, `name`, `description`, and `picture`. | | `multi_share_optimized` | boolean | Whether Facebook picked the card order. | | `multi_share_end_card` | boolean | Whether the link carousel shows an end card. | `settings` is absent when Facebook reported none of these for the post. It is also returned for posts published outside SocialAPI, since it reflects what Facebook reports at the time of the read rather than what was sent through this API. ### Media on published posts A Facebook target includes `platform_media`: every photo and video on the published post, album photos in order. It is returned for posts published outside SocialAPI too, so you can read the photos and videos of a Page's existing posts. | Field | Type | Description | | - | - | - | | `type` | string | `image` or `video`. | | `url` | string | Link to the file, hosted by Facebook. | | `thumbnail_url` | string | Link to a video's cover image. Absent on images. | Link and share posts have no `platform_media`: their image is a preview of the linked site, not media the Page posted. A Reel has none either, since List posts does not return Reels, whatever `share_to_feed` is. The links expire. Download the files when you read the post rather than storing the links. To get fresh links, call `POST /v1/accounts/{id}/sync`, then read the post again. `url` is absent when Facebook does not expose a video's file. The cover image is still included. `POST /v1/accounts/{id}/sync` covers up to the 500 most recent posts on each Page of the account. Each account can be synced once every 5 minutes; an earlier call returns `429 accounts.sync_rate_limited` with a `Retry-After` header. The account needs the `posts.read` [permission](/guides/permissions): without it the call returns `403 account.permission_missing`. `platform_media` is separate from `media`, which lists what you sent when publishing through SocialAPI and is empty for posts published elsewhere. *** ## 7. Quirks, errors, and recovery ### Page access A Page access token does not expire on its own. A call returns `401 platform.facebook.auth` when the person revoked access or lost their role on the Page: reconnect the account. Connection rules, including which Pages a connection holds, are in [Facebook connect](/platforms/facebook#connect-an-account). ### Multi-photo album semantics A multi-photo album publishes as one post or not at all. Mixing images and videos, more than 10 images, or a Reel with more or fewer than one video fails pre-publish validation with `400 publishing.validation_failed`. With `skip_validation: true`, the target fails with `validation.media_mixed`, `validation.media_too_many`, or `validation.media_invalid`. ### First comment is non-blocking If `first_comment` is set and the comment fails after publish, the post stays `published`. Nothing in the post response reports the failed comment. ### Stories expire Facebook Page Stories are visible for 24 hours. The post stays in SocialAPI, so `GET /v1/posts` can return a Story that is no longer live. A photo or video that appeared in an earlier post is refused as a Story: upload a fresh file for each Story. ### Posting caps SocialAPI refuses a publish that would pass 25 posts in an hour or 100 posts in 24 hours for one Page, with `429 publishing.velocity_limit`. A scheduled post is held and published when the window clears. When Facebook refuses first, the target fails with `429 platform.facebook.rate_limit`. See [Posting velocity caps](/guides/rate-limits#posting-velocity-caps). ### Errors on a failed target A publish that fails after the request returned puts a structured error on the target, in `targets[].error`: ```json theme={null} { "code": "platform.facebook.reel_processing_failed", "message": "reel video processing failed: error", "category": "platform", "caused_by": "platform" } ``` * `401 platform.facebook.auth`: the access token is invalid or revoked. Reconnect the account. * `429 platform.facebook.rate_limit`: Facebook's own limit. Wait, then retry. * `platform.facebook.`: Facebook refused the request with its own error code and message. The HTTP status is `502` unless the code has its own mapping, such as `404 platform.facebook.100` or `403 platform.facebook.230`. Read `message`. Common causes are a missing Page scope, a post that no longer exists, and an invalid media URL. * `502 platform.facebook.api_error`: a failure reaching Facebook, or a response with no usable error. * `422 platform.facebook.reel_processing_failed` and `422 platform.facebook.story_processing_failed`: Facebook could not process the video. * `504 platform.facebook.reel_timeout` and `504 platform.facebook.story_timeout`: Facebook did not finish processing the video in about 10 minutes. Every code is in [Errors](/guides/errors). ### Recovery * **Rate limit.** Wait, then send `POST /v1/posts/{pid}/retry`. See [Delivery](/guides/delivery). * **Auth error.** Reconnect the Facebook account through the OAuth flow to get a fresh token. * **API error.** Read `message`, fix the cause, and retry. *** ## 8. Full worked example The following example publishes a link preview post, then checks the metrics after publish. **Step 1: Publish the post** ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "We just launched our spring collection. Shop now and get 20% off your first order.", "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform_data": { "link": "https://example.com/spring-collection" } }] }' ``` Response: ```json theme={null} { "id": "6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "text": "We just launched our spring collection. Shop now and get 20% off your first order.", "status": "publishing", "targets": [ { "platform": "facebook", "status": "publishing" } ], "created_at": "2026-04-10T09:00:00Z" } ``` **Step 2: Check status** ```bash theme={null} curl "https://api.social-api.ai/v1/posts/6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response once published: ```json theme={null} { "id": "6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "text": "We just launched our spring collection. Shop now and get 20% off your first order.", "status": "published", "targets": [ { "platform": "facebook", "platform_post_id": "123456789012345_987654321098765", "status": "published", "permalink": "https://www.facebook.com/permalink/123456789012345", "metrics": { "likes": 15, "comments": 3, "shares": 7, "saves": 0, "extra": null }, "metrics_synced_at": "2026-04-10T12:00:00Z" } ], "created_at": "2026-04-10T09:00:00Z", "published_at": "2026-04-10T09:00:12Z" } ``` **Step 3: Refresh metrics on demand** ```bash theme={null} curl "https://api.social-api.ai/v1/posts/6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d/metrics" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` *** ## 9. Required OAuth scopes SocialAPI's Meta app requests these scopes during the OAuth flow. You configure nothing. The full list, including `business_management`, is in [Facebook scopes](/platforms/facebook#scopes). Publishing uses these: | Scope | Purpose | | - | - | | `pages_show_list` | List the Pages the person manages | | `pages_read_engagement` | Read likes, comments, and shares on Page posts | | `pages_manage_engagement` | Post the first comment | | `pages_manage_posts` | Create, edit, and delete Page posts | The scopes a scoped API key needs, the credit cost of a post, and the MCP tools that publish are on [Publishing](/guides/publishing). # Google Business Profile API Source: https://docs.social-api.ai/platforms/google Connect Google Business Profile locations to publish local posts, manage reviews, update the profile, and read performance metrics and search keywords. The Google Business Profile API in SocialAPI connects a Google Business Profile location, the business listing shown on Google Search and Maps. It publishes local posts, lists and answers reviews, reads and updates the location's public profile and photos, and reads performance metrics and search keywords. SocialAPI's own Google app connects the location, so you register nothing: see [Platform apps](/guides/platform-credentials). ## At a glance | Field | Value | | - | - | | Platform slug | `google` | | Auth | OAuth 2.0 with Google | | Platform API | Google Business Profile API | | Status | Available | ## Capabilities An unsupported call returns `501 resource.not_supported`. | Feature | Status | Condition | | - | - | - | | Publish a local post | Yes | Update, event, offer, or alert posts. Text, plus at most 1 JPEG or PNG image. See [Google Business Profile publishing](/platforms/google/publishing) | | Schedule a post | Yes | `scheduled_at` on the post | | First comment on a post | No | Google Business Profile has no post comments. SocialAPI ignores `first_comment` on a Google target | | Update a published post | No | `PATCH /v1/posts/{pid}` returns `409 post.state_invalid`. Delete the post and publish a new one | | Delete a published post | Yes | Removes the local post from Google | | List posts | Yes | The location's local posts | | Read a location's details | Yes | See [Locations](/platforms/google/locations) | | Update a location | Yes | Google moderates the edit, and allows 10 location updates per minute per Business Profile | | Photos and videos on the profile | Yes | List, add, and remove. Customer photos are read-only | | List reviews | Yes | Verified locations only. See [Reviews](/platforms/google/reviews) | | Reply to a review | Yes | Google moderates the reply. It is `PENDING` until Google approves it | | Edit or delete a reply, delete a review | No | SocialAPI has no endpoint for them. A new reply replaces the old one | | Comments and Q\&A | No | `501 resource.not_supported`. See [Comments and Q\&A](#comments-and-qa) | | DMs | No | `501 resource.not_supported` | | Mentions | No | `501 resource.not_supported` | | Automations | No | Automations run on Instagram and Facebook accounts. A Google account returns `422 automation.platform_unsupported` | | Performance metrics | Yes | Daily series per location. See [Performance metrics](#performance-metrics) | | Search keywords | Yes | Monthly. See [Search keywords](#search-keywords) | | Webhook events | Yes | Review, location, and connection events. See [Webhook events](#webhook-events) | The cross-platform matrix is on [Platforms](/platforms/overview). ## Connect an account Connect with `POST /v1/accounts/connect` and `"platform": "google"`. The shared flow, request fields, and result are in [Connect accounts](/guides/connect-accounts), and the redirect parameters are in [Redirect parameters](/guides/connect-accounts#redirect-parameters). ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/connect \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "google", "redirect_uri": "https://app.example.com/oauth/callback", "state": "session_abc123" }' ``` Response (HTTP 202): ```json theme={null} { "auth_url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...&state=...", "state": "4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2", "message": "Open this link to connect your google account. Once you've authorized, the account will be connected automatically." } ``` Google-specific rules: * **One location per account.** Each connected `google` account is 1 Business Profile location. * **A choice when the login manages more than 1 location.** The redirect carries `status=selection_required` and a `connection_id` instead of an `account_id`. You choose exactly 1 location: see [Choose a Google Business Profile](/guides/connect-accounts#choose-a-google-business-profile). A login with 1 location connects at once. A login with none ends with `status=error` and `error=platform.google.no_locations` on your `redirect_uri`. A person who leaves the Business Profile permission unticked on Google's consent screen ends with `status=error` and `error=platform.google.insufficient_scope`: ask them to connect again and allow every permission. * **One Google Business Profile per brand.** Connecting a second location to the same brand ends with `status=error` and `error=account.already_linked` on your `redirect_uri`, or returns `409 account.already_linked` from the select call. Assigning a second location with the assign call returns `409 platform.google.brand_has_account`. Assigning a location that another brand already holds with the assign call returns `400 validation.location_already_assigned`. * **More locations later, without a new consent screen.** The login stays. Assign its other locations to your other brands with [`POST /v1/platforms/google/logins/{loginId}/locations/{locationId}/assign`](/platforms/google/locations#connect-another-location-to-a-brand). * **Reconnect.** When Google no longer accepts the saved connection, calls return `401 platform.google.auth`. See [Reconnect an account](/guides/connect-accounts#reconnect-an-account). ## Comments and Q\&A Google Business Profile has no comments or Q\&A API. Google discontinued the Business Profile Q\&A API on November 3, 2025, with no replacement, and local posts never had comments. Comment, reply, hide, delete, and private-reply calls on a Google account return `501 resource.not_supported`. Customer feedback arrives as [reviews](/platforms/google/reviews). ## Performance metrics `GET /v1/platforms/google/accounts/{account_id}/insights` returns how a location performed day by day: how many people saw it on Google Search and Maps, and how many then called, asked for directions, messaged, booked, ordered, or clicked through to the website. For the metrics of other platforms, see [Analytics](/guides/analytics). ```bash theme={null} curl "https://api.social-api.ai/v1/platforms/google/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/insights?start_date=2026-08-01&end_date=2026-08-31&metric=CALL_CLICKS" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` | Parameter | Description | | - | - | | `start_date`, `end_date` | Required. Inclusive, formatted `YYYY-MM-DD`. A missing or malformed date returns `400 validation.start_date_required`, `validation.start_date_invalid`, `validation.end_date_required`, or `validation.end_date_invalid`. An `end_date` before `start_date` returns `400 validation.range_inverted` | | `metric` | A comma-separated list of the metrics below. The default is every metric. An unknown name returns `400 validation.metric_invalid` | Response (HTTP 200): ```json theme={null} { "data": { "start_date": "2026-08-01", "end_date": "2026-08-31", "metrics": [ { "metric": "CALL_CLICKS", "points": [ { "date": "2026-08-01", "value": 12 }, { "date": "2026-08-02", "value": 0 } ], "total": 12 } ] } } ``` A day with no activity is an explicit `0`, not a gap. Google publishes no retention window and no freshness lag for this data. | Metric | Counts | | - | - | | `BUSINESS_IMPRESSIONS_DESKTOP_MAPS`, `BUSINESS_IMPRESSIONS_DESKTOP_SEARCH`, `BUSINESS_IMPRESSIONS_MOBILE_MAPS`, `BUSINESS_IMPRESSIONS_MOBILE_SEARCH` | Times the listing was shown, by device and by Google Maps or Google Search | | `BUSINESS_CONVERSATIONS` | Conversations started from the listing | | `BUSINESS_DIRECTION_REQUESTS` | Requests for directions | | `CALL_CLICKS` | Clicks on the call button | | `WEBSITE_CLICKS` | Clicks on the website link | | `BUSINESS_BOOKINGS` | Bookings made from the listing | | `BUSINESS_FOOD_ORDERS` | Food orders placed from the listing | | `BUSINESS_FOOD_MENU_CLICKS` | Clicks on the food menu | ## Search keywords `GET /v1/platforms/google/accounts/{account_id}/search-keywords` returns the searches people made before seeing the location, aggregated by month over an inclusive `YYYY-MM` range: ```bash theme={null} curl "https://api.social-api.ai/v1/platforms/google/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/search-keywords?start_month=2026-06&end_month=2026-08" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": { "keywords": [ { "keyword": "pizza near me", "value": 1420, "is_threshold": false }, { "keyword": "late night pizza", "threshold": 15, "is_threshold": true } ], "next_cursor": "CgwIvKeZyQYQwP3DzgE" } } ``` | Parameter | Description | | - | - | | `start_month`, `end_month` | Required. Inclusive, formatted `YYYY-MM`. A missing or malformed month returns `400 validation.start_month_required`, `validation.start_month_invalid`, `validation.end_month_required`, or `validation.end_month_invalid`. An `end_month` before `start_month` returns `400 validation.range_inverted` | | `limit` | Keywords per page, 1 to 100. Omit it or send `0` for the default, 100. A value above 100 returns `400 validation.page_size_invalid` | | `cursor` | The `next_cursor` of the previous page | > [!WARNING] > > A keyword with `is_threshold: true` has `threshold` instead of `value`. Google withholds counts that are too low to disclose and gives an upper bound, so the real number is below `threshold`. Charting a threshold as a count overstates the keyword. ## Webhook events Google Business Profile events arrive as ordinary SocialAPI webhooks. You set nothing up on Google's side: the events start when you connect a location. | Event | When it fires | | - | - | | `review.received` | A customer left a new review. `data.review` holds the whole review | | `review.updated` | A review changed. `data.change` is `review_edited`, `reply_approved`, or `reply_rejected` | | `account.updated` | The location changed on Google. `data.change` is `customer_media_added`, `google_update_pending`, `voice_of_merchant_changed`, `duplicate_location`, or `permissions_reduced` (the account lost [permissions](/guides/permissions) you did not remove yourself) | | `account.connected` | You finished connecting or reconnecting the location | | `account.disconnected` | The connection needs reconnecting | | `post.published`, `post.partial`, `post.failed` | A post to this account finished: see [Delivery](/guides/delivery) | Every `post.*` event in [Webhook events](/guides/webhook-events#posts) is emitted for this platform. The payloads are in [Webhook events](/guides/webhook-events). Google can miss a notification and does not replay it, so a gap is permanent. Read reviews on a schedule to backfill: [Reviews](/platforms/google/reviews#review-events). ## Limits | Limit | Value | | - | - | | Local post text | 1,500 characters | | Local post image | 1 image, JPEG or PNG, up to 5 MB. Video is not accepted | | Location updates | 10 per minute per Business Profile. The 11th returns `429 platform.google.rate_limit` | | Reviews per page | 1 to 50 | | Search keywords per page | 1 to 100. The default is 100 | ## Scopes SocialAPI's Google app requests these scopes at connect. You apply for no Google app review. The consent screen is the same whichever [permissions](/guides/permissions) you select. | Scope | Used for | | - | - | | `openid` | Identifies the Google login behind the connection | | `https://www.googleapis.com/auth/userinfo.profile` | The login's name and picture | | `https://www.googleapis.com/auth/userinfo.email` | The login's email address | | `https://www.googleapis.com/auth/business.manage` | Everything on the Business Profile: local posts, reviews, location details, photos, performance metrics, and search keywords | Google Business Profile asks for 3 identity scopes and 1 scope, `business.manage`, that covers all of its permissions, so the consent screen shows the same scopes whatever you select. The account is still limited to the permissions you selected: an account connected with `reviews.read` alone cannot reply to reviews. Editing the location's details and photos does not depend on the account's permissions. ## Related * [Google Business Profile publishing](/platforms/google/publishing): fields, media rules, and quirks for local posts * [Locations](/platforms/google/locations): read and update locations, and manage profile photos and videos * [Reviews](/platforms/google/reviews): list and reply to reviews, and the review events * [Connect accounts](/guides/connect-accounts#choose-a-google-business-profile): choose a location when a login manages more than 1 * [Platforms](/platforms/overview): the capability matrix * [Errors](/guides/errors): every error code # Google Business Profile locations Source: https://docs.social-api.ai/platforms/google/locations Read, assign, and update Google Business Profile locations, and manage the photos and videos on a location's public profile. A Google Business Profile location is one business listing on Google Search and Maps. In SocialAPI, each connected `google` account is one location. These endpoints list the locations a Google login manages, connect another one to a brand, update a location's public profile, and manage the photos and videos on it. ## Endpoints | Method | Path | Description | | - | - | - | | `GET` | `/v1/platforms/google/logins/{loginId}/locations` | List every location a Google login manages, with its details and assignment | | `POST` | `/v1/platforms/google/logins/{loginId}/locations/{locationId}/assign` | Connect a location from the login to a brand | | `PUT` | `/v1/platforms/google/accounts/{id}` | Update a location's public profile | | `GET` | `/v1/platforms/google/accounts/{id}/media` | List the photos and videos on a location's profile | | `GET` | `/v1/platforms/google/accounts/{id}/media/{mediaId}` | Read one photo or video | | `POST` | `/v1/platforms/google/accounts/{id}/media` | Add one photo or video to the profile | | `DELETE` | `/v1/platforms/google/accounts/{id}/media/{mediaId}` | Remove a photo or video you added | `{loginId}` is the `login_id` returned when you connect Google and on every Google account: see [Choose a Google Business Profile](/guides/connect-accounts#choose-a-google-business-profile). `{id}` is the connected account ID (`acc_...`). The full schemas are in the [API reference](/api-reference/google/list-a-google-logins-business-profile-locations). ## Read a location's details `GET /v1/platforms/google/logins/{loginId}/locations` returns the locations the Google login can manage, read from Google at request time. A location added in Google Business Profile after the login was authorized appears at once, with no new consent screen. ```bash theme={null} curl https://api.social-api.ai/v1/platforms/google/logins/cred_01J8X3Q4R5M6N7P8V2K0W1JA/locations \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "login_id": "cred_01J8X3Q4R5M6N7P8V2K0W1JA", "login_name": "Joe Rossi", "login_email": "joe@example.com", "count": 1, "locations": [ { "platform_account_id": "accounts/123/locations/456", "name": "Joe's Pizza (Downtown)", "bio": "Wood-fired pizza since 1998", "category": "Pizza restaurant", "maps_uri": "https://maps.google.com/?cid=123", "assignable": false, "assigned_brand_id": "7d3b2c1a-5e4f-4a6b-9c8d-0e1f2a3b4c5d", "assigned_brand_name": "Joe's Pizza", "assigned_account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "details": { "place_id": "ChIJabc", "new_review_uri": "https://g.page/r/abc/review", "website_uri": "https://joes.example", "primary_phone": "+33 1 23 45 67 89", "storefront_address": { "region_code": "FR", "postal_code": "75001", "locality": "Paris", "address_lines": ["12 rue de Rivoli"] }, "primary_category": { "name": "categories/gcid:pizza_restaurant", "display_name": "Pizza restaurant" }, "latlng": { "latitude": 48.8566, "longitude": 2.3522 }, "regular_hours": [ { "open_day": "MONDAY", "open_time": "09:00", "close_day": "MONDAY", "close_time": "17:30" } ], "open_status": "OPEN", "language_code": "en", "has_voice_of_merchant": true, "can_operate_local_post": true } } ] } ``` | Field | Description | | - | - | | `login_id`, `login_name`, `login_email` | The Google login. `login_name` and `login_email` are absent for a login authorized before SocialAPI stored them | | `count` | The number of locations in `locations` | | `locations[].platform_account_id` | Google's resource name for the location, for example `accounts/123/locations/456` | | `locations[].name`, `bio`, `category`, `maps_uri` | The location's name, description, primary category, and Google Maps link | | `locations[].assignable` | `true` when no brand holds the location. `false` when a brand holds it: `assigned_brand_id`, `assigned_brand_name`, and `assigned_account_id` then name it | | `locations[].details` | Everything else Google publishes about the location, in the table below | `details` carries these fields. Each one is absent when Google returns no value for it, so treat every field as optional. A service-area business (a locksmith, a plumber) has no `storefront_address` and carries `service_area` instead. | Field | Description | | - | - | | `place_id` | The Google Places identifier. Use it to join a location to Places, Maps, and Geocoding results | | `new_review_uri` | The short link that opens Google's review form for the location | | `website_uri`, `primary_phone`, `additional_phones`, `ad_phone` | Website and phone numbers. `ad_phone` is the number shown on Google Ads extensions | | `storefront_address` | `region_code`, `postal_code`, `administrative_area`, `locality`, `sublocality`, `sorting_code`, `organization`, and `address_lines` | | `primary_category`, `additional_categories` | Each has `name` (`categories/gcid:...`) and `display_name` | | `latlng` | `latitude` and `longitude` | | `regular_hours`, `special_hours`, `more_hours` | Weekly hours, holiday and exception hours, and hours for a department such as a drive-through | | `open_status`, `opening_date` | `OPEN`, `CLOSED_PERMANENTLY`, or `CLOSED_TEMPORARILY`, and the opening date | | `service_area`, `service_items`, `relationship_data` | Where a business without a storefront operates, the services it offers with prices, and its chain and parent or child locations | | `store_code`, `labels`, `language_code` | Your own code and labels for the location, and its language | | `has_voice_of_merchant` | `true` when the merchant controls the profile. `false` when the merchant does not: publishing and replying fail until the merchant resolves it in Google Business Profile. Absent when Google reported nothing | | `has_pending_edits`, `has_google_updated` | Edits awaiting Google's review, and updates Google suggested | | `can_operate_local_post`, `can_have_food_menus`, and the other `can_*` flags | Which Google surfaces the location supports. `can_operate_local_post` says whether the location supports local posts | | `duplicate_location` | Present when Google flagged the location as a duplicate | A boolean flag that is absent means Google told SocialAPI nothing. It does not mean `false`: check that the key is present before treating a flag as a denial. ## Connect another location to a brand `POST /v1/platforms/google/logins/{loginId}/locations/{locationId}/assign` connects one location from the login to a brand, without a second consent screen. `{locationId}` is the `platform_account_id`, with the slashes either sent as they are or URL-encoded as `%2F`. ```bash theme={null} curl -X POST "https://api.social-api.ai/v1/platforms/google/logins/cred_01J8X3Q4R5M6N7P8V2K0W1JA/locations/accounts%2F123%2Flocations%2F789/assign" \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{"brand_id": "9a1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d"}' ``` Response (HTTP 201): ```json theme={null} { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1K", "brand_id": "9a1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d", "platform_account_id": "accounts/123/locations/789" } ``` The rules: * **One brand per location.** A location belongs to exactly 1 brand. Assigning a location that another brand holds returns `400 validation.location_already_assigned`. * **One Google Business Profile per brand.** A brand holds at most 1 location. Assigning a second location to the same brand returns `409 platform.google.brand_has_account`. Disconnect the first account, or assign the location to another brand. * **The login must still grant the location.** A location the login no longer manages returns `400 validation.location_not_granted`. * **The brand must exist.** An unknown brand returns `404 brand.not_found`. A missing `brand_id` returns `400 validation.field_required`. * **Cost.** Assigning a location costs no credits. When the Google login manages more than 1 location, the connect flow pauses so you choose one. The other locations stay available on the same login: assign each to another brand with this endpoint. ## Update a location `PUT /v1/platforms/google/accounts/{id}` writes the location's public profile. Send only the fields you want to change: ```bash theme={null} curl -X PUT https://api.social-api.ai/v1/platforms/google/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "website_uri": "https://joes-pizza.example", "phone_numbers": { "primary_phone": "+33 1 23 45 67 89" }, "regular_hours": { "periods": [ { "open_day": "MONDAY", "open_time": "09:00", "close_day": "MONDAY", "close_time": "17:30" } ] } }' ``` Response (HTTP 200): the location as Google now holds it, in `data`, in the shape of `details` above: ```json theme={null} { "data": { "website_uri": "https://joes-pizza.example", "primary_phone": "+33 1 23 45 67 89", "regular_hours": [ { "open_day": "MONDAY", "open_time": "09:00", "close_day": "MONDAY", "close_time": "17:30" } ], "has_pending_edits": true } } ``` The writable fields: | Field | Description | | - | - | | `title`, `description`, `store_code`, `website_uri`, `ad_phone` | Strings | | `labels` | Array of strings. Replaces the whole list | | `phone_numbers` | `primary_phone` (string) and `additional_phones` (array, replaces the whole list) | | `storefront_address` | Any of `region_code`, `postal_code`, `administrative_area`, `locality`, `sublocality`, `sorting_code`, `organization`, `address_lines` | | `latlng` | `latitude` and `longitude` | | `categories` | `primary_category` and `additional_categories` (array, replaces the whole list), as `categories/gcid:...` names | | `open_status` | `OPEN`, `CLOSED_PERMANENTLY`, or `CLOSED_TEMPORARILY` | | `regular_hours`, `special_hours` | `{ "periods": [...] }`. Replaces all periods. Google rejects `special_hours` on a location with no `regular_hours` | | `more_hours` | Array of `{ "hours_type_id": "DRIVE_THROUGH", "periods": [...] }`. Replaces the whole list | | `service_area` | `business_type` (`CUSTOMER_LOCATION_ONLY` or `CUSTOMER_AND_BUSINESS_LOCATION`) and `place_ids` (array) | > [!WARNING] > > A field you omit is left alone. A field you send with an empty value is cleared: `"website_uri": ""` removes the website. Send only the fields you mean to change, and never send back a location you just read. Fields the read omitted would arrive empty and erase what is there. Lists replace instead of merging: `regular_hours.periods`, `labels`, `additional_phones`, and `additional_categories` each overwrite the whole list, so include every entry you want to keep. Objects merge: inside `storefront_address` you can send only `postal_code` and the rest of the address stays. Not writable: `place_id`, `maps_uri`, `new_review_uri`, and the status flags are output only. `language_code` is fixed when the location is created. A request with none of the writable fields returns `400 validation.no_fields_to_update`. A value Google rejects returns `502 platform.google.api_error`, with Google's reason in `error.message`. ### Validate before you write Google has no sandbox, so every write changes a real Business Profile. Add `validate_only=true` to have Google check the change and apply nothing: ```bash theme={null} curl -X PUT "https://api.social-api.ai/v1/platforms/google/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J?validate_only=true" \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Joe'"'"'s Pizza (Downtown)" }' ``` Response (HTTP 200): ```json theme={null} { "data": {}, "validated": true } ``` ### Edits are moderated A `200` means Google accepted the change for review. It does not mean the change is live on Search and Maps. Read the location back to see what is live, and check `details.has_pending_edits` for edits still under review. ## Profile media A location's profile carries its own photos and videos: the logo, the cover image, interior and exterior shots. They are separate from the media attached to a post, and they are what customers see on the listing. `GET /v1/platforms/google/accounts/{id}/media` lists them: ```bash theme={null} curl "https://api.social-api.ai/v1/platforms/google/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/media" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "items": [ { "id": "accounts/123/locations/456/media/789", "media": { "url": "https://lh3.googleusercontent.com/p/abc", "type": "image" }, "category": "INTERIOR", "description": "Main dining room", "width_pixels": 1600, "height_pixels": 1200, "created_at": "2026-03-01T12:00:00Z" } ], "count": 1, "total_count": 42, "next_cursor": "CiAKGjB4MTIzNDU2Nzg" } ``` | Field | Description | | - | - | | `items[].id` | Google's resource name for the item | | `items[].media` | `url` and `type`, the same object review photos and post attachments use | | `items[].category` | Where the item sits on the profile | | `items[].thumbnail_url`, `description`, `width_pixels`, `height_pixels`, `created_at` | Present when Google reports them | | `count` | The items on this page | | `total_count` | The items on the location across all pages. 0 for an empty gallery | | `next_cursor` | Pass it as `cursor` for the next page. Absent on the last page. `limit` sets the page size | `GET /v1/platforms/google/accounts/{id}/media/{mediaId}` returns one item. `mediaId` accepts the bare ID or the full resource name from the list. ### Add a photo or video `POST /v1/platforms/google/accounts/{id}/media` adds one item. `media` takes exactly 1 entry, in the same `{source_type, source}` form the publishing endpoints accept: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/platforms/google/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/media \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "media": [{ "source_type": "media_id", "source": "550e8400-e29b-41d4-a716-446655440000" }], "category": "INTERIOR", "description": "Main dining room" }' ``` Response (HTTP 201): the new item, in the shape of a list item. | `source_type` | Use it for | | - | - | | `media_id` | A file in your [media library](/guides/media). SocialAPI reads the photo or video format from the file | | `url` | A public URL Google can reach | | `platform_attachment_id` | Bytes you uploaded to Google yourself. `source` is the resource name Google returned. Set `type` as well, because there is no file to read it from | `category` is one of `COVER`, `PROFILE`, `LOGO`, `EXTERIOR`, `INTERIOR`, `PRODUCT`, `AT_WORK`, `FOOD_AND_DRINK`, `MENU`, `COMMON_AREA`, `ROOMS`, `TEAMS`, or `ADDITIONAL`. `PROFILE` sets the avatar shown for the location. `LOGO` is the fallback when no `PROFILE` photo exists. A request with other than 1 media entry returns `422 google.media.single_item_required`. An unknown `category` returns `422 google.media.invalid_category`. A media entry that cannot be resolved returns `422 google.media.unresolvable`. ### Remove a photo or video `DELETE /v1/platforms/google/accounts/{id}/media/{mediaId}` removes an item you added and returns `204` with no body. `mediaId` accepts the bare ID or the full resource name. ### Photos your customers posted Customers add their own photos to a listing. They live in a separate read-only gallery. Read it with `contributed=true`: ```bash theme={null} curl "https://api.social-api.ai/v1/platforms/google/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/media?contributed=true" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Each item carries an `attribution` block with `profile_name`, `profile_photo_url`, `profile_url`, and `takedown_url`. Google requires you to display the attribution exactly as given, without changing or removing it. A delete on a customer photo returns `403 platform.google.access_denied`. Report the photo to Google through its `takedown_url` instead. ## Limits | Limit | Value | When it is hit | | - | - | - | | Location updates | 10 per minute per Business Profile | Google cannot raise it. The 11th location update in a minute returns `429 platform.google.rate_limit`. Wait, then retry | ## Scopes | Operation | Scope | | - | - | | List a login's locations, list and read media | `accounts:read` | | Assign a location, update a location, add or delete media | `accounts:manage` | ## Related * [Google Business Profile API](/platforms/google): capabilities, connect rules, and limits for the platform * [Reviews](/platforms/google/reviews): list and reply to the reviews on a location * [Google Business Profile publishing](/platforms/google/publishing): publish local posts to a location * [Connect accounts](/guides/connect-accounts#choose-a-google-business-profile): choose a location when a login manages more than 1 * [Errors](/guides/errors): every error code # Google Business Profile publishing Source: https://docs.social-api.ai/platforms/google/publishing Publish, schedule, and delete Google Business Profile local posts (updates, events, offers, alerts) with text, one image, and a call-to-action button. ## 1. Overview SocialAPI publishes local posts to one connected Google Business Profile location: updates, events, offers, and alerts. A local post appears on the location's Business Profile in Google Search and Google Maps. Each connected `google` account is one location, so a post with one Google target publishes on one location. For everything outside publishing, see the [Google Business Profile API](/platforms/google) page. | Feature | Supported | Notes | | - | - | - | | Create post | Yes | Text, plus at most 1 JPEG or PNG image | | Update post | Partial | Before publishing only: `PATCH /v1/posts/{pid}` edits a `draft`, `scheduled`, or `failed` post. A published local post cannot be edited: delete it and publish a new one | | Delete post | Yes | Removes the local post from Google | | Schedule | Yes | Google has no scheduling of its own. SocialAPI holds the post and creates it on Google at `scheduled_at` | | First comment | No | Google Business Profile has no comments. SocialAPI ignores `first_comment` on Google targets | | Multiple images, video | No | Pre-publish validation rejects them with `400 publishing.validation_failed` | | Posting caps | Yes | 25 posts per hour and 50 per 24 hours for each account: see [Posting velocity caps](/guides/rate-limits#posting-velocity-caps) | *** ## 2. Supported media | Content type | How to trigger | Notes | | - | - | - | | Text only | No `media` | The post shows only its text | | Single image | One image in `media`, as a `media_id` or `url` source | JPEG or PNG, up to 5 MB | * **Text**: `text` holds up to 1,500 characters. A longer `text` fails pre-publish validation with `400 publishing.validation_failed`. * **Images**: 1 image per post, JPEG or PNG, up to 5 MB. A second media item fails validation with `400 publishing.validation_failed` ("carousel/album posts are not supported on google"). * **Video**: not accepted. A video item fails validation with `400 publishing.validation_failed`. * **Media sources**: Google fetches the image from a URL, so use a `media_id` (a file in your [media library](/guides/media)) or a public `url`. A `platform_attachment_id` item returns `422 publishing.media.unsupported_source`. *** ## 3. Create post Send `POST /v1/posts` with a Google account in `targets` and `"publish_now": true`. Without `publish_now` or `scheduled_at`, SocialAPI saves the post as a draft. * `text` becomes the body of the local post. It is required: a request without it returns `400 validation.field_required`. * `title` is not used by Google Business Profile posts. Put an event's name in `platform_data.google.event.title`. * `platform_data.google` (or the target's own `platform_data`) carries the post type and its extra fields. A key in a target's `platform_data` wins over the same key in `platform_data.google`. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Our spring menu is here: wild garlic pizza, burrata, and lemon tart.", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "platform_data": { "google": { "topicType": "STANDARD", "callToAction": { "actionType": "ORDER", "url": "https://joes-pizza.example/order" } } } }' ``` Response (HTTP 201): ```json theme={null} { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "text": "Our spring menu is here: wild garlic pizza, burrata, and lemon tart.", "status": "publishing", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "media_ids": ["f47ac10b-58cc-4372-a567-0e02b2c3d479"], "hidden": false, "retry_count": 0, "created_at": "2026-10-13T09:00:00Z", "updated_at": "2026-10-13T09:00:00Z", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "google", "status": "pending", "text": "Our spring menu is here: wild garlic pizza, burrata, and lemon tart.", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "media_ids": ["f47ac10b-58cc-4372-a567-0e02b2c3d479"] } ] } ``` SocialAPI publishes in the background. The target moves from `pending` to `publishing`, then to `published` or `failed`, and the post ends as `published`, `partial`, or `failed`. Poll `GET /v1/posts/{pid}` or subscribe to the `post.published`, `post.partial`, and `post.failed` [webhook events](/guides/webhook-events). Publishing or scheduling costs 1 post credit; a draft is free (see [Plans and credits](/guides/plans)). ### Platform data fields Pass these in `platform_data.google` or in a target's `platform_data`: | Field | Type | Default | Description | | - | - | - | - | | `topicType` | string | `STANDARD` | Post type: `STANDARD` (an update), `EVENT`, `OFFER`, or `ALERT`. Pre-publish validation rejects any other value | | `callToAction` | object | none | A button on the post: `{ "actionType": "...", "url": "..." }`. Every post type accepts it | | `callToAction.actionType` | string | none | Required in `callToAction`. One of `BOOK`, `ORDER`, `SHOP`, `LEARN_MORE`, `SIGN_UP`, `CALL`. `GET_OFFER` is rejected: use `topicType: "OFFER"` instead | | `callToAction.url` | string | none | The page the button opens. `CALL` takes no `url`: the button dials the phone number listed on the location | | `event` | object | none | Required by Google when `topicType` is `EVENT`. Holds `title` and `schedule` (`startDate`, `endDate`, `startTime`, `endTime`). Dates are `{ "year": 2026, "month": 11, "day": 7 }` objects | | `offer` | object | none | Required by Google when `topicType` is `OFFER`. Its fields `couponCode`, `redeemOnlineUrl`, and `termsConditions` are optional | | `alertType` | string | none | Required by Google when `topicType` is `ALERT`. Google's only value is `COVID_19` | | `languageCode` | string | none | Language of the post text, as a BCP-47 code such as `en` or `fr` | SocialAPI validates `topicType` and `callToAction`: an invalid value fails pre-publish validation with `400 publishing.validation_failed`. When you set `skip_validation`, the target fails instead with `validation.invalid_call_to_action`, and `error.message` gives the reason. SocialAPI forwards any additional keys in `platform_data.google` that name one of Google's writable local post fields (`event`, `offer`, `alertType`, `languageCode`) to Google without validating them, so Google's own rules apply to their content. SocialAPI drops every other key, including keys meant for other platforms in the same post. See Google's [LocalPost reference](https://developers.google.com/my-business/reference/rest/v4/accounts.locations.localPosts) for the full shape of `event` and `offer`. ### Event post example ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Ten years of wood-fired pizza. Free tasting all evening.", "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "platform_data": { "google": { "topicType": "EVENT", "event": { "title": "10th anniversary tasting", "schedule": { "startDate": { "year": 2026, "month": 11, "day": 7 }, "endDate": { "year": 2026, "month": 11, "day": 7 } } }, "callToAction": { "actionType": "BOOK", "url": "https://joes-pizza.example/book" } } } }' ``` *** ## 4. Update post A local post cannot be edited after it is published. To change a published post, delete it (section 5) and publish a new one. Before publishing, `PATCH /v1/posts/{pid}` edits a post in `draft`, `scheduled`, or `failed` status. The change applies when the post publishes: ```bash theme={null} curl -X PATCH https://api.social-api.ai/v1/posts/7c9e6679-7425-40de-944b-e07fc1f90ae7 \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Our spring menu is here: wild garlic pizza, burrata, and lemon tart. Order before 9 pm." }' ``` The response (HTTP 200) is the updated post, in the same shape as the create response. A `PATCH` on a published post returns `409 post.state_invalid` ("Only draft, scheduled, or failed posts can be edited"). See [Scheduling](/guides/scheduling) for rescheduling and the full list of editable fields. *** ## 5. Delete post `DELETE /v1/posts/{pid}` deletes the local post from Google for every published target, and removes a draft, scheduled, or failed post from SocialAPI: ```bash theme={null} curl -X DELETE https://api.social-api.ai/v1/posts/7c9e6679-7425-40de-944b-e07fc1f90ae7 \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "success": true, "deleted": true, "results": [ { "platform": "google", "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "outcome": "deleted" } ] } ``` If Google refuses the delete, or the local post no longer exists on Google, the target's `outcome` is `failed` with Google's reason in `message`, and SocialAPI keeps the post (`"deleted": false`). To remove the post from one location of a multi-target post, send `POST /v1/posts/{pid}/unpublish` with that target's `account_id`: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts/7c9e6679-7425-40de-944b-e07fc1f90ae7/unpublish \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }' ``` *** ## 6. Retrieve posts `GET /v1/posts` lists posts, filtered by platform or account: ```bash theme={null} curl "https://api.social-api.ai/v1/posts?platform=google&account_ids=acc_01HZ9X3Q4R5M6N7P8V2K0W1J&limit=20" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "text": "Our spring menu is here: wild garlic pizza, burrata, and lemon tart.", "status": "published", "hidden": false, "retry_count": 0, "created_at": "2026-10-13T09:00:00Z", "updated_at": "2026-10-13T09:00:06Z", "published_at": "2026-10-13T09:00:06Z", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "google", "status": "published", "text": "Our spring menu is here: wild garlic pizza, burrata, and lemon tart.", "platform_post_id": "accounts/123456789/locations/987654321/localPosts/4567", "permalink": "https://local.google.com/place?id=123456789&use=posts&lpsid=4567", "published_at": "2026-10-13T09:00:06Z" } ] } ], "pagination": { "limit": 20, "has_more": false } } ``` * `platform_post_id` is Google's full resource name for the local post. Treat it as an opaque string. * `permalink` is the link Google returns to view the post. * Google reports no engagement counts for local posts. A Google target has no `metrics` object, or one whose `likes`, `comments`, `shares`, and `saves` are all `0`. Location-level performance (views, calls, direction requests, website clicks) is at [Performance metrics](/platforms/google#performance-metrics). *** ## 7. Quirks, errors, and recovery ### Scheduling is held by SocialAPI Google publishes a local post the moment it is created and has no scheduling of its own. With `scheduled_at`, SocialAPI keeps the post as `scheduled` and creates it on Google at that time. Until then the post exists only in SocialAPI, and you can still edit or cancel it. ### A published post cannot be edited `PATCH /v1/posts/{pid}` works only before publishing. A published local post keeps its text, image, and button until you delete it. ### The location must allow posting A location whose merchant does not control the profile (`details.has_voice_of_merchant` is `false`) cannot publish. `details.can_operate_local_post` reports whether the location supports local posts at all. Both flags are described on [Google Business Profile locations](/platforms/google/locations#read-a-locations-details). ### Posting caps SocialAPI refuses a publish that would pass 25 posts in an hour or 50 posts in 24 hours for one account, with `429 publishing.velocity_limit`. A scheduled post is held and published when the window clears. See [Posting velocity caps](/guides/rate-limits#posting-velocity-caps). ### Errors on a failed target A failed target carries an `error` object: ```json theme={null} { "code": "platform.google.api_error", "message": "Request contains an invalid argument.", "category": "platform", "caused_by": "platform" } ``` * `platform.google.api_error`: Google rejected the post. `message` carries Google's reason, for example an `EVENT` post with no `event.title`, an `ALERT` post with no `alertType`, or an image Google could not fetch. Fix the request and retry. * `platform.google.access_denied`: Google refused the operation for this location, for example because posting is disabled on the profile or the connected Google account has no role on the location. `message` carries Google's reason. Reconnecting does not fix it: resolve it in Google Business Profile. * `platform.google.auth`: Google no longer accepts the connection. [Reconnect the account](/guides/connect-accounts). * `platform.google.rate_limit`: Google's own limit was reached. Wait, then retry the post. ### Recovery * Retry a `failed` or `partial` post with `POST /v1/posts/{pid}/retry`. A retry costs 1 post credit. * To change a published post, delete it and publish a new one. All codes, with their HTTP status, are listed in [Errors](/guides/errors). *** ## 8. Full worked example This example uploads an image, publishes an offer post with it, and checks the result. **Step 1: Upload the image** ```bash theme={null} curl -X POST https://api.social-api.ai/v1/media/upload \ -H "Authorization: Bearer $SOCAPI_KEY" \ -F "file=@spring-offer.jpg" ``` Response (HTTP 201): ```json theme={null} { "media_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479" } ``` **Step 2: Publish the offer post** ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "20% off every pizza this week with code SPRING20.", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "platform_data": { "google": { "topicType": "OFFER", "offer": { "couponCode": "SPRING20", "redeemOnlineUrl": "https://joes-pizza.example/order", "termsConditions": "Valid until October 20, 2026, on orders placed online." } } } }' ``` The response (HTTP 201) has `"status": "publishing"` and the post `id`, as in section 3. **Step 3: Check the result** ```bash theme={null} curl https://api.social-api.ai/v1/posts/7c9e6679-7425-40de-944b-e07fc1f90ae7 \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200) once Google has the post: ```json theme={null} { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "text": "20% off every pizza this week with code SPRING20.", "status": "published", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "media_ids": ["f47ac10b-58cc-4372-a567-0e02b2c3d479"], "hidden": false, "retry_count": 0, "created_at": "2026-10-13T09:00:00Z", "updated_at": "2026-10-13T09:00:07Z", "published_at": "2026-10-13T09:00:07Z", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "google", "status": "published", "text": "20% off every pizza this week with code SPRING20.", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "media_ids": ["f47ac10b-58cc-4372-a567-0e02b2c3d479"], "platform_post_id": "accounts/123456789/locations/987654321/localPosts/4567", "permalink": "https://local.google.com/place?id=123456789&use=posts&lpsid=4567", "published_at": "2026-10-13T09:00:07Z" } ] } ``` If the target is `failed`, read `targets[].error` (section 7), fix the request, and call `POST /v1/posts/{pid}/retry`. *** ## 9. Required OAuth scopes Publishing uses 1 of the scopes SocialAPI's Google app requests when an account connects. You register no Google app and configure nothing. | Scope | Used for | | - | - | | `https://www.googleapis.com/auth/business.manage` | Create and delete local posts on the location | The scopes that identify the Google login are listed in [Scopes](/platforms/google#scopes). The scopes a scoped API key needs, the credit cost of a post, and the MCP tools that publish are on [Publishing](/guides/publishing). # Google Business Profile reviews Source: https://docs.social-api.ai/platforms/google/reviews List the Google reviews on a Business Profile location, reply to them, and handle Google's moderation of replies and the review webhook events. SocialAPI lists the reviews on a connected Google Business Profile location, reports its rating summary, and posts the business's public reply. App Store Connect reviews are on [App Store Connect](/platforms/app-store-connect#reviews). Google moderates every reply: a reply is accepted first and goes live only after Google approves it. For the platform-independent review workflow, see [Reviews](/guides/reviews). ## Endpoints | Method | Path | Description | | - | - | - | | `GET` | `/v1/inbox/reviews` | List your review-capable accounts, each with its rating summary | | `GET` | `/v1/inbox/reviews/{account_id}` | List the reviews of one account, with the account's summary | | `POST` | `/v1/inbox/reviews/{id}/reply` | Post the business's public reply to a review | Reviews cannot be created or deleted through the API, and SocialAPI has no endpoint to edit or delete a reply. To change a reply, send a new one: a review has one business reply, and a new reply replaces it. ## The two list endpoints The two `GET` endpoints answer different questions: * **`GET /v1/inbox/reviews`** returns one row per review-capable account: the account, its average rating, its review count, and the time of its newest review. Use it to build a list of locations. It returns no reviews. Add `platform=google` to filter. * **`GET /v1/inbox/reviews/{account_id}`** returns the reviews of the 1 account you name. Use it to read, sort, and page through them. ```bash theme={null} curl https://api.social-api.ai/v1/inbox/reviews \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "name": "Joe's Pizza (Downtown)", "platform": "google", "average_rating": 4.2, "total_count": 87, "last_review_at": "2026-06-22T02:24:05Z", "status": "ok" } ] } ``` `status` is `ok`, `reconnection_required` (the account needs [reconnecting](/guides/connect-accounts#reconnect-an-account)), or `error` (Google did not answer in time). `average_rating`, `total_count`, and `last_review_at` are `null` unless `status` is `ok`. A failing account never fails the whole list. ## List the reviews of a location ```bash theme={null} curl "https://api.social-api.ai/v1/inbox/reviews/acc_01HZ9X3Q4R5M6N7P8V2K0W1J?limit=20&order=newest" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` | Parameter | Description | | - | - | | `limit` | 1 to 50 reviews per page. The default is 20. A value above 50 is treated as 50. A value below 1 returns `400 validation.field_invalid` | | `cursor` | The `next_cursor` of the previous page | | `order` | `newest` (default), `rating_asc`, or `rating_desc`. Another value returns `400 validation.field_invalid` | Response (HTTP 200): ```json theme={null} { "data": [ { "id": "sapi_rev_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "rating": 3, "text": "Great pizza, slow service.", "author": { "name": "Jane Smith", "avatar_url": "https://lh3.googleusercontent.com/a/abc" }, "media": [ { "thumbnail_label": "Storefront", "thumbnail_url": "https://lh3.googleusercontent.com/p/abc" } ], "reply": { "state": "APPROVED", "text": "Thanks for the feedback!", "updated_at": "2026-06-21T10:00:00Z" }, "reply_url": "https://business.google.com/reviews/reply/abc", "created_at": "2026-06-20T18:24:31Z", "updated_at": "2026-06-20T18:24:31Z" } ], "summary": { "average_rating": 4.2, "total_count": 87 }, "pagination": { "has_more": true, "next_cursor": "CgwI9ZyPyQYQ6IiYuQE" } } ``` | Field | Description | | - | - | | `id` | The review's SocialAPI ID (`sapi_rev_...`). Use it in the reply path | | `rating` | 1 to 5. 0 when Google gave no rating | | `text` | The review text. Empty for a rating-only review | | `author` | `name` and `avatar_url`. Both are empty for an anonymous reviewer | | `media` | Photos or videos the reviewer attached. Each has `thumbnail_url`, and optionally `thumbnail_label` and `video_url` (videos only). Absent when there are none | | `reply` | The business's reply: `text`, `updated_at`, `state`, and `policy_violation` on a rejected reply. `null` when there is no reply | | `reply_url` | A Google-hosted page for replying to the review. Absent when empty | | `created_at`, `updated_at` | When the reviewer posted the review and last changed it | | `summary` | The average rating and total review count of the whole location, not of this page | | `pagination` | `has_more`, and `next_cursor` when `has_more` is `true` | Google serves reviews for verified locations only. `GET /v1/inbox/reviews/{account_id}` on an account whose platform has no reviews returns `422 account.platform_unsupported`. ## Reply to a review ```bash theme={null} curl -X POST https://api.social-api.ai/v1/inbox/reviews/sapi_rev_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I/reply \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "text": "Thank you for your kind review, Jane!" }' ``` Response (HTTP 200): ```json theme={null} { "success": true, "reply_id": "sapi_rev_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "state": "PENDING" } ``` | Field | Description | | - | - | | `account_id` | Required. The connected account that owns the review | | `text` | Required. The reply. Google accepts up to 4,096 bytes. The MCP tool `social_api_reply_to_review` accepts up to 2,200 bytes | | `reply_id` | The review's SocialAPI ID, which is also the ID of its reply | | `state` | The reply's moderation state, in the next section | A reply costs 1 interaction credit. When the allowance is used up, the call returns `429 validation.interaction_limit_exhausted`. A missing `account_id` or `text` returns `400 validation.field_required`. A review ID that is not a review ID returns `400 validation.id_invalid`, and one from another platform returns `400 validation.id_platform_mismatch`. An account that needs reconnecting returns `403 account.reconnection_required`. ### Replies are moderated > [!WARNING] > > **A successful reply is not yet published.** `success: true` means Google accepted the reply for review. Nobody can see it until its `state` is `APPROVED`. | `state` | Meaning | | - | - | | `PENDING` | Submitted and awaiting Google's review. A new reply returns this | | `APPROVED` | Published and visible on the listing | | `REJECTED` | Not published, and never will be. `policy_violation` holds Google's reason | SocialAPI sends a `review.updated` webhook event when Google decides, with `change` set to `reply_approved` or `reply_rejected`. You can also read the review again and look at `reply.state`. A rejected reply carries `reply.policy_violation`, passed through as Google sends it: ```json theme={null} { "reply": { "policy_violation": { "reason": "PROMOTIONAL_CONTENT" }, "state": "REJECTED", "text": "Thanks! Use code SAVE10 for 10% off.", "updated_at": "2026-07-03T10:00:00Z" } } ``` Google rejects promotional language, discount codes, and links to sales pages. Send a new reply without them. A review read right after a reply can still show the old reply. ## Review events Google Business Profile sends review events to SocialAPI as they happen, and SocialAPI delivers them to your [webhook endpoints](/guides/webhooks) like every other event. | `change` | Event | Meaning | | - | - | - | | `new_review` | `review.received` | Someone left a new review. `data.review` holds the whole review | | `review_edited` | `review.updated` | The reviewer edited the review | | `reply_approved` | `review.updated` | A reply you posted passed moderation and is live | | `reply_rejected` | `review.updated` | Google rejected a reply you posted | The payloads are in [Webhook events](/guides/webhook-events#reviews). Google can miss a notification and does not replay it, so a gap is permanent. Read `GET /v1/inbox/reviews/{account_id}` on a schedule to backfill. ## Scopes | Operation | Scope | | - | - | | List accounts and list reviews | `inbox:read` | | Reply to a review | `inbox:write` | ## Use from an AI agent | Tool | What it does | | - | - | | `social_api_list_review_accounts` | Lists review-capable accounts with their rating summaries | | `social_api_get_reviews` | Lists the reviews of 1 account | | `social_api_reply_to_review` | Replies to a review | ## Related * [Reviews](/guides/reviews): the platform-independent review workflow * [Webhook events](/guides/webhook-events#reviews): the `review.received` and `review.updated` payloads * [Google Business Profile API](/platforms/google): capabilities, connect rules, and limits for the platform * [Google Business Profile locations](/platforms/google/locations): read and update the location behind the reviews * [Plans and usage](/guides/plans#what-counts-as-a-post-or-interaction): what costs an interaction credit # Instagram API Source: https://docs.social-api.ai/platforms/instagram Connect Instagram Business and Creator accounts to publish, moderate comments, send DMs, and read mentions, stories, and insights through one API. The Instagram API in SocialAPI connects an Instagram Business or Creator account through Instagram Login by default, or through Facebook Login when the account has a linked Facebook Page: see [Connect through Facebook Login](#connect-through-facebook-login). It publishes posts, Reels, carousels, and Stories, reads and moderates comments, sends private replies and direct messages (DMs), and reads mentions, stories, and insights. SocialAPI's own Meta app connects the account, so you register nothing: see [Platform apps](/guides/platform-credentials). ## At a glance | Field | Value | | - | - | | Platform slug | `instagram` | | Auth | OAuth 2.0 with Instagram Login (default): the account needs no linked Facebook Page. Or OAuth 2.0 with Facebook Login, selected with `metadata.connection_type: "facebook_login"`: the account needs a linked Facebook Page | | Platform API | Instagram API with Instagram Login, or Instagram API with Facebook Login for Business (Meta) | | Status | Available | ## Capabilities An unsupported call returns `501 resource.not_supported`. Read the `capabilities` object on each comment instead of assuming (`can_like`, `can_private_reply`). | Feature | Status | Condition | | - | - | - | | Publish feed image, Reel, carousel, Story | Yes | See [Instagram publishing](/platforms/instagram/publishing) | | Schedule a post | Yes | `scheduled_at` on the post | | First comment on a post | Yes | Not part of a Story | | Update a published post | No | `PATCH /v1/posts/{pid}` returns `409 post.state_invalid` for a published post. A `draft`, `scheduled`, or `failed` post can be edited before it publishes. To correct it, delete the post in the Instagram app and publish a new one | | Delete a published post | Partial | Instagram Login: `DELETE /v1/posts/{pid}` returns HTTP 200 with `results[].outcome: "not_supported"` for the Instagram target, and the post stays live. `POST /v1/posts/{pid}/unpublish` returns `501 resource.not_supported`. Delete the post in the Instagram app. [Facebook Login](#connect-through-facebook-login): `DELETE /v1/posts/{pid}` deletes the post from Instagram, and `POST /v1/posts/{pid}/unpublish` removes it from Instagram and keeps it in SocialAPI | | List posts | Yes | Posts, Reels, and carousels | | List active stories | Yes | Only stories that are live now | | List comments and replies | Yes | Comments are read per post | | Reply to a comment | Yes | | | Hide, unhide, delete a comment | Yes | | | Like a comment | Partial | Instagram Login: `501 resource.not_supported`, and `can_like` is `false` on every comment. [Facebook Login](#connect-through-facebook-login): yes, and `can_like` is `true` | | Private reply to a comment | Yes | One per comment, within 7 days of the comment | | DMs: list, read, send | Yes | Inside the 24-hour messaging window. Text, attachments, quick replies, buttons, cards | | React to a DM | Yes | | | Unsend or delete a DM | No | Only the Instagram app can | | Messaging participant profile | Yes | Only for a person who messaged the account or tapped an icebreaker or the persistent menu | | Mentions | Partial | `GET /v1/accounts/{id}/mentions` returns media the account is tagged in. Caption and comment @mentions arrive as `mention.received` events only | | Reply to a mention | Yes | | | Reviews | No | Instagram has no reviews. `GET /v1/inbox/reviews/{account_id}` returns `422 account.platform_unsupported` | | Automations | Yes | Comment and DM triggers, buttons, multi-step flows, the Follow gate. See [Automations](/guides/automations) | | Account summary | Yes | Needs the `analytics.read` [permission](/guides/permissions): see [Account summary](#account-summary) | | Analytics | Yes | See [Analytics](/guides/analytics) | | Collaboration invites | Yes | List, accept, decline. Invites are sent at publish time only | | Webhook events | Yes | See [Webhook events](#webhook-events) | The cross-platform matrix is on [Platforms](/platforms/overview). ## Connect an account Connect with `POST /v1/accounts/connect` and `"platform": "instagram"`. The shared flow, request fields, and result are in [Connect accounts](/guides/connect-accounts), and the redirect parameters are in [Redirect parameters](/guides/connect-accounts#redirect-parameters). ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/connect \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "instagram", "redirect_uri": "https://app.example.com/oauth/callback", "state": "session_abc123" }' ``` Response (HTTP 202): ```json theme={null} { "auth_url": "https://www.instagram.com/oauth/authorize/?client_id=1234567890&redirect_uri=https%3A%2F%2Fapi.social-api.ai%2Foauth%2Fcallback%2Finstagram&response_type=code&state=4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2#weblink", "state": "4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2", "message": "Open this link to connect your instagram account. Once you've authorized, the account will be connected automatically." } ``` Instagram-specific rules: * **Account type.** The person must connect an Instagram Business or Creator account. A personal account cannot be connected. * **One account per brand.** A brand holds one Instagram account. With [Facebook Login](#connect-through-facebook-login), each further Instagram account the login granted appears as a Page of the brand's Instagram account. Connecting a different Instagram account to the same brand ends with `status=error` and `error=account.already_linked` on your `redirect_uri`. * **Reconnect.** If a token is no longer valid, calls return `401 platform.instagram.auth` and the account needs reconnecting. See [Reconnect an account](/guides/connect-accounts#reconnect-an-account). * **Permissions.** Send `permissions` to ask the person for fewer scopes: see [Scopes](#scopes). ## Connect through Facebook Login Connect through Facebook Login when the Instagram account has a linked Facebook Page and you need to delete published posts, like comments, or set the paid-partnership label. Instagram Login stays the default: send no `connection_type` and nothing changes. The two paths are the same platform, `instagram`, and every call, field, and webhook event is the same unless this section says otherwise. Select Facebook Login with `metadata.connection_type: "facebook_login"` on `POST /v1/accounts/connect`. Any other value, or none, connects through Instagram Login. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/connect \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "instagram", "redirect_uri": "https://app.example.com/oauth/callback", "state": "session_abc123", "metadata": {"connection_type": "facebook_login"} }' ``` Response (HTTP 202): ```json theme={null} { "auth_url": "https://www.facebook.com/v25.0/dialog/oauth?client_id=1234567890&redirect_uri=https%3A%2F%2Fapi.social-api.ai%2Foauth%2Fcallback%2Finstagram&state=4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2&response_type=code&auth_type=rerequest&scope=...", "state": "4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2", "message": "Open this link to connect your instagram account. Once you've authorized, the account will be connected automatically." } ``` The person signs in to Facebook, not Instagram, and approves the scopes in [Facebook Login scopes](#facebook-login-scopes). The redirect then carries `status=selection_required`, and you choose which Instagram accounts join the brand as described in [Choose Facebook Pages](/guides/connect-accounts#choose-facebook-pages). When `facebook_page_selection` is `false` on your SocialAPI account (`GET /v1/users/me`), or the connection started from an invite link, the redirect carries `status=success` and the brand's Instagram account instead. ### More than one Instagram account under one login A Facebook login can administer more than one Instagram account. The brand's Instagram account holds each Instagram account you select (or, without a selection step, every Instagram account the login granted that none of your other brands holds), each as a Page. The `platform_page_id` of such a Page is the Instagram account ID. List the Pages with `GET /v1/accounts/{id}/pages`, and publish to one with `page_id` on the post target: see [Delivery](/guides/delivery). A Facebook Page with no linked Instagram account is not listed. * A login that grants no Instagram account ends with `status=error` and `error=platform.instagram.no_pages` on your `redirect_uri`. * Without a selection step, a login whose every Instagram account belongs to another of your brands, or is already held through Instagram Login, ends with `status=error` and `error=platform.instagram.no_assignable_pages`. * A brand whose Instagram account is connected through a different Facebook login ends with `status=error` and `error=account.different_login`. Disconnect the brand's Instagram account first. ### Requirements * The Instagram account is a Business or Creator account linked to a Facebook Page. An account with no linked Page does not appear in the connection: use Instagram Login. * The person holds the Page task that each capability needs. Meta grants an Instagram scope only for a task the person holds on the Page: create content for publishing, moderate for comments, messaging for DMs, and analyze for insights. * A DM or private reply on an account with no Facebook Page linked to it returns `409 platform.instagram.no_page`. Reconnect the account. ### One path per Instagram account Hold each Instagram account through one path. Connecting through Facebook Login does not add an account that you hold through Instagram Login: such an account comes back with `assignable: false` in the [selection step](/guides/connect-accounts#choose-facebook-pages), and selecting it returns `409 platform.instagram.flow_conflict`. Without a selection step, a connection with no other account to add ends with `status=error` and `error=platform.instagram.no_assignable_pages`. To switch an account to Facebook Login, disconnect it, then connect it again with `connection_type: "facebook_login"`. ### Differences from Instagram Login | Feature | Instagram Login | Facebook Login | | - | - | - | | Linked Facebook Page | Not needed | Required | | Instagram accounts per connection | One | Every account the login administers, each as a Page of the brand's Instagram account | | Delete a published post | No | Yes: `DELETE /v1/posts/{pid}` deletes it, and `POST /v1/posts/{pid}/unpublish` removes it from Instagram and keeps it in SocialAPI | | Like a comment | No | Yes | | `message_tag: "HUMAN_AGENT"` on a DM | Extends the window to 7 days | Not sent: the DM goes out untagged and the 24-hour window applies | | `content_type` on a post | Required | Optional. `stories` and `carousel` apply as given. Otherwise, including `feed` and `reel`, the media decides: one video publishes as a Reel, one image as a feed post, 2 or more items as a carousel | | Paid-partnership label | Either key fails validation | `platform_data.is_paid_partnership` (`true` or `false`) and `platform_data.branded_content_sponsor_ids` (1 or 2 numeric Instagram account IDs) | | Scope names | `instagram_business_*` | `instagram_*` and `pages_*` | Every other row of the Capabilities table behaves the same on both paths. ### Facebook Login scopes SocialAPI's Meta app requests these scopes at connect. You apply for no app review. Each scope belongs to the account [permissions](/guides/permissions) in the last column, and the consent screen asks for the scopes of the permissions you selected. The scopes marked Every account are always requested. | Scope | Used for | Permissions | | - | - | - | | `instagram_basic` | Profile and media | Every account | | `pages_show_list` | List the Pages the person manages | Every account | | `pages_read_engagement` | Read engagement on the linked Page | Every account | | `pages_read_user_content` | Read content on the linked Page | Every account | | `pages_manage_metadata` | Subscribe the linked Page to webhook events | Every account | | `business_management` | List Pages owned through a Business Portfolio | Every account | | `instagram_content_publish` | Publish posts and Reels | `posts.write` | | `instagram_manage_contents` | Delete a published post | `posts.write` | | `instagram_manage_comments` | Read, reply to, hide, and delete comments, read mentions, and the first comment | `comments.read`, `comments.write` | | `instagram_manage_engagement` | Like a comment | `comments.write` | | `instagram_manage_messages` | Read and send DMs and private replies | `messages.read`, `messages.write` | | `pages_messaging` | Receive DM events for the linked Page | `messages.read`, `messages.write` | | `instagram_manage_insights` | Account summary and post insights | `analytics.read` | The account stays limited to the permissions you selected: an account connected with `comments.read` alone cannot reply to comments. ## Comments List comments on a post with `GET /v1/inbox/comments/{postId}`, where `postId` is the post's platform ID (`targets[].platform_post_id`). Reply, hide, unhide, and delete work as described in [Comments](/guides/comments). Instagram-specific rules: * On Instagram Login, a comment's `capabilities.can_like` is always `false`, because Instagram has no like endpoint for comments on that path. On [Facebook Login](#connect-through-facebook-login) it is `true`. ## Private replies A private reply sends a DM to the person who commented. Send it with `POST /v1/inbox/comments/{postId}/{commentId}/private-reply`. * Instagram allows one private reply per comment, within 7 days of the comment. A second reply, or a comment older than 7 days, returns `400 platform.instagram.private_reply_ineligible`. * A private reply does not open the 24-hour messaging window: the person's answer does. * The request accepts up to 3 `buttons`, sent as a button template. * An [Automation](/guides/automations) can send the private reply whenever a comment matches its keywords. ## Direct messages and the messaging window An Instagram account can message a person only inside the 24-hour messaging window that the person's last message opened. Outside it, the send returns `400 platform.instagram.window_closed`. On Instagram Login, send `message_tag: "HUMAN_AGENT"` to reply up to 7 days after the person's last message. Instagram Login accepts no other tag: any other value returns `400 validation.invalid_message_tag`. On [Facebook Login](#connect-through-facebook-login), `message_tag` is not sent: the DM goes out untagged and the 24-hour window applies. * **Message types.** Text, attachments, quick replies, buttons, and cards. Each attachment in `attachments[]` has a `source_type` (`url`, `media_id`, or `platform_attachment_id`) and a `type`. `type` is required for `platform_attachment_id`, or the request returns `400 validation.attachment_type_required`. Field rules are in [Direct messages](/guides/direct-messages). * **Size limits.** Instagram sets them and refuses a larger message: text up to 1,000 bytes of UTF-8; images in PNG or JPEG up to 8 MB; video in MP4, OGG, AVI, MOV, or WebM up to 25 MB; audio in AAC, M4A, WAV, or MP4 up to 25 MB; files in PDF up to 25 MB. * **Unsending.** Instagram does not let apps unsend or delete a sent DM. When the other person unsends a message, you receive a `dm.deleted` event and the message disappears from the conversation. * **Reactions.** `POST /v1/inbox/conversations/{id}/messages/{messageId}/reaction` reacts to a message with any emoji, and `DELETE` on the same path removes the reaction. * **Story mentions.** A story that mentions your account arrives as `dm.received` with an attachment of type `story_mention`, not as `mention.received`. ### Participant profile `GET /v1/platforms/instagram/accounts/{id}/participants/{participantId}/profile` returns the profile of a person in a conversation, including whether they follow the account (`is_user_follow_business`) and whether the account follows them (`is_business_follow_user`). `participantId` is the `participant_id` of the conversation. ```bash theme={null} curl "https://api.social-api.ai/v1/platforms/instagram/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/participants/2287522528728101/profile" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "id": "2287522528728101", "username": "janedoe", "name": "Jane Doe", "profile_pic": "https://scontent.cdninstagram.com/v/.../profile.jpg", "follower_count": 310, "is_user_follow_business": true } ``` * Read the follow fields at each call, because they change when a person unfollows. * `profile_pic` links expire: do not store them. * Instagram discloses a profile only for a person who messaged the account or tapped an icebreaker or the persistent menu. For anyone else the call returns `403 platform.instagram.230`, which means Instagram withholds the data. It does not mean the person does not follow the account. * A field that Instagram has no value for, such as `name`, is absent. * `is_verified_user`, `is_user_follow_business`, and `is_business_follow_user` are absent when `false`, and `follower_count` is absent when it is `0`. An absent follow flag means the person does not follow in that direction, or Instagram did not report it: the response does not tell the two apart. When Instagram withholds the relationship because the person has not given consent, the call returns `403 platform.instagram.230` instead. ## Mentions `GET /v1/accounts/{id}/mentions` returns media in which another account tagged yours. An @mention written in a caption or a comment is not in that list: it arrives as a `mention.received` event, with `metadata.mention_type` set to `comment` or `caption`. Reply to a mention with `POST /v1/accounts/{id}/mentions/reply`. A story mention arrives as a DM: see [Direct messages](#direct-messages-and-the-messaging-window). Details are in [Mentions](/guides/mentions). ## Stories `GET /v1/platforms/instagram/accounts/{id}/stories` lists the stories that are live now. Instagram removes a story 24 hours after posting, so a call returns nothing older. The request takes `limit` (1 to 100, default 20) and `cursor`, and no `since`. ```bash theme={null} curl "https://api.social-api.ai/v1/platforms/instagram/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/stories?limit=10" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "id": "17895695668004550", "platform": "instagram", "caption": "", "media_type": "VIDEO", "media_url": "https://scontent.cdninstagram.com/v/t51.../story.jpg", "permalink": "https://www.instagram.com/stories/acmecorp/17895695668004550/", "timestamp": "2026-10-12T12:00:00Z", "like_count": 0, "comments_count": 0, "shares_count": 0, "view_count": 0, "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "metadata": { "is_story": true, "expires_at": "2026-10-13T12:00:00Z" } } ], "count": 1 } ``` Each item has `id`, `platform`, `caption`, `media_type`, `media_url`, `permalink`, `timestamp`, `like_count`, `comments_count`, `shares_count`, `view_count`, `account_id`, and `metadata`. `like_count`, `comments_count`, `shares_count`, and `view_count` are always `0` on a story. An item also carries `settings` when Instagram reports any: see [Settings on published posts](/platforms/instagram/publishing#settings-on-published-posts). `metadata.is_story` is always `true`, and `metadata.expires_at` is the posting time plus 24 hours. For a video story with no `media_url`, the cover image is returned as `media_url`. ## Collaboration invites A collaborator is invited when a post is published, with `platform_data.collaborators`: see [Instagram publishing](/platforms/instagram/publishing#platform-data-fields). Instagram has no call that adds a collaborator to a published post. Three calls remain: | Method | Path | Description | | - | - | - | | `GET` | `/v1/platforms/instagram/accounts/{id}/media/{mediaId}/collaborators` | Lists the collaborators of a post and the `status` of each invite. Call it with the account that published the post | | `POST` | `/v1/platforms/instagram/accounts/{id}/media/{mediaId}/collab/accept` | Accepts a pending invite and publishes the co-authored post on this account's feed. Call it with the invited account. An invite expires 14 days after it is sent | | `POST` | `/v1/platforms/instagram/accounts/{id}/media/{mediaId}/collab/decline` | Declines a pending invite. The post does not appear on this account's feed. Call it with the invited account | The list call returns HTTP 200: ```json theme={null} { "data": [ { "id": "17841405793187218", "username": "janesmith", "status": "pending" } ], "count": 1 } ``` `status` is `accepted`, `pending`, or `declined`. Accepting or declining with no pending invite for the account on that media returns an error from Instagram. Accept and decline return `{"success": true}`. ## Account summary `GET /v1/platforms/instagram/accounts/{id}/summary` returns live profile counts and insight totals for the trailing 30 days. ```bash theme={null} curl "https://api.social-api.ai/v1/platforms/instagram/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/summary" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "username": "acmecorp", "display_name": "Acme Corp", "picture_url": "https://scontent.cdninstagram.com/v/t51.../profile.jpg", "follower_count": 12400, "following_count": 87, "media_count": 340, "reach": 22400, "views": 90100, "interactions": 5500, "accounts_engaged": 4000, "net_follows": 120, "period_start": "2026-09-13T10:00:00Z", "period_end": "2026-10-13T10:00:00Z" } ``` The insight totals need the `analytics.read` [permission](/guides/permissions) on the account. An account without it returns `403 account.permission_missing`. When Instagram withdraws the insights access, the next call can return `403 platform.instagram.insights_scope_missing` instead: reconnect the account. Instagram's insight data can lag by up to 48 hours. Per-post and per-account metrics over a window you choose are in [Analytics](/guides/analytics). ## Webhook events Instagram accounts emit these events to your webhook endpoints. Payloads are in [Webhook events](/guides/webhook-events). | Event | When it fires | | - | - | | `comment.received` | A person comments on a post | | `mention.received` | Another account @mentions yours in a caption or a comment | | `dm.received` | A person sends a DM, including a story mention | | `dm.sent` | A message left the account, including one sent from the Instagram app | | `dm.deleted` | A person unsent a message | | `dm.referral` | A person opened a conversation from an ad or referral link without sending a message | | `dm.postback` | A person tapped a quick reply or a button | | `dm.status.sent` | Instagram accepted a DM you sent | | `dm.status.read` | A person read a DM you sent. Instagram sends no delivery receipts, so there is no `dm.status.delivered` | | `account.connected` | You finished connecting or reconnecting the account | | `account.disconnected` | The person revoked access to the account on Instagram, or SocialAPI flagged the account for reconnection (`reconnect_required: true`, see `reason`) | | `account.updated` | The account lost [permissions](/guides/permissions) you did not remove yourself (`change` is `permissions_reduced`) | | `post.published`, `post.partial`, `post.failed` | A post to this account finished: see [Delivery](/guides/delivery) | Every `post.*` event in [Webhook events](/guides/webhook-events#posts) is emitted for this platform. ## Limits | Limit | Value | | - | - | | Caption | 2,200 characters, 30 hashtags, 20 mentions | | Carousel | 2 to 10 items | | Posts | 45 per 24 hours and 25 per hour for each account. Instagram's own cap is 50 per rolling 24 hours: see [Posting velocity caps](/guides/rate-limits#posting-velocity-caps) | | Private reply | 1 per comment, within 7 days | | Instagram's own rate limit | `429 platform.instagram.rate_limit`. Credit rules: see [when a credit comes back](/guides/plans#when-a-credit-comes-back) | `GET /v1/accounts/{id}/limits` returns `posts_remaining`, the posts left in Instagram's rolling 24 hours. ## Scopes SocialAPI's Meta app requests these scopes at connect through Instagram Login. You apply for no app review. The scopes of Facebook Login are in [Facebook Login scopes](#facebook-login-scopes). Each scope belongs to the account [permissions](/guides/permissions) in the last column, and the consent screen asks for the scopes of the permissions you selected. `instagram_business_basic` is always requested. | Scope | Used for | Permissions | | - | - | - | | `instagram_business_basic` | Profile and media | Every account | | `instagram_business_manage_comments` | Read, reply to, hide, and delete comments, read mentions, and the first comment | `comments.read`, `comments.write` | | `instagram_business_manage_messages` | Read and send DMs and private replies | `messages.read`, `messages.write` | | `instagram_business_content_publish` | Publish posts, Reels, carousels, and Stories | `posts.write` | | `instagram_business_manage_insights` | Account summary and post insights | `analytics.read` | Instagram asks for comments and for messages as 1 scope each, covering both reading and writing, so the consent screen shows the broader scope. The account stays limited to the permissions you selected: an account connected with `comments.read` alone cannot reply to comments. ## Related * [Instagram publishing](/platforms/instagram/publishing): fields, media rules, and quirks for publishing * [Connect accounts](/guides/connect-accounts): the shared connect flow * [Automations](/guides/automations): comment-to-DM and the Follow gate * [Platforms](/platforms/overview): the capability matrix * [Errors](/guides/errors): every error code # Instagram publishing Source: https://docs.social-api.ai/platforms/instagram/publishing Publish image, video, Reel, Story, and carousel posts to Instagram through SocialAPI, with platform-specific fields, limits, and error recovery. ## 1. Overview Instagram processes media before it can be published, so every post type (image, video, reel, carousel, story) goes live once Instagram reports the media ready. | Field | Value | | - | - | | Platform slug | `instagram` | | Auth type | OAuth 2.0 with Instagram Login, or with Facebook Login: see [Connect through Facebook Login](/platforms/instagram#connect-through-facebook-login) | | Platform API | Instagram API with Instagram Login, or with Facebook Login (Meta) | | Create post | Yes | | Update post | Not after publishing (`409 post.state_invalid`). `draft`, `scheduled`, and `failed` posts can be edited | | Delete post | Instagram Login: No (`outcome: not_supported`). Facebook Login: Yes, and unpublish works. See [Delete post](#5-delete-post) | | Schedule | Yes (deferred publish via `scheduled_at`) | | First comment | Yes (best-effort, non-blocking) | | Posting caps | 45 posts per 24 hours and 25 per hour for each account: see [Posting velocity caps](/guides/rate-limits#posting-velocity-caps) | *** ## 2. Supported media | Content type | How to trigger | Notes | | - | - | - | | Single image | `content_type: "feed"` | Standard image post | | Single video (Reel) | `content_type: "reel"` | Published as `media_type: REELS` | | Carousel | `content_type: "carousel"` with 2 to 10 `media` entries | 2 to 10 items; mix of images and videos allowed | | Story | `content_type: "stories"` | A single image (JPEG, up to 8 MB) or a single video (MP4 or QuickTime, 3 to 60 seconds, up to 100 MB). Captions are not supported | `content_type` is required to publish or schedule an Instagram post, including a draft you publish later. Without it, the post fails validation with `400 publishing.validation_failed`. A draft can be saved without it, because drafts skip validation. On an account connected through [Facebook Login](/platforms/instagram#connect-through-facebook-login), `content_type` is optional: see [Differences from Instagram Login](/platforms/instagram#differences-from-instagram-login). **File limits:** images must be JPEG and at most 8 MB. Videos must be MP4 or QuickTime, at most 300 MB, and 3 seconds to 15 minutes long. A Story video has the tighter limits of 3 to 60 seconds and 100 MB. A file outside these limits fails validation with `400 publishing.validation_failed`. SocialAPI does not convert files: convert a PNG to JPEG before you send it. Media you upload to SocialAPI (`source_type: "media_id"`) is checked against these limits. For a `url` item, SocialAPI checks the file type against these limits, and the size when the host reports it. The duration of a video sent by URL is not checked in advance, so a video outside the duration limits reaches Instagram and fails there. **Caption limits:** 2,200 characters, 30 hashtags, and 20 mentions. More returns `400 publishing.validation_failed`. **Carousel limits:** 2 to 10 items, images and videos mixed. Only the carousel itself carries the caption. A caption on an item is ignored. Stories expire 24 hours after posting and carry no caption. > [!WARNING] > > Attach media through the `media` array. Each item sets a `source_type`: `media_id` for a file you uploaded to SocialAPI, or `url` for a public `http(s)` URL SocialAPI fetches at publish time. There is no separate `image_url`, `video_url`, or `media_urls` field. To upload first, use `POST /v1/media/upload` (multipart) or the presigned-URL flow (`GET /v1/media/upload-url` → `PUT` the file → `POST /v1/media/:id/verify`); the `verify` step is required, without it the media stays `pending`, and a post that names media that is not `ready` returns `400 publishing.validation_failed`. > > Instagram also accepts `source_type: "platform_attachment_id"` for a container you created through the Graph API yourself. It must then be the only item in `media`: Instagram cannot combine a pre-built container with other media, and doing so returns `422 publishing.media.mixing_unsupported`. *** ## 3. Create post Use `POST /v1/posts` with `targets` targeting an Instagram account. The `text` field becomes the caption. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "Check out our new collection. #fashion #style", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "platform_data": { "instagram": { "content_type": "feed", "alt_text": "A flat-lay photo of our spring collection", "location_id": "110506962309835" } } }' ``` Response (HTTP 201): ```json theme={null} { "id": "6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "text": "Check out our new collection. #fashion #style", "status": "publishing", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "media_ids": ["f47ac10b-58cc-4372-a567-0e02b2c3d479"], "hidden": false, "retry_count": 0, "created_at": "2026-10-13T09:00:00Z", "updated_at": "2026-10-13T09:00:00Z", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "instagram", "status": "pending", "text": "Check out our new collection. #fashion #style", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "media_ids": ["f47ac10b-58cc-4372-a567-0e02b2c3d479"] } ] } ``` SocialAPI publishes in the background. The target moves from `pending` to `publishing`, then to `published` or `failed`. Poll `GET /v1/posts/{pid}` or subscribe to the `post.published`, `post.partial`, and `post.failed` [webhook events](/guides/webhook-events). Publishing or scheduling costs 1 post credit; a draft is free (see [Plans and credits](/guides/plans)). ### Platform data fields Send these keys in `platform_data.instagram` on the post, which applies them to every Instagram target. To set them for one target, send them flat in that target's `platform_data` (`targets[].platform_data`, with no `instagram` key). When both set the same key, the target's value wins. | Key | Type | Applies to | Notes | | - | - | - | - | | `content_type` | string | feed, reel, carousel, stories | One of `"feed"`, `"reel"`, `"carousel"`, `"stories"`, case-insensitive. Required to publish or schedule. Optional on Facebook Login: see [Differences from Instagram Login](/platforms/instagram#differences-from-instagram-login). | | `alt_text` | string, up to 1000 characters | feed, carousel | Accessibility text describing the image. Not used on Reels or stories. | | `location_id` | numeric string | feed, reel, carousel (whole carousel, not per slide) | Geo-tags the post to a Facebook location page. Returns an error on stories. | | `share_to_feed` | boolean | reel | When `true`, the Reel also appears in the main profile feed. | | `collaborators` | string or array, up to 3 | feed, reel, carousel (whole carousel, not per slide) | Instagram usernames invited as collaborators. Leading `@` is stripped automatically. Accepts a single string, a comma-separated string, or a JSON array. | | `cover_url` | string (URL) | reel | Custom cover image for the Reel. Takes priority over `video_cover_timestamp_ms` and `thumb_offset` when more than one is set. | | `video_cover_timestamp_ms` | number, 0 or more | reel | Millisecond offset into the video used as the cover frame, when `cover_url` is not set. | | `thumb_offset` | number, 0 or more | reel | Same as `video_cover_timestamp_ms`, kept as an alternate name. When both are set, `thumb_offset` is used. `cover_url` overrides both. | | `is_ai_generated` | boolean | feed, reel, stories, carousel (whole carousel, not per slide) | Adds Instagram's AI info label. See "AI-generated content" below. | | `user_tags` | array of `{username, x, y}` | feed (x and y required, 0 to 1), reel (username only) | Tags people in the post. Returns an error on carousels. | | `audio_name` | string | reel | Name of the audio track shown on the Reel. | | `is_paid_partnership` | boolean | feed, reel, carousel | Facebook Login only. Adds Instagram's paid-partnership label. On Instagram Login it fails validation. | | `branded_content_sponsor_ids` | array of 1 or 2 numeric strings | feed, reel, carousel | Facebook Login only. Instagram account IDs of the sponsors named on the paid-partnership label. On Instagram Login it fails validation. | | `trial_params` | `{graduation_strategy}` | reel | Sets how a trial Reel graduates to your full audience. `graduation_strategy` is `"MANUAL"` or `"SS_PERFORMANCE"`. | SocialAPI forwards any additional keys in `platform_data.instagram` that the table above lists for the post type you publish, and no others. Any other key, including a key listed for another post type, is not sent, and the response lists each one as a validation warning naming the key. `location_id` must be a string of digits, `share_to_feed` and `is_ai_generated` must be booleans, and `alt_text` on a Reel is not sent. ### AI-generated content Set `is_ai_generated: true` to add Instagram's AI info label. It applies to feed posts, Reels, stories and carousels. On a carousel it labels the whole post: there is no per-slide option. Per Meta, the label is for AI-generated media, not AI-written captions. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "A little AI magic for our spring lookbook.", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "platform_data": { "instagram": { "content_type": "feed", "is_ai_generated": true } } }' ``` ### Scheduling Set `scheduled_at` to an ISO 8601 timestamp (UTC) to defer publishing: ```json theme={null} { "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "Scheduled caption", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "platform_data": { "instagram": { "content_type": "feed" } }, "scheduled_at": "2026-11-15T10:00:00Z" } ``` ### First comment Set `first_comment` to a string to post an automated comment immediately after publish. This is best-effort: if the comment fails (for example due to a temporary rate limit), the post is still considered published. ```json theme={null} { "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "New drop. Link in bio.", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "platform_data": { "instagram": { "content_type": "feed" } }, "first_comment": "Shop now: https://example.com/shop" } ``` *** ## 4. Update post A published Instagram post cannot be edited through SocialAPI. `PATCH /v1/posts/{pid}` returns `409 post.state_invalid` for any post whose status is not `draft`, `scheduled`, or `failed`, with the message "Only draft, scheduled, or failed posts can be edited". A post in `draft`, `scheduled`, or `failed` status can be edited with `PATCH /v1/posts/{pid}` before it publishes. To correct a published post, delete it in the Instagram app and publish a new post. *** ## 5. Delete post On an account connected through [Facebook Login](/platforms/instagram#connect-through-facebook-login), `DELETE /v1/posts/{pid}` deletes the post from Instagram, and `POST /v1/posts/{pid}/unpublish` removes it from Instagram and keeps it in SocialAPI. The rest of this section covers Instagram Login. On Instagram Login, `DELETE /v1/posts/{pid}` returns HTTP 200 with `results[].outcome: "not_supported"` for an Instagram target, and the post stays live on Instagram. `POST /v1/posts/{pid}/unpublish` returns `501 resource.not_supported` for an Instagram target. Instagram Login gives apps no media deletion call, so SocialAPI refuses rather than report a deletion that did not happen. Delete the post in the Instagram app. For a cross-platform post, the other platforms are still deleted or unpublished. The response names every platform whose content is still live, so an Instagram target refusing does not undo the targets that succeeded. See [Delete a post](/guides/delivery#delete-a-post). *** ## 6. Retrieve posts Use `GET /v1/posts` to list posts. Filter by platform or account: ```bash theme={null} curl "https://api.social-api.ai/v1/posts?platform=instagram&account_ids=acc_01HZ9X3Q4R5M6N7P8V2K0W1J&limit=20" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Each post includes a `targets` array with per-platform status and engagement metrics: ```json theme={null} { "id": "6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "text": "Check out our new collection.", "status": "published", "targets": [ { "platform": "instagram", "platform_post_id": "17895695668004550", "status": "published", "permalink": "https://www.instagram.com/p/ABC123/", "platform_media": [ { "type": "image", "url": "https://scontent.cdninstagram.com/v/t51.29350-15/123_n.jpg" }, { "type": "video", "url": "https://scontent.cdninstagram.com/o1/v/t16/456.mp4", "thumbnail_url": "https://scontent.cdninstagram.com/v/t51.29350-15/456_n.jpg" } ], "metrics": { "likes": 42, "comments": 5, "shares": 0, "saves": 0 }, "metrics_synced_at": "2026-04-10T12:00:00Z" } ] } ``` **Metrics notes:** * `likes` and `comments` come from the media object itself. `shares`, `saves` and the insight metrics below are read live from `GET /v1/posts/{pid}/metrics`. * `metrics_synced_at` reflects when SocialAPI last refreshed the metrics from Instagram. ### Live metrics and insights `GET /v1/posts/{pid}/metrics` reads the post's current counts and insights from Instagram: ```bash theme={null} curl "https://api.social-api.ai/v1/posts/6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d/metrics" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` ```json theme={null} { "data": { "post_id": "6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "targets": [ { "platform": "instagram", "metrics": { "likes": 42, "comments": 5, "shares": 8, "saves": 31, "views": 4300, "reach": 1820, "interactions": 86, "avg_watch_time_ms": 3400, "total_watch_time_ms": 14620000, "skip_rate": 41.2, "crossposted_views": 310 }, "metrics_synced_at": "2026-04-10T12:00:00Z" } ] } } ``` The metric names are the same on every platform, so code written against them does not change per platform. Which metrics Instagram reports depends on the media type: | Metric | Feed posts and carousels | Reels | | - | - | - | | `views`, `reach`, `interactions`, `saves`, `shares` | yes | yes | | `follows`, `profile_visits` | yes | no | | `profile_actions`, broken down into `website_clicks`, `calls`, `directions`, `emails`, `texts` | yes | no | | `avg_watch_time_ms`, `total_watch_time_ms`, `skip_rate` | no | yes | | `crossposted_views` | when shared to Facebook | when shared to Facebook | * A metric Instagram has not reported is absent rather than returned as `0`, so a missing key means "not reported", not "zero". * `interactions` is Instagram's own total of likes, comments, saves and shares. `skip_rate` is a percentage. * If an Instagram target returns `likes` and `comments` but none of the insight metrics, the account has no `instagram_business_manage_insights` scope. Reconnect the account to enable them. * Instagram's insight data can lag by up to 48 hours. ### Settings on published posts An Instagram target can include a `settings` object with the options Instagram reports back for the published post: | Field | Type | Description | | - | - | - | | `is_ai_generated` | boolean | Whether the AI info label is on the post. | | `alt_text` | string | Alt text on the post. | | `share_to_feed` | boolean | Whether a Reel also appears in the main profile feed. | | `comments_enabled` | boolean | Whether comments are open on the post. | | `product_type` | string | Surface the post lives on: `FEED`, `REELS`, `STORY`, or `AD`. | | `audio_type` | string | Kind of audio on a Reel: `MUSIC` or `ORIGINAL_SOUND`. | `settings` is absent when Instagram reported none of these for the post. Instagram does not report location, user tag, audio name, or trial settings back, so those never appear here even when you set them at publish time. `settings` is also returned for posts published outside SocialAPI, since it reflects what Instagram reports at the time of the read for the post rather than what was sent through this API. ### Media on published posts An Instagram target includes `platform_media`: every image and video on the published post, carousel slides in order. It is returned for posts published outside SocialAPI too, so you can read the photos and videos of an account's existing posts. | Field | Type | Description | | - | - | - | | `type` | string | `image` or `video`. | | `url` | string | Link to the file, hosted by Instagram. | | `thumbnail_url` | string | Link to a video's cover image. Absent on images. | The links expire. Download the files when you read the post rather than storing the links. To get fresh links, call `POST /v1/accounts/{id}/sync`, then read the post again. `url` is absent when Instagram withholds the file, for example a video with licensed audio. The cover image is still included. `POST /v1/accounts/{id}/sync` covers up to the 500 most recent posts on an Instagram account. Each account can be synced once every 5 minutes; an earlier call returns `429 accounts.sync_rate_limited` with a `Retry-After` header. The account needs the `posts.read` [permission](/guides/permissions): without it the call returns `403 account.permission_missing`. `platform_media` is separate from `media`, which lists what you sent when publishing through SocialAPI and is empty for posts published elsewhere. *** ## 7. Quirks, errors, and recovery ### Media processing delay A target still processing after about 5 minutes fails with `504 platform.instagram.container_timeout`. ### First comment is non-blocking If `first_comment` is set and the comment fails after publish, the post stays `published`. Nothing in the post response reports the failed comment. ### Stories expire Instagram removes a Story 24 hours after posting. The post stays in SocialAPI. ### Posting caps SocialAPI refuses a publish that would pass 25 posts in an hour or 45 posts in 24 hours for one account, with `429 publishing.velocity_limit`. A scheduled post is held and published when the window clears. Instagram's own cap is 50 posts per rolling 24 hours: `GET /v1/accounts/{id}/limits` returns `posts_remaining`. When Instagram refuses first, the target fails with `429 platform.instagram.rate_limit`. See [Posting velocity caps](/guides/rate-limits#posting-velocity-caps). ### Errors on a failed target A publish that fails after the request returned puts a structured error on the target, in `targets[].error`: ```json theme={null} { "code": "platform.instagram.container_failed", "message": "Instagram could not process the media", "category": "platform", "caused_by": "platform" } ``` * `401 platform.instagram.auth`: the access token expired or was revoked. Reconnect the account. * `429 platform.instagram.rate_limit`: Instagram's own limit. Wait, then retry. * `422 platform.instagram.container_failed`: Instagram accepted the media, then failed to process it. See "Container failures report no reason". * `422 platform.instagram.container_expired`: the media container was not published within 24 hours. Create the post again. * `504 platform.instagram.container_timeout`: the media was still processing after about 5 minutes. * `503 platform.instagram.1`, `503 platform.instagram.2`: a temporary Instagram failure. Retry. * `502 platform.instagram.api_error`: a failure reaching Instagram, or a response with no usable error. * `platform.instagram.`: Instagram refused the request with its own error code and message, for example "The aspect ratio is not supported." The HTTP status is `502` unless the code has its own mapping, such as `403 platform.instagram.200`, `404 platform.instagram.100`, or `409 platform.instagram.9007`. Read `message`. Every code is in [Errors](/guides/errors). ### Container failures report no reason When Instagram rejects media during processing, it reports only a status of `ERROR`, with no code and no message. `platform.instagram.container_failed` reports the failure and the container ID, not a cause. It almost always means the file does not meet Instagram's specifications: for Reels, MOV or MP4 with H.264 or HEVC video, up to 300 MB and 15 minutes, 9:16 recommended. ### Recovery * **Rate limit.** Wait, then send `POST /v1/posts/{pid}/retry`. See [Delivery](/guides/delivery). * **Auth error.** Reconnect the Instagram account through the OAuth flow to get a fresh token. * **Container timeout.** A large video or a slow media URL causes it. Upload the media again and retry. * **Container failed.** A temporary fault and a rejected file look the same. Retry once with `POST /v1/posts/{pid}/retry`. If it fails again, re-encode the media to the specifications above. *** ## 8. Full worked example The following example publishes a Reel with a cover image, a collaborator tag, and a first comment. **Step 1: Upload media** ```bash theme={null} # Get a presigned upload URL curl "https://api.social-api.ai/v1/media/upload-url?filename=reel.mp4&media_type=video/mp4" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response: ```json theme={null} { "upload_url": "https://upload.example.com/media/reel.mp4?X-Amz-Signature=...", "media_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479" } ``` Upload the file, then verify: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/media/f47ac10b-58cc-4372-a567-0e02b2c3d479/verify \ -H "Authorization: Bearer $SOCAPI_KEY" ``` **Step 2: Publish the post** ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "Behind the scenes from our studio shoot. #bts #fashion", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "first_comment": "Full video on YouTube: https://youtube.com/watch?v=example", "platform_data": { "instagram": { "content_type": "reel", "share_to_feed": true, "cover_url": "https://cdn.example.com/media/reel-cover.jpg", "collaborators": "@partner_brand" } } }' ``` **Step 3: Check status** ```bash theme={null} curl "https://api.social-api.ai/v1/posts/6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response once published: ```json theme={null} { "id": "6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "text": "Behind the scenes from our studio shoot. #bts #fashion", "status": "published", "targets": [ { "platform": "instagram", "platform_post_id": "17895695668004550", "status": "published", "permalink": "https://www.instagram.com/reel/ABC123/", "metrics": { "likes": 42, "comments": 5, "shares": 0, "saves": 0 }, "metrics_synced_at": "2026-04-10T12:00:00Z" } ], "created_at": "2026-04-10T09:00:00Z", "published_at": "2026-04-10T09:00:47Z" } ``` *** ## 9. Required OAuth scopes SocialAPI's Meta app requests these scopes during the OAuth flow. You configure nothing. The full list is in [Instagram scopes](/platforms/instagram#scopes). Publishing uses `instagram_business_basic`, `instagram_business_content_publish`, and, for `first_comment`, `instagram_business_manage_comments`. | Scope | Purpose | | - | - | | `instagram_business_basic` | Read basic account info and media | | `instagram_business_manage_comments` | Post the first comment | | `instagram_business_content_publish` | Create and publish media containers | | `instagram_business_manage_insights` | Read per-post insights | The scopes a scoped API key needs, the credit cost of a post, and the MCP tools that publish are on [Publishing](/guides/publishing). # LinkedIn API Source: https://docs.social-api.ai/platforms/linkedin Connect LinkedIn member profiles and company Pages to publish posts, answer comments and mentions, and read follower and post insights. The LinkedIn API in SocialAPI connects a LinkedIn member profile and the company Pages that member administers, all under the platform slug `linkedin`. A member profile publishes text, image, video, multi-image, and link posts as the person, replies to, likes, and deletes comments, and returns insights. A company Page publishes as the Page, returns insights, and adds comment listing, mentions, and the Page's posts made outside SocialAPI. SocialAPI's own LinkedIn app connects both, so you register nothing: see [Platform apps](/guides/platform-credentials). ## At a glance | Field | Value | | - | - | | Platform slug | `linkedin`, for member profiles and company Pages. `GET /v1/accounts` tells them apart with `connection_type`: `profile` or `organization` | | Auth | OAuth 2.0 (LinkedIn) | | Platform API | LinkedIn Posts API | | Status | Available | ## Capabilities An unsupported call returns `501 resource.not_supported`. | Feature | Status | Condition | | - | - | - | | Publish text, image, video, multi-image, link posts | Yes | See [LinkedIn publishing](/platforms/linkedin/publishing) | | Schedule a post | Yes | `scheduled_at` on the post | | First comment on a post | No | `first_comment` is ignored | | Update a published post | No | `PATCH /v1/posts/{pid}` returns `409 post.state_invalid` for a published post. Delete the post and publish a new one | | Delete a published post | Yes | | | List posts | Partial | Company Page: the Page's posts, including posts made outside SocialAPI. Member profile: only the posts you published through SocialAPI | | Comments | Conditional | Company Page: list, reply, like, and delete. Member profile: reply, like, and delete only, because LinkedIn does not allow reading comments on a member's posts: listing returns `501 resource.not_supported`. Hide and private reply are not supported | | Analytics | Conditional | `GET /v1/platforms/linkedin/insights` on a company Page or a member profile: see [Insights](#insights). Live post metrics return `501 resource.not_supported`. Stored post history covers company Page posts only: see [Analytics](/guides/analytics) | | DMs | No | `501 resource.not_supported` | | Mentions | Conditional | Company Page only: `GET /v1/accounts/{id}/mentions` returns posts and comments that mention the Page from the last 60 days, and `POST /v1/accounts/{id}/mentions/reply` answers them as the Page. A member profile returns `501 resource.not_supported` | | Reviews | No | LinkedIn has no reviews. `GET /v1/inbox/reviews/{account_id}` returns `422 account.platform_unsupported` | | Automations | No | `422 automation.platform_unsupported` | | Webhook events | Partial | `account.connected`, `account.updated`, the `post.*` events, and on a company Page `account.disconnected` and `mention.received` | The cross-platform matrix is on [Platforms](/platforms/overview). ## Connect an account Connect with `POST /v1/accounts/connect` and `"platform": "linkedin"`. One LinkedIn authorization offers the member profile of the person who signs in and every company Page that person administers. The shared flow, request fields, and result are in [Connect accounts](/guides/connect-accounts), and the redirect parameters are in [Redirect parameters](/guides/connect-accounts#redirect-parameters). ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/connect \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "linkedin", "brand_id": "1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f", "redirect_uri": "https://app.example.com/oauth/callback", "state": "session_abc123" }' ``` Response (HTTP 202): ```json theme={null} { "auth_url": "https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=77abc123&state=4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2", "state": "4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2", "message": "Open this link to connect your linkedin account. Once you've authorized, the account will be connected automatically." } ``` LinkedIn-specific rules: * **One account per connect.** When the authorization offers exactly 1 account, it connects at once with `status=success`. When it offers more than 1, the redirect carries `status=selection_required` and you choose one, as described in [Choose a LinkedIn profile or Page](/guides/connect-accounts#choose-a-linkedin-profile-or-page). Connect the others from the same login with [the login endpoints](#connect-more-profiles-and-pages-from-a-login), with no new consent screen. * **Narrow the connect with `metadata.connection_type`.** `"profile"` requests the member profile scopes only and offers the member profile only. `"organization"` requests the company Page scopes only and offers the member's company Pages only. Without `connection_type`, the connect requests both sets and offers both. Any other value returns `400 validation.connection_type_invalid`. * **No company Page.** A connect with `"organization"` by a member who administers no company Page ends with `status=error` and `error=platform.linkedin.no_pages` on your `redirect_uri`. * **The account.** `GET /v1/accounts` returns every LinkedIn account with `platform: "linkedin"` and `connection_type`: `profile` for a member profile, `organization` for a company Page. In the selection candidates, the login's profile list, and the assign response, `platform_account_id` is LinkedIn's URN: `urn:li:person:...` for a member profile, `urn:li:organization:...` for a company Page. * **More than 1 LinkedIn account per brand.** A brand can hold more than 1 LinkedIn account, for example a member profile and the company Pages that member administers. Every other platform holds 1 account per brand. Each LinkedIn account belongs to 1 brand. * **Reconnect.** Send the same connect request with `"platform": "linkedin"` and the account's `brand_id`, and choose the same profile or Page if asked. A reconnect that grants every scope the account's login holds renews that login: each other LinkedIn account with the same `login_id` that is `token_expired` returns to `active` too. A reconnect that grants fewer scopes than the login holds renews only the account you reconnect, and removes its `login_id`. For example, a `"profile"` or `"organization"` reconnect of an account whose login was authorized without `connection_type`, or a reconnect with a shorter `permissions` list. See [Reconnect an account](/guides/connect-accounts#reconnect-an-account). * **Token lifetime.** LinkedIn access tokens last 60 days and cannot be refreshed. When LinkedIn reports that the token is invalid, expired, or revoked, the call returns `401 platform.linkedin.auth` and the account is marked `token_expired`. Every later call that acts on the account returns `403 account.reconnection_required` until you reconnect it. A `401` that does not name the token returns `401 platform.linkedin.unauthorized` and the account stays usable: see [Account status](/guides/connect-accounts#account-status). * **Disconnect.** LinkedIn offers apps no way to end an authorization. Disconnecting an account removes it from SocialAPI, and the person removes SocialAPI's app in their LinkedIn settings. `POST /v1/accounts/connect` and `POST /v1/invites` return `400 validation.platform_invalid` for `platform: "linkedin_page"`. ## Connect more profiles and Pages from a login A login is one LinkedIn member's authorization. Every LinkedIn account connected through it carries the same `login_id` in `GET /v1/accounts`. Use it to connect the member profile or another company Page of that member to any of your brands, with no new consent screen. | Method | Path | Description | | - | - | - | | `GET` | `/v1/platforms/linkedin/logins/{loginId}/profiles` | List the member profile and the company Pages the login can connect, with their brand | | `POST` | `/v1/platforms/linkedin/logins/{loginId}/profiles/{profileId}/assign` | Connect one of them to a brand | `GET /v1/platforms/linkedin/logins/{loginId}/profiles` includes company Pages the member started administering after the authorization. A login authorized with `connection_type: "profile"` lists no company Page. The list leaves out the member profile when the login cannot publish as the member: a login authorized with `"organization"`, or with a `permissions` list that leaves out `posts.write`. ```bash theme={null} curl https://api.social-api.ai/v1/platforms/linkedin/logins/cred_01J9K4R5M6N7P8V2K0W1J3QX/profiles \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "login_id": "cred_01J9K4R5M6N7P8V2K0W1J3QX", "login_name": "Jane Doe", "profiles": [ { "platform_account_id": "urn:li:person:8fA3kLm2Qx", "connection_type": "profile", "name": "Jane Doe", "username": "janedoe", "picture_url": "https://media.licdn.com/dms/image/v2/jane-doe.jpg", "assigned_brand_id": "1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f", "assigned_brand_name": "Jane Doe", "assigned_account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "assignable": false }, { "platform_account_id": "urn:li:organization:12345678", "connection_type": "organization", "name": "Acme Corp", "username": "acme-corp", "assignable": true } ], "count": 2 } ``` | Field | Description | | - | - | | `login_id`, `login_name` | The login, and the name of the LinkedIn member who authorized it | | `count` | The number of items in `profiles` | | `profiles[].platform_account_id` | LinkedIn's URN for the member profile or the company Page. Use it as `{profileId}` to assign it | | `profiles[].connection_type` | `profile` for the member profile, `organization` for a company Page | | `profiles[].name`, `username`, `picture_url` | The name, LinkedIn vanity name, and picture. `username` and `picture_url` are absent when LinkedIn returns none | | `profiles[].assignable` | `true` when none of your brands holds the account. `false` when a brand holds it: `assigned_brand_id`, `assigned_brand_name`, and `assigned_account_id` then name the brand and its account | `POST /v1/platforms/linkedin/logins/{loginId}/profiles/{profileId}/assign` connects one of them to a brand. `{profileId}` is the `platform_account_id`, sent as it is or URL-encoded. ```bash theme={null} curl -X POST "https://api.social-api.ai/v1/platforms/linkedin/logins/cred_01J9K4R5M6N7P8V2K0W1J3QX/profiles/urn%3Ali%3Aorganization%3A12345678/assign" \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{"brand_id": "1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f"}' ``` Response (HTTP 201): ```json theme={null} { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1K", "brand_id": "1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f", "platform_account_id": "urn:li:organization:12345678" } ``` The new account is usable at once, and SocialAPI sends `account.connected`. The rules: * **One brand per LinkedIn account.** Assigning an account that another of your brands holds returns `400 validation.profile_already_assigned`, with `meta.brand_id` naming that brand. Assigning it to the brand that already holds it returns HTTP 201 with the same `account_id`. * **The login must still reach the account.** A company Page the login's member does not administer, any company Page when the login was authorized with `connection_type: "profile"`, or a member profile other than that member's own, returns `400 validation.profile_not_granted`, with the URN in `meta.platform_account_id`. * **The brand must be yours.** A missing `brand_id` returns `400 validation.field_required`. A brand that is not yours, or that is outside your [brand-restricted key](/guides/scoped-keys)'s brands, returns `404 brand.not_found`. * **The login must be yours.** An unknown `{loginId}` returns `404 resource.not_found` on both endpoints. * **The request must be well formed.** A body that is not valid JSON returns `400 validation.body_invalid`, and a `{profileId}` with broken URL encoding returns `400 validation.invalid_profile_id`. * **The login must still be authorized.** When LinkedIn refuses the login, both endpoints return `401 platform.linkedin.auth` or `401 platform.linkedin.unauthorized`: reconnect an account of that member with every scope the login holds. LinkedIn's own rate limit returns `429 platform.linkedin.rate_limit`, and other refusals by LinkedIn return a [platform error](/guides/errors#platform-errors). These errors do not change the status of any account. * **A login ends with its last account.** Disconnecting the last LinkedIn account on a login removes the login: both endpoints then return `404 resource.not_found` for its `login_id`. * **Cost.** Listing and assigning cost no credits. * **Scopes.** Listing needs the `accounts:read` scope, and assigning needs `accounts:manage`. * **AI agents.** No MCP tool covers the login endpoints. Call them through the REST API. A LinkedIn account has no `login_id` in two cases: * **The connect granted fewer scopes than the member's login holds.** The login holds the scopes of the connect that gave that member's other LinkedIn accounts their `login_id`. For example, a `"profile"` connect when the login was authorized without `connection_type`, an `"organization"` connect when it was authorized with `"profile"`, or a shorter `permissions` list. Use the `login_id` of that member's other LinkedIn accounts instead, or reconnect the account with the same `connection_type` and `permissions` as the connect that authorized the login. * **The account was connected before 2026-11-05.** Reconnect it to get a `login_id`. The first case applies to that reconnect too. See the [changelog](/changelog). ## Insights `GET /v1/platforms/linkedin/insights?account_id={id}` returns the follower count and per-post counts for a company Page or a member profile. `kind` tells them apart: `member` for a member profile, `organization` for a company Page. On a Page, `posts` covers the Page's 50 most recent posts. * On a member profile, `posts` covers the 5 most recent posts published to the profile through SocialAPI. It is an empty array until you publish one, and `follower_count` is returned either way. * Each member profile post returns `impressions`, `unique_views`, `likes`, `comments`, and `shares`. `clicks` and `engagement_rate` are always 0 for a member profile. * A count LinkedIn cannot return for a post is 0. * The route requires the `analytics.read` [permission](/guides/permissions). ## Webhook events | Event | When it fires | | - | - | | `account.connected` | The account was connected, through the connect flow or the assign endpoint | | `account.updated` | The account lost [permissions](/guides/permissions) you did not remove yourself (`change` is `permissions_reduced`) | | `post.published`, `post.partial`, `post.failed` | A post to this account finished: see [Delivery](/guides/delivery) | | `account.disconnected` | Company Page only: LinkedIn refused a read of the Page's posts because the login needs renewing (`reason` is `posts_sync_auth_failed`). Reconnect the account | | `mention.received` | Company Page only: you listed mentions with `GET /v1/accounts/{id}/mentions` and the call returned at least one item. See [Mentions you list](/guides/webhook-events#mentions-you-list) | Every event is described in [Webhook events](/guides/webhook-events). ## Limits | Limit | Value | | - | - | | Post text | 3,000 characters | | Multi-image post | 2 to 20 images | | Posts | 25 per hour and 50 per 24 hours for each account: see [Posting velocity caps](/guides/rate-limits#posting-velocity-caps) | | LinkedIn's own rate limit | `429 platform.linkedin.rate_limit`. Credit rules: see [when a credit comes back](/guides/plans#when-a-credit-comes-back) | ## Scopes SocialAPI's LinkedIn app requests these scopes at connect. You apply for no app review. Each scope belongs to the account [permissions](/guides/permissions) in the last column, and the consent screen asks for the scopes of the permissions you selected. A connect without `connection_type` requests the scopes of both tables, `"profile"` the member profile table only, and `"organization"` the company Page table only. Member profile scopes: | Scope | Used for | Permissions | | - | - | - | | `r_basicprofile` | Identify the member: name, picture, and ID | Every account | | `w_member_social` | Publish posts as the member | `posts.write` | | `w_member_social_feed` | Reply to, like, and delete comments as the member | `comments.write` | | `r_member_postAnalytics`, `r_member_profileAnalytics` | Post counts and the follower count | `analytics.read` | Company Page scopes: | Scope | Used for | Permissions | | - | - | - | | `r_basicprofile`, `rw_organization_admin` | Identify the member and list the Pages they administer | Every account | | `r_organization_social` | Read the Page's posts | `posts.read` | | `w_organization_social` | Publish posts as the Page | `posts.write` | | `r_organization_social_feed` | Read comments on the Page's posts | `comments.read` | | `w_organization_social_feed` | Reply to, like, and delete comments as the Page | `comments.write` | | `r_organization_followers` | Follower count and post statistics | `analytics.read` | ## Related * [LinkedIn publishing](/platforms/linkedin/publishing): fields, media rules, text formatting, and quirks for publishing * [Connect accounts](/guides/connect-accounts): the shared connect flow and the profile or Page choice * [Platforms](/platforms/overview): the capability matrix * [Errors](/guides/errors): every error code # LinkedIn publishing Source: https://docs.social-api.ai/platforms/linkedin/publishing Full reference for publishing LinkedIn posts via SocialAPI, including images, videos, multi-image posts, link posts, text formatting, and visibility. ## 1. Overview LinkedIn posts publish as the connected member profile (`connection_type: "profile"`) or as the connected company Page (`connection_type: "organization"`). Both are `linkedin` accounts and follow the publishing rules on this page, with one difference when publishing: `visibility: "connections_only"` on a company Page target returns `400 publishing.validation_failed` on the field `visibility`. Listing posts differs too: see [Retrieve posts](#6-retrieve-posts). | Field | Value | | - | - | | Platform slug | `linkedin` (member profile and company Page) | | Auth type | OAuth 2.0 (LinkedIn) | | API | LinkedIn Posts API | | Create post | Yes | | Update post | No (`409 post.state_invalid` on a published post) | | Delete post | Yes | | Schedule | Yes (deferred publish via `scheduled_at`) | | First comment | No (`first_comment` is ignored) | | Posting caps | 25 posts per hour and 50 per 24 hours for each account: see [Posting velocity caps](/guides/rate-limits#posting-velocity-caps) | *** ## 2. Supported media | Content type | How to trigger | Notes | | - | - | - | | Text only | No `media` | `text` is the post commentary | | Single image | One image in `media` | JPEG, PNG, or GIF | | Single video | One video in `media` | MP4 | | Multi-image | 2 to 20 images in `media` | Images only | | Link post | `platform_data.article_url` | The first attached image becomes the link thumbnail. Other media is ignored | A post cannot mix a video with images. A `media_id` video among 2 or more `media` items returns `400 publishing.validation_failed`. A `platform_attachment_id` video among 2 or more items fails the target at publish time with `400 validation.media_type`. A video sent by `url` among 2 or more items is refused by LinkedIn at upload. **File limits for media you upload to SocialAPI** (`source_type: "media_id"`): images at most 10 MB. Videos at most 500 MB, 3 seconds to 30 minutes long, and at most 30 frames per second. Text holds at most 3,000 characters. SocialAPI checks these limits before it publishes, and a violation returns `400 publishing.validation_failed`. > [!WARNING] > > Attach media through the `media` array. Each item sets a `source_type`: `media_id` for a file you uploaded to SocialAPI, `url` for a public `http(s)` URL SocialAPI fetches at publish time, or `platform_attachment_id` for an asset URN you created through LinkedIn yourself. A multi-image post can mix all three. To upload first, see [Media](/guides/media). *** ## 3. Create post Use `POST /v1/posts` with `targets` pointing at a LinkedIn account and `publish_now: true`. The `text` field becomes the post commentary. The platform comes from the account, so a target has no `platform` field. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "Big news from our team! #hiring", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "visibility": "public", "publish_now": true, "platform_data": { "linkedin": { "alt_text": "The team at our spring offsite", "disable_reshare": false } } }' ``` Response (HTTP 201): ```json theme={null} { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "text": "Big news from our team! #hiring", "status": "publishing", "hidden": false, "retry_count": 0, "created_at": "2026-10-13T09:00:00Z", "updated_at": "2026-10-13T09:00:00Z", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "linkedin", "status": "publishing" } ] } ``` ### Visibility `visibility` sets who sees the post. Send it on the post, or on a target to override the post. When both set it, the target's value wins. | Value | LinkedIn audience | | - | - | | `public` (default) | Anyone on or off LinkedIn | | `connections_only` | The member's connections. Member profile targets only: a company Page target returns `400 publishing.validation_failed` on the field `visibility` | | `logged_in` | Any logged-in LinkedIn member | Any other value fails validation on the field `visibility`. ### Text formatting Write `text` as plain text. Characters LinkedIn reserves for formatting (`| { } @ [ ] ( ) < > # \ * _ ~`) are escaped for you, so they appear on LinkedIn exactly as typed. Text you already escaped with a backslash (for example `\(`) is published unchanged. | To publish | Write | | - | - | | A hashtag | `#word` at the start of a word, for example `#hiring` | | A mention of a member | `@[Name](urn:li:person:...)` | | A mention of a company | `@[Name](urn:li:organization:...)` | A `#` inside a word (`C#`, `page#section`) and an `@` outside the mention syntax (an email address) are published as plain text. ### Link post with a call to action Set `article_url` to publish a link post. Add `cta_label` and `cta_url` for a call-to-action button. An attached image becomes the link thumbnail. LinkedIn builds no preview of its own, so set `article_title` and `article_description` too. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Read our latest product update.", "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "visibility": "public", "platform_data": { "article_url": "https://example.com/update", "article_title": "Product update", "article_description": "What shipped this month", "cta_label": "LEARN_MORE", "cta_url": "https://example.com/update" } }] }' ``` ### Platform data fields Send these keys in `platform_data.linkedin` on the post, which applies them to every LinkedIn target, member profile and company Page alike. To set them for one target, send them flat in that target's `platform_data` (`targets[].platform_data`, with no `linkedin` key). When both set the same key, the target's value wins. | Key | Type | Applies to | Notes | | - | - | - | - | | `article_url` | string | link post | An absolute `http` or `https` URL. Publishes a link post | | `article_title` | string | link post | Title on the link card | | `article_description` | string | link post | Description on the link card | | `cta_label` | string | link post | One of `LEARN_MORE`, `SIGN_UP`, `DOWNLOAD`, `REGISTER`, `SUBSCRIBE`, `APPLY`, `REQUEST_DEMO`, `JOIN`, `ATTEND`. Requires `article_url` | | `cta_url` | string | link post | Destination of the call-to-action button. Requires `article_url` | | `alt_text` | string | single image | Alt text for the image | | `alt_texts` | array of strings | multi-image | Alt text per image, matched to `media` by position | | `media_title` | string | single image, single video | Title of the attached file | | `disable_reshare` | boolean | every post | When `true`, others cannot reshare the post | SocialAPI forwards any additional keys in `platform_data.linkedin` that the table above lists, and no others. LinkedIn has no other publish options that SocialAPI passes through: an unlisted key is ignored without a warning. A malformed `article_url`, a `cta_label` outside the list, or a `cta_label` or `cta_url` without `article_url` fails validation on that field. ### Scheduling Set `scheduled_at` to an ISO 8601 timestamp (UTC) instead of `publish_now`. SocialAPI holds the post and publishes it at that time. ```json theme={null} { "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "Scheduled announcement", "scheduled_at": "2026-11-15T10:00:00Z" } ``` *** ## 4. Update post LinkedIn posts cannot be edited through SocialAPI once they are published. `PATCH /v1/posts/{pid}` edits only a post in `draft`, `scheduled`, or `failed` status, and returns `409 post.state_invalid` for a published post. To correct a published post, delete it and publish a new one. *** ## 5. Delete post `DELETE /v1/posts/{pid}` deletes the post from LinkedIn when its LinkedIn target is `published`. A target that is not published is removed from SocialAPI only. See [Delete a post](/guides/delivery#delete-a-post). ```bash theme={null} curl -X DELETE https://api.social-api.ai/v1/posts/7c9e6679-7425-40de-944b-e07fc1f90ae7 \ -H "Authorization: Bearer $SOCAPI_KEY" ``` *** ## 6. Retrieve posts `GET /v1/posts?platform=linkedin&account_ids={id}` lists the posts of a member profile or a company Page. For a member profile, the list holds the posts you published through SocialAPI. LinkedIn gives apps no read of a member's own posts, so a post published on LinkedIn directly never appears, and `GET /v1/posts` returns no metrics for these posts. Read counts for the 5 most recent ones from [LinkedIn insights](/platforms/linkedin#insights). For a company Page, the list also holds the Page's posts made outside SocialAPI. *** ## 7. Quirks, errors, and recovery ### The post ID is returned for you SocialAPI returns the LinkedIn post ID as `platform_post_id` in the post body and in `post.published` events, with a `permalink`. You do nothing to retrieve it. ### Video processing LinkedIn processes a video after upload. A video LinkedIn cannot process fails the target with `422 platform.linkedin.processing_failed`. An upload or processing step that does not finish in time fails it with `504 platform.linkedin.upload_timeout` or `504 platform.linkedin.processing_timeout`. Upload a smaller or re-encoded file, then retry. ### Link posts need their own preview LinkedIn builds no preview for a link post. Set `article_title`, `article_description`, and attach an image. A video, or more than one media item, on a link post is ignored apart from the first image, and validation returns a warning on the field `media`. ### Errors on a failed target A publish that fails after the request returned puts a structured error on the target, in `targets[].error`, with its `code`. Every code is in [Errors](/guides/errors). * `401 platform.linkedin.auth`: LinkedIn reported that the access token is invalid, expired, or revoked. LinkedIn tokens last 60 days and cannot be refreshed. The account is marked `token_expired`, and every later call that acts on the account returns `403 account.reconnection_required` until you reconnect it. * `401 platform.linkedin.unauthorized`: LinkedIn refused the call as unauthorized without naming the token. The account stays usable, with `status: reconnect_recommended`. Reconnect it if the error repeats. * `403 platform.linkedin.access_denied`: LinkedIn refused the action for this account. Reconnecting does not change it. Read `message`. * `429 platform.linkedin.rate_limit`: LinkedIn's own limit. Wait, then retry. ### Recovery * **Rate limit.** Wait for the seconds in `Retry-After`, or at least 60 seconds when it is absent, then send `POST /v1/posts/{pid}/retry`. See [Delivery](/guides/delivery). * **Auth error.** Reconnect the LinkedIn account through the OAuth flow. * **Media failure.** Replace the file, create the post again, or edit the failed post and retry it. *** ## 8. Full worked example This example publishes an image post with a mention and a hashtag, then reads the result. **Step 1: Upload media** ```bash theme={null} curl "https://api.social-api.ai/v1/media/upload-url?filename=team.jpg&media_type=image/jpeg" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Upload the file to the returned `upload_url`, then call `POST /v1/media/{media_id}/verify`. The steps and the response are in [Media](/guides/media). The result is a `media_id`, here `f47ac10b-58cc-4372-a567-0e02b2c3d479`. **Step 2: Publish the post** ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "Welcome @[Acme Corp](urn:li:organization:12345678) to the team. #hiring", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "visibility": "public", "publish_now": true }' ``` Response (HTTP 201): the post with `status: "publishing"`, as in [Create post](#3-create-post). **Step 3: Check the status** ```bash theme={null} curl https://api.social-api.ai/v1/posts/7c9e6679-7425-40de-944b-e07fc1f90ae7 \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200) once published: ```json theme={null} { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "text": "Welcome @[Acme Corp](urn:li:organization:12345678) to the team. #hiring", "status": "published", "hidden": false, "retry_count": 0, "published_at": "2026-10-13T09:00:06Z", "created_at": "2026-10-13T09:00:00Z", "updated_at": "2026-10-13T09:00:06Z", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "linkedin", "status": "published", "platform_post_id": "urn:li:share:7251234567890123456", "permalink": "https://www.linkedin.com/feed/update/urn:li:share:7251234567890123456/", "published_at": "2026-10-13T09:00:06Z" } ] } ``` *** ## 9. Required OAuth scopes SocialAPI's LinkedIn app requests these scopes during the OAuth flow: `w_member_social` for member profiles, the other two for company Pages. You configure nothing. The full list is in [LinkedIn scopes](/platforms/linkedin#scopes). | Scope | Purpose | | - | - | | `w_member_social` | Publish posts as the member | | `w_organization_social` | Publish posts as the company Page | | `r_organization_social` | Read the company Page's posts | The scopes a scoped API key needs, the credit cost of a post, and the MCP tools that publish are on [Publishing](/guides/publishing). # Supported platforms Source: https://docs.social-api.ai/platforms/overview Which operations each SocialAPI platform supports for the inbox, Automations, publishing, analytics, and webhook events, in one matrix per feature area. SocialAPI connects Instagram, Facebook, Threads, Google Business Profile, TikTok, LinkedIn, X / Twitter, Telegram, WhatsApp, Zalo, Bluesky, Pinterest, YouTube, and App Store Connect through one API. Trustpilot is not available and cannot be connected: see [Trustpilot](/platforms/trustpilot). Most platforms connect through SocialAPI's own platform app, so you register nothing with the platform. Telegram connects with a bot token from @BotFather, Bluesky with an app password, App Store Connect with your App Store Connect API key, and X / Twitter through an X developer app that you create. See [Platform apps](/guides/platform-credentials) for who owns the app for each platform. Every platform and every operation is available on every plan, with two exceptions that need a paid plan: WhatsApp template messages (`403 whatsapp.paid_plan_required` on the Free plan) and visual analysis in analytics exports (`include_vision`, ignored on the Free plan). Plans otherwise differ only in resource limits: see [Plans](/guides/plans). Each platform page holds a Capabilities table with the condition behind every cell below. When you call an operation a platform does not support, SocialAPI returns `501 resource.not_supported`. That is a platform limit, not a mistake in your integration, so handle the 501 instead of branching per platform. An Automation on an account that does not support Automations returns `422 automation.platform_unsupported`, and a reviews call on a platform without reviews returns `422 account.platform_unsupported`. The cells have 5 values: | Value | Meaning | | - | - | | Yes | Supported | | No | Not supported. The call returns an error, or the feature does not exist on the platform | | Partial | Supported with a limit on what it covers | | Conditional | Supported only when a condition holds, such as an action on your own comment | | Beta | Supported for accounts with beta access, on the connection types named on the platform page | The condition behind every Partial, Conditional, and Beta cell is on the platform page. Each comment returned by the inbox also carries a `capabilities` object that tells you which actions the platform allows for it: see [Comments](/guides/comments). ## Inbox Comments, replies, moderation, private replies, direct messages, mentions, and reviews. Guides: [Comments](/guides/comments), [Direct messages](/guides/direct-messages), [Mentions](/guides/mentions), [Reviews](/guides/reviews). | Platform | Comments | Reply | Hide | Delete | Like | Private reply | DMs | Mentions | Reviews | | - | - | - | - | - | - | - | - | - | - | | [Instagram](/platforms/instagram) | Yes | Yes | Yes | Yes | Partial | Yes | Yes | Partial | No | | [Facebook](/platforms/facebook) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Partial | No | | [Threads](/platforms/threads) | Yes | Yes | Yes | Conditional | No | No | No | Yes | No | | [Google Business Profile](/platforms/google) | No | No | No | No | No | No | No | No | Yes | | [TikTok](/platforms/tiktok) | Yes | Yes | Conditional | Conditional | Yes | No | No | Conditional | No | | [LinkedIn](/platforms/linkedin) | Conditional | Conditional | No | Conditional | Conditional | No | No | Conditional | No | | [X / Twitter](/platforms/twitter) | Yes | Yes | Yes | Conditional | No | No | Yes | Yes | No | | [Telegram](/platforms/telegram) | No | No | No | No | No | No | Conditional | No | No | | [WhatsApp](/platforms/whatsapp) | No | No | No | No | No | No | Yes | No | No | | [Zalo](/platforms/zalo) | No | No | No | No | No | No | Yes | No | No | | [Bluesky](/platforms/bluesky) | Yes | Yes | Yes | Conditional | No | No | Partial | Yes | No | | [Pinterest](/platforms/pinterest) | No | No | No | No | No | No | No | No | No | | [YouTube](/platforms/youtube) | Yes | Yes | Yes | Conditional | No | No | No | No | No | | [App Store Connect](/platforms/app-store-connect) | No | No | No | No | No | No | No | No | Yes | | [Trustpilot](/platforms/trustpilot) | No | No | No | No | No | No | No | No | No | A TikTok account connected with a [creator connection](/platforms/tiktok#creator-connection) has no comments and no mentions. LinkedIn comments and mentions work on a company Page (`connection_type: "organization"`). A member profile (`connection_type: "profile"`) can reply to, like, and delete comments but cannot list them, and it has no mentions: see [LinkedIn](/platforms/linkedin#capabilities). ## Automations [Automations](/guides/automations) run on Instagram and Facebook accounts. | Platform | Comment trigger | DM trigger | Postback buttons | Follow gate | | - | - | - | - | - | | [Instagram](/platforms/instagram) | Yes | Yes | Yes | Yes | | [Facebook](/platforms/facebook) | Yes | Yes | Partial | No | | [Threads](/platforms/threads) | No | No | No | No | | [Google Business Profile](/platforms/google) | No | No | No | No | | [TikTok](/platforms/tiktok) | No | No | No | No | | [LinkedIn](/platforms/linkedin) | No | No | No | No | | [X / Twitter](/platforms/twitter) | No | No | No | No | | [Telegram](/platforms/telegram) | No | No | No | No | | [WhatsApp](/platforms/whatsapp) | No | No | No | No | | [Zalo](/platforms/zalo) | No | No | No | No | | [Bluesky](/platforms/bluesky) | No | No | No | No | | [Pinterest](/platforms/pinterest) | No | No | No | No | | [YouTube](/platforms/youtube) | No | No | No | No | | [App Store Connect](/platforms/app-store-connect) | No | No | No | No | | [Trustpilot](/platforms/trustpilot) | No | No | No | No | ## Publishing Publish, schedule, update, and delete posts, and add a first comment. Guides: [Publishing](/guides/publishing), [Scheduling](/guides/scheduling), [Media](/guides/media). The media columns show the types a post can carry. Counts, sizes, and formats are on each platform's publishing page. | Platform | Publish | Text only | Single image | Video | Multiple images | Schedule | Update | Delete | First comment | | - | - | - | - | - | - | - | - | - | - | | [Instagram](/platforms/instagram) | Yes | No | Yes | Yes | Yes | Yes | No | Partial | Yes | | [Facebook](/platforms/facebook) | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes | Yes | | [Threads](/platforms/threads) | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes | No | | [Google Business Profile](/platforms/google) | Yes | Yes | Yes | No | No | Yes | No | Yes | No | | [TikTok](/platforms/tiktok) | Yes | No | No | Yes | Yes | Yes | No | No | No | | [LinkedIn](/platforms/linkedin) | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes | No | | [X / Twitter](/platforms/twitter) | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes | No | | [Telegram](/platforms/telegram) | No | No | No | No | No | No | No | No | No | | [WhatsApp](/platforms/whatsapp) | No | No | No | No | No | No | No | No | No | | [Zalo](/platforms/zalo) | Yes | Yes | No | No | No | Yes | No | Yes | No | | [Bluesky](/platforms/bluesky) | Yes | Yes | Yes | No | Yes | Yes | No | Yes | No | | [Pinterest](/platforms/pinterest) | Yes | No | Yes | No | No | Yes | No | Yes | No | | [YouTube](/platforms/youtube) | Yes | No | No | Yes | No | Yes | Yes | Yes | Yes | | [App Store Connect](/platforms/app-store-connect) | No | No | No | No | No | No | No | No | No | | [Trustpilot](/platforms/trustpilot) | No | No | No | No | No | No | No | No | No | Update and Delete describe a post that is already published. ## Analytics Live post metrics, a live account summary, and the history SocialAPI stores. Guide: [Analytics](/guides/analytics). | Platform | Post metrics | Live account summary | Stored history | | - | - | - | - | | [Instagram](/platforms/instagram) | Yes | Yes | Yes | | [Facebook](/platforms/facebook) | Yes | Yes | Yes | | [Threads](/platforms/threads) | Yes | Yes | Yes | | [Google Business Profile](/platforms/google) | No | Yes | No | | [TikTok](/platforms/tiktok) | Yes | Yes | Yes | | [LinkedIn](/platforms/linkedin) | No | Conditional | Partial | | [X / Twitter](/platforms/twitter) | Yes | No | Partial | | [Telegram](/platforms/telegram) | No | No | No | | [WhatsApp](/platforms/whatsapp) | No | No | No | | [Zalo](/platforms/zalo) | Partial | No | Yes | | [Bluesky](/platforms/bluesky) | Yes | No | Yes | | [Pinterest](/platforms/pinterest) | No | Yes | No | | [YouTube](/platforms/youtube) | Yes | Yes | Yes | | [App Store Connect](/platforms/app-store-connect) | No | No | No | | [Trustpilot](/platforms/trustpilot) | No | No | No | LinkedIn stored post history covers company Page posts. Member profile posts have none: see [LinkedIn](/platforms/linkedin#capabilities). ## Webhook events Which event groups each platform emits. The events, their payloads, and the platforms behind each one are on [Webhook events](/guides/webhook-events), the canonical home of this list. Setup is in [Webhooks](/guides/webhooks). A TikTok [creator connection](/platforms/tiktok#creator-connection) emits no comment or mention events. | Platform | Comment events | Mention events | DM events | Review events | Post events | Account events | | - | - | - | - | - | - | - | | [Instagram](/platforms/instagram) | Yes | Yes | Yes | No | Yes | Yes | | [Facebook](/platforms/facebook) | Yes | Yes | Yes | No | Yes | Yes | | [Threads](/platforms/threads) | Yes | Yes | No | No | Yes | Yes | | [Google Business Profile](/platforms/google) | No | No | No | Yes | Yes | Yes | | [TikTok](/platforms/tiktok) | Yes | Partial | No | No | Yes | Yes | | [LinkedIn](/platforms/linkedin) | No | Partial | No | No | Yes | Yes | | [X / Twitter](/platforms/twitter) | No | Partial | No | No | Yes | Yes | | [Telegram](/platforms/telegram) | No | No | Yes | No | No | Yes | | [WhatsApp](/platforms/whatsapp) | No | No | Yes | No | No | Yes | | [Zalo](/platforms/zalo) | No | No | Partial | No | Yes | Yes | | [Bluesky](/platforms/bluesky) | No | Partial | No | No | Yes | Yes | | [Pinterest](/platforms/pinterest) | No | No | No | No | Yes | Yes | | [YouTube](/platforms/youtube) | No | No | No | No | Yes | Yes | | [App Store Connect](/platforms/app-store-connect) | No | No | No | No | No | Yes | | [Trustpilot](/platforms/trustpilot) | No | No | No | No | No | No | ## Related * [Platform apps](/guides/platform-credentials): who owns the app behind each connection * [Connect accounts](/guides/connect-accounts): the connect flow and the account object * [Plans](/guides/plans): resource limits and storage per plan * [Errors](/guides/errors): every error code # Pinterest API Source: https://docs.social-api.ai/platforms/pinterest Connect Pinterest accounts to publish Pins to boards, manage boards, list Pins, and read profile counts through one API. The Pinterest API in SocialAPI connects a Pinterest account through Pinterest's authorization screen. It publishes Pins to boards, creates, renames, and deletes boards, lists Pins, and reads profile counts. Pinterest is a publishing surface: it exposes no comments, DMs, mentions, or reviews to apps. SocialAPI's own Pinterest app connects the account, so you register nothing: see [Platform apps](/guides/platform-credentials). ## At a glance | Field | Value | | - | - | | Platform slug | `pinterest` | | Auth | OAuth 2.0 | | Platform API | Pinterest API v5 | | Status | Available | ## Capabilities An unsupported call returns `501 resource.not_supported`. | Feature | Status | Condition | | - | - | - | | Publish a Pin | Yes | One image and a `board_id` on the target. See [Pinterest publishing](/platforms/pinterest/publishing) | | Schedule a Pin | Yes | `scheduled_at` on the post | | First comment on a Pin | No | `first_comment` is ignored | | Update a published Pin | No | `PATCH /v1/posts/{pid}` returns `409 post.state_invalid` for a published post. Pinterest's edit call is in beta and Pin media never changes | | Delete a Pin | Yes | | | List Pins | Yes | Read from Pinterest on every call. See [List Pins](#list-pins) | | Boards: list, create, rename, delete | Yes | See [Pinterest boards](/platforms/pinterest/boards) | | Account summary | Yes | See [Account summary](#account-summary) | | Post metrics | No | Pinterest reports no engagement counts on the Pin list. `GET /v1/posts/{pid}/metrics` returns HTTP 200, and the Pinterest target carries `error.code: resource.not_supported` | | Comments, DMs, mentions | No | Pinterest has no public API for them. `501 resource.not_supported` | | Reviews | No | Pinterest has no reviews. `GET /v1/inbox/reviews/{account_id}` returns `422 account.platform_unsupported` | | Automations | No | `422 automation.platform_unsupported` | | Analytics | Partial | Live account summary only; SocialAPI stores no account history, and Pins have no post metrics. See [Analytics](/guides/analytics) | | Webhook events | Partial | `account.connected` and the `post.*` events. Pinterest sends no events of its own | The cross-platform matrix is on [Platforms](/platforms/overview). ## Connect an account Connect with `POST /v1/accounts/connect` and `"platform": "pinterest"`. The shared flow, request fields, and result are in [Connect accounts](/guides/connect-accounts), and the redirect parameters are in [Redirect parameters](/guides/connect-accounts#redirect-parameters). ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/connect \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "pinterest", "redirect_uri": "https://app.example.com/oauth/callback", "state": "session_abc123" }' ``` Response (HTTP 202): ```json theme={null} { "auth_url": "https://www.pinterest.com/oauth/?client_id=1234567&response_type=code&state=4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2", "state": "4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2", "message": "Open this link to connect your pinterest account. Once you've authorized, the account will be connected automatically." } ``` Pinterest-specific rules: * **Token lifetime.** Pinterest access tokens last 30 days and refresh tokens last 60 days, and each use of a refresh token extends it. SocialAPI renews the access token before it expires, so a connected account keeps working without a reconnect. * **Reconnect.** When Pinterest reports that authentication failed, the first call that fails returns `401 platform.pinterest.auth` and the account is marked `token_expired`. Every later call that acts on the account returns `403 account.reconnection_required` until you reconnect it. Any other `401` from Pinterest returns `401 platform.pinterest.unauthorized` and leaves the account working: reconnect only if it persists. See [Reconnect an account](/guides/connect-accounts#reconnect-an-account). * **Disconnect.** Pinterest offers apps no way to end an authorization. Disconnecting an account removes it from SocialAPI, and the person removes SocialAPI's app in their Pinterest settings. ## Boards and Pins are read live SocialAPI reads boards and Pins from Pinterest on every request and stores neither, because Pinterest's developer terms forbid storing information read through its API. A board created in the Pinterest app a moment ago appears on the next call, and a call fails when Pinterest is unavailable instead of returning an older list. ## List Pins `GET /v1/platforms/pinterest/accounts/{id}/pins` returns the account's Pins in the order Pinterest returns them. `limit` is optional, defaults to 50, and is at most 50. The response holds only the first 50 Pins at most, with no cursor: you cannot page to older Pins. It needs the `accounts:read` scope. ```bash theme={null} curl "https://api.social-api.ai/v1/platforms/pinterest/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/pins?limit=20" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "id": "813744", "title": "One pan, forty minutes, no marinade.", "board_id": "8828", "permalink": "https://www.pinterest.com/pin/813744/", "media_url": "https://i.pinimg.com/600x/ab/cd/ef.jpg", "created_at": "2026-08-01T12:00:00Z" } ] } ``` `title` carries the Pin's description, not its separate Pinterest title. ## Account summary `GET /v1/platforms/pinterest/accounts/{id}/summary` reads the account's public profile counts from Pinterest at the time of the call. It needs the `accounts:read` scope. A count Pinterest does not report is `0`. ```bash theme={null} curl https://api.social-api.ai/v1/platforms/pinterest/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/summary \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "username": "boardandco", "profile_image": "https://i.pinimg.com/280x280_RS/ab/cd/ef.jpg", "follower_count": 12400, "pin_count": 847, "board_count": 23, "monthly_views": 1200000 } ``` ## Webhook events Pinterest sends SocialAPI no events. These SocialAPI events still fire for a Pinterest account: | Event | When it fires | | - | - | | `account.connected` | The account was connected | | `account.updated` | The account lost [permissions](/guides/permissions) you did not remove yourself (`change` is `permissions_reduced`) | | `post.published`, `post.partial`, `post.failed` | A post to this account finished: see [Delivery](/guides/delivery) | Every event is described in [Webhook events](/guides/webhook-events). ## Limits | Limit | Value | | - | - | | Image per Pin | 1 | | Pin title | 100 characters | | Pin description | 800 characters | | Alt text | 500 characters | | Destination link | 2,048 characters | | Posts | 1,000 per 24 hours for each board, with no hourly cap: see [Posting velocity caps](/guides/rate-limits#posting-velocity-caps) | | Pins per list call | 50 | | Pinterest's own rate limit | `429 platform.pinterest.rate_limit`. Credit rules: see [when a credit comes back](/guides/plans#when-a-credit-comes-back) | ## Scopes SocialAPI's Pinterest app requests these scopes at connect. You apply for no app review. SocialAPI does not request `boards:read_secret` or `pins:read_secret`. Each scope belongs to the account [permissions](/guides/permissions) in the last column, and the consent screen asks for the scopes of the permissions you selected. | Scope | Used for | Permissions | | - | - | - | | `boards:read` | List boards | `posts.read`, `posts.write` | | `boards:write` | Create, rename, and delete boards. Pinterest also requires it to create and delete Pins | `posts.write` | | `pins:read` | List Pins | `posts.read`, `posts.write` | | `pins:write` | Create and delete Pins | `posts.write` | | `user_accounts:read` | Account summary and the account's identity | Every account | ## Related * [Pinterest publishing](/platforms/pinterest/publishing): fields, media rules, and quirks for publishing Pins * [Pinterest boards](/platforms/pinterest/boards): list, create, rename, and delete boards * [Connect accounts](/guides/connect-accounts): the shared connect flow * [Platforms](/platforms/overview): the capability matrix * [Errors](/guides/errors): every error code # Pinterest boards Source: https://docs.social-api.ai/platforms/pinterest/boards List, create, rename, and delete Pinterest boards through SocialAPI, and use a board ID to publish Pins to it. A Pinterest board is the collection a Pin is saved to. SocialAPI lists, creates, renames, and deletes the boards of a connected Pinterest account, and every Pin you publish names one board. Board operations cost no credits, and SocialAPI reads boards from Pinterest on every call without storing them. ## Endpoints | Method | Path | Description | | - | - | - | | `GET` | `/v1/accounts/{id}/boards` | List the account's boards | | `POST` | `/v1/accounts/{id}/boards` | Create a board | | `PATCH` | `/v1/accounts/{id}/boards/{boardId}` | Change a board's name, description, or privacy | | `DELETE` | `/v1/accounts/{id}/boards/{boardId}` | Delete a board and every Pin on it | `{id}` is the connected Pinterest account's ID. `{boardId}` is either the `id` or the `board_id` of the board. Every endpoint returns `400 account.boards_not_supported` for an account that is not on Pinterest. ## List boards ```bash theme={null} curl https://api.social-api.ai/v1/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/boards \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "id": "sapi_board_01HZ9X3Q4R5M6N7P8V2K0W1J", "board_id": "8828", "name": "Recipes", "privacy": "PUBLIC" } ] } ``` | Field | Description | | - | - | | `id` | SocialAPI's ID for the board, prefixed `sapi_board_` | | `board_id` | Pinterest's own ID for the board | | `name` | The board name | | `privacy` | `PUBLIC`, `PROTECTED`, or `SECRET` | Pass either `id` or `board_id` as `board_id` on a Pinterest target when you publish a Pin: see [Pinterest publishing](/platforms/pinterest/publishing#3-create-post). Listing boards needs the `accounts:read` scope. `GET /v1/accounts/{id}/pages` refuses a Pinterest account with `400 account.use_boards_endpoint`. ## Create a board ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/boards \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Recipes", "description": "Weeknight dinners", "privacy": "PUBLIC" }' ``` | Field | Type | Required | Description | | - | - | - | - | | `name` | string | Yes | The board name. A request without it fails with `400 board.name_required` | | `description` | string | No | The board description | | `privacy` | string | No | `PUBLIC`, `PROTECTED`, or `SECRET`. Pinterest applies its default when you omit it | Response (HTTP 201): the board, in the shape of a list item. Creating a board needs the `accounts:manage` scope. ## Rename a board or change its privacy ```bash theme={null} curl -X PATCH https://api.social-api.ai/v1/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/boards/8828 \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Weeknight recipes", "privacy": "SECRET" }' ``` | Field | Type | Required | Description | | - | - | - | - | | `name` | string | No | The new name | | `description` | string | No | The new description | | `privacy` | string | No | `PUBLIC` or `SECRET` | Send at least one field. A request with none fails with `400 board.update_empty`. Any other `privacy` value, including `PROTECTED`, fails with `400 board.privacy_invalid`: Pinterest cannot set a board to `PROTECTED` after creation, and a board that is `PROTECTED` cannot change its privacy at all. Create a new board instead. The response is the updated board (HTTP 200). This endpoint needs the `accounts:manage` scope. ## Delete a board > [!WARNING] > > Deleting a board permanently deletes every Pin saved to it. Pinterest offers no undo and SocialAPI keeps no copy. ```bash theme={null} curl -X DELETE https://api.social-api.ai/v1/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/boards/8828 \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response: HTTP 204 with no body. A board that no longer exists on Pinterest also returns 204. This endpoint needs the `accounts:manage` scope. ## Limits | Limit | Value | | - | - | | Boards per list call | Up to 100, with no cursor | | Privacy on create | `PUBLIC`, `PROTECTED`, `SECRET` | | Privacy on update | `PUBLIC`, `SECRET` | | Pinterest's own rate limit | `429 platform.pinterest.rate_limit` | ## Errors Every code is in [Errors](/guides/errors). * `400 account.boards_not_supported`: the account is not a Pinterest account. * `400 board.name_required`: a create request has no `name`. * `400 board.update_empty`: an update request sets no field. * `400 board.privacy_invalid`: an update sets `privacy` to anything but `PUBLIC` or `SECRET`. * `401 platform.pinterest.auth`: Pinterest reported that authentication failed. The account is marked `token_expired`, and every later call that acts on the account returns `403 account.reconnection_required` until you reconnect it. * `403 platform.pinterest.403`: Pinterest refused the action for this account. * `404 platform.pinterest.404`: Pinterest reports that the board or Pin does not exist. ## Use from an AI agent | Tool | What it does | | - | - | | `social_api_list_pinterest_boards` | Lists the account's boards. Pass `id` or `board_id` as `board_id` on a Pinterest target of `social_api_create_post` | Create, rename, and delete have no tool. Use the endpoints above. ## Related * [Pinterest publishing](/platforms/pinterest/publishing): publish a Pin to a board * [Pinterest API](/platforms/pinterest): capabilities, limits, and scopes * [Connect accounts](/guides/connect-accounts): connect a Pinterest account * [Errors](/guides/errors): every error code # Pinterest publishing Source: https://docs.social-api.ai/platforms/pinterest/publishing Full reference for publishing Pins via SocialAPI, including the board target, Pin title, description, link, alt text, scheduling, and quirks. ## 1. Overview A Pin needs a destination board and one image. Pinterest has no text-only or link-only post, and no default board: every Pinterest target sets `board_id`. | Field | Value | | - | - | | Platform slug | `pinterest` | | Auth type | OAuth 2.0 | | API | Pinterest API v5 | | Create post | Yes (a Pin) | | Update post | No (`409 post.state_invalid` on a published post) | | Delete post | Yes | | Schedule | Yes (deferred publish via `scheduled_at`) | | First comment | No (`first_comment` is ignored) | | Posting caps | 1,000 posts per 24 hours for each board, with no hourly cap: see [Posting velocity caps](/guides/rate-limits#posting-velocity-caps) | *** ## 2. Supported media | Content type | How to trigger | Notes | | - | - | - | | Image Pin | One image in `media` | The only Pin type SocialAPI publishes | SocialAPI sends the first `media` item to Pinterest as the Pin's image and ignores the rest. A Pin with no image fails validation on the field `media`. SocialAPI does not check the image's type or size in advance: Pinterest rejects a file it does not accept, and the target fails. A video is not supported. **Field limits:** title 100 characters, description 800 characters, alt text 500 characters, and link 2,048 characters. A longer value in `platform_data.pinterest` fails validation with `400 publishing.validation_failed`, and the issue names the field. SocialAPI checks the 800-character limit only on `platform_data.pinterest.description`: a longer `text` is sent as is and Pinterest refuses it. > [!WARNING] > > Attach the image through the `media` array. Each item sets a `source_type`: `media_id` for a file you uploaded to SocialAPI, or `url` for a public `http(s)` URL Pinterest can fetch. A `platform_attachment_id` item returns `422 publishing.media.unsupported_source`. To upload first, see [Media](/guides/media). *** ## 3. Create post Use `POST /v1/posts` with a target that sets `account_id` and `board_id`, and `publish_now: true`. Get `board_id` from [Pinterest boards](/platforms/pinterest/boards#list-boards). ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "One pan, forty minutes, no marinade.", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "board_id": "8828" }], "platform_data": { "pinterest": { "title": "Weeknight roast chicken", "link": "https://example.com/recipes/chicken", "alt_text": "A roast chicken on a sheet pan with lemons" } } }' ``` Response (HTTP 201): ```json theme={null} { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "text": "One pan, forty minutes, no marinade.", "status": "publishing", "hidden": false, "retry_count": 0, "created_at": "2026-10-13T09:00:00Z", "updated_at": "2026-10-13T09:00:00Z", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "page_id": "sapi_board_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "pinterest", "status": "publishing" } ] } ``` ### Target field | Field | Type | Required | Description | | - | - | - | - | | `board_id` | string | Yes | The board to Pin to. Use the `id` or the `board_id` that [List boards](/platforms/pinterest/boards#list-boards) returns. `page_id` is accepted as an alias, but sending both fails with `400 validation.duplicate_board_id` | A target with neither `board_id` nor `page_id` fails with `400 validation.board_id_required`. A `board_id` inside `platform_data` fails with `400 validation.use_board_id`: set it on the target. ### Pin text The post's `text` is the Pin's description. Set the Pin's title in `platform_data.pinterest.title`: the post's top-level `title` field is not used for a Pin. ### Platform data fields Send these keys in `platform_data.pinterest` on the post, which applies them to every Pinterest target. To set them for one target, send them flat in that target's `platform_data` (`targets[].platform_data`, with no `pinterest` key). When both set the same key, the target's value wins. | Key | Type | Required | Notes | | - | - | - | - | | `title` | string, up to 100 characters | No | The Pin title | | `description` | string, up to 800 characters | No | Replaces `text` as the Pin description | | `link` | string, up to 2,048 characters | No | The destination URL. An absolute `http` or `https` URL | | `alt_text` | string, up to 500 characters | No | Alt text for the image | SocialAPI forwards any additional keys in `platform_data.pinterest` that the table above lists, and no others. Pinterest has no other publish options that SocialAPI passes through: an unlisted key is ignored without a warning. ### Scheduling Set `scheduled_at` to an ISO 8601 timestamp (UTC) instead of `publish_now`. SocialAPI holds the post and publishes it at that time. ```json theme={null} { "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "board_id": "8828" }], "scheduled_at": "2026-11-15T10:00:00Z" } ``` *** ## 4. Update post SocialAPI offers no edit of a published Pin. Pinterest's edit call is in beta and not open to every app, and a Pin's image never changes. `PATCH /v1/posts/{pid}` edits only a post in `draft`, `scheduled`, or `failed` status, and returns `409 post.state_invalid` for a published post. To change a Pin, delete it and publish a new one: the new Pin has a new ID. *** ## 5. Delete post `DELETE /v1/posts/{pid}` deletes the Pin from Pinterest when its Pinterest target is `published`. See [Delete a post](/guides/delivery#delete-a-post). Deleting a board deletes every Pin on it: see [Pinterest boards](/platforms/pinterest/boards#delete-a-board). ```bash theme={null} curl -X DELETE https://api.social-api.ai/v1/posts/7c9e6679-7425-40de-944b-e07fc1f90ae7 \ -H "Authorization: Bearer $SOCAPI_KEY" ``` *** ## 6. Retrieve posts `GET /v1/platforms/pinterest/accounts/{id}/pins` returns the account's Pins, read from Pinterest on every call. The response and its fields are in [List Pins](/platforms/pinterest#list-pins). Each published target of a post you created carries `platform_post_id` and `permalink`. Pinterest reports no engagement counts on the Pin list, so a Pin has no metrics. `GET /v1/posts/{pid}/metrics` returns HTTP 200, and the Pinterest target carries `error.code: resource.not_supported`. Account-level counts are in [Analytics](/guides/analytics). *** ## 7. Quirks, errors, and recovery ### A Pin needs a board There is no default board, and a target without `board_id` is refused. When the connected account does not own the board, Pinterest refuses the Pin and the target fails with `403 platform.pinterest.403`. ### Boards and Pins are read live SocialAPI stores no board or Pin. A board you just created appears on the next call. If Pinterest is unavailable, a call fails. ### Errors on a failed target A publish that fails after the request returned puts a structured error on the target, in `targets[].error`, with its `code`. Every code is in [Errors](/guides/errors). * `401 platform.pinterest.auth`: Pinterest reported that authentication failed. The account is marked `token_expired`, and every later call that acts on the account returns `403 account.reconnection_required` until you reconnect it. * `401 platform.pinterest.unauthorized`: Pinterest answered 401 without naming the token. The account keeps working. Reconnect if it persists. * `403 platform.pinterest.403`: Pinterest refused the action for this account or board. Read `message`. * `404 platform.pinterest.404`: Pinterest reports that the board or Pin does not exist. * `429 platform.pinterest.rate_limit`: Pinterest's own limit. Wait, then retry. ### Recovery * **Rate limit.** Wait for the seconds in `Retry-After`, or at least 60 seconds when it is absent, then send `POST /v1/posts/{pid}/retry`. See [Delivery](/guides/delivery). * **Auth error.** Reconnect the Pinterest account through the OAuth flow. * **Wrong board.** Delete the Pin, then publish it to the right board. *** ## 8. Full worked example This example finds a board, publishes a Pin to it, and reads the result. **Step 1: List boards** ```bash theme={null} curl https://api.social-api.ai/v1/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/boards \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "id": "sapi_board_01HZ9X3Q4R5M6N7P8V2K0W1J", "board_id": "8828", "name": "Recipes", "privacy": "PUBLIC" } ] } ``` **Step 2: Upload media** Upload the image and verify it as described in [Media](/guides/media). The result is a `media_id`, here `f47ac10b-58cc-4372-a567-0e02b2c3d479`. **Step 3: Publish the Pin** ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Weeknight dinner idea: sheet-pan salmon.", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "board_id": "8828" }], "platform_data": { "pinterest": { "title": "Sheet-pan salmon", "link": "https://example.com/recipes/salmon" } } }' ``` Response (HTTP 201): the post with `status: "publishing"`, as in [Create post](#3-create-post). **Step 4: Check the status** ```bash theme={null} curl https://api.social-api.ai/v1/posts/7c9e6679-7425-40de-944b-e07fc1f90ae7 \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200) once published: ```json theme={null} { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "text": "Weeknight dinner idea: sheet-pan salmon.", "status": "published", "hidden": false, "retry_count": 0, "published_at": "2026-10-13T09:00:04Z", "created_at": "2026-10-13T09:00:00Z", "updated_at": "2026-10-13T09:00:04Z", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "page_id": "sapi_board_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "pinterest", "status": "published", "platform_post_id": "813744", "permalink": "https://www.pinterest.com/pin/813744/", "published_at": "2026-10-13T09:00:04Z" } ] } ``` *** ## 9. Required OAuth scopes SocialAPI's Pinterest app requests these scopes during the OAuth flow. You configure nothing. The full list is in [Pinterest scopes](/platforms/pinterest#scopes). | Scope | Purpose | | - | - | | `boards:read` | List boards to choose `board_id` | | `boards:write` | Required by Pinterest to create and delete Pins | | `pins:write` | Create and delete Pins | The scopes a scoped API key needs, the credit cost of a post, and the MCP tools that publish are on [Publishing](/guides/publishing). # Telegram API Source: https://docs.social-api.ai/platforms/telegram Connect a Telegram bot with its BotFather token, then read and answer the direct messages people send to your bot through one REST API. SocialAPI connects a Telegram bot that you create with @BotFather and reads and answers the direct messages (DMs) people send to it. You connect the bot with its token: there is no OAuth, no Telegram developer account, and no app to register. Telegram bots receive DMs only, so SocialAPI offers no posts, comments, reviews, or mentions for Telegram. [Platform apps](/guides/platform-credentials) lists which app connects each platform. ## At a glance | Field | Value | | - | - | | Platform slug | `telegram` | | Auth | Bot token from @BotFather | | Platform API | Telegram Bot API | | Status | Available on every plan | ## Capabilities | Feature | Supported | Notes | | - | - | - | | Receive DMs | Yes | Messages people send to your bot arrive as `dm.received` webhooks and in the inbox | | Send DMs | Conditional | Only to a person who has messaged the bot first. Otherwise the send fails with `403 telegram.cannot_message` | | DM attachments | Yes | One file per message bubble, sent as a photo or a document. `platform_attachment_id` is not supported | | Buttons, quick replies, cards, and lists | No | SocialAPI sends only the text of a Telegram message | | Message history | No | The inbox starts when you connect the bot | | Message reactions | No | Returns `501 resource.not_supported` | | Comments | No | SocialAPI reads no Telegram comments | | Mentions | No | Returns `501 resource.not_supported` | | Reviews | No | Telegram has no reviews | | Publishing | No | `POST /v1/posts` and `PATCH /v1/posts/{pid}` with a Telegram target return `422 account.platform_unsupported` | | Automations | No | Automations run on Instagram and Facebook accounts only. `422 automation.platform_unsupported` | The capability matrix for every platform is on [Supported platforms](/platforms/overview). ## Connect a bot A Telegram bot connects in one request: you send its token, and SocialAPI returns the connected account. **1. Create the bot** Open Telegram and message [@BotFather](https://t.me/BotFather). Send `/newbot`, choose a display name, then choose a username that ends in `bot`. **2. Copy the token** BotFather replies with a token of the form `123456789:ABCdefGhIjKlMnOpQrStUvWxYz-1234`. Anyone who holds the token controls the bot, so keep it secret. **3. Connect the bot** Send the token as `metadata.bot_token`: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/connect \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "telegram", "metadata": { "bot_token": "123456789:ABCdefGhIjKlMnOpQrStUvWxYz-1234" } }' ``` Response (HTTP 201): ```json theme={null} { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "brand_id": "1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f", "platform": "telegram", "username": "acme_support_bot", "display_name": "Acme Support" } ``` `username` is the bot's username and `display_name` is its name. Without `brand_id` in the request, SocialAPI creates a brand for the bot. SocialAPI also sends `account.connected` to your webhook endpoints. A missing `bot_token` returns `400 validation.field_required`. A token Telegram rejects returns `401 platform.telegram.auth`. ### One bot, one receiver Connecting the bot points its Telegram webhook at SocialAPI and replaces any webhook the bot already had. Telegram delivers a bot's updates to one receiver only: while the bot is connected to SocialAPI, another service cannot receive its messages by webhook or with `getUpdates`. Disconnecting the account (`DELETE /v1/accounts/{account_id}`) removes the webhook, so Telegram stops sending the bot's messages to SocialAPI. The token itself stays valid until you revoke it with `/revoke` in @BotFather. A bot token has no expiry date; after a revoke, connect the bot again with the new token. ## Receive messages Each message a person sends to your bot arrives as a `dm.received` event and is stored in the inbox. Telegram offers no message history to bots, so the inbox holds only messages received after you connected the bot. The `dm.received` data carries the fields you need to reply and to match the stored message: | Field | Description | | - | - | | `conversation_id` | The SocialAPI conversation to reply into | | `platform_id` | Telegram's message ID, the same value as `platform_id` on the message in `GET /v1/inbox/conversations/{conversation_id}/messages` | | `reply_to.platform_id` | Set when the person replied to an earlier message: that message's `platform_id` | | `content.text` | The message text, or the caption of a file | | `content.media` | The file the person sent, as `{ "url", "type" }`. `type` is `image`, `audio`, `video`, `document`, or `sticker` | Photos, documents, voice notes, audio, videos, video notes, animations, and stickers all arrive in `content.media`. The `url` is a SocialAPI URL of the form `https://api.social-api.ai/v1/inbox/media/{id}`: Telegram files can be downloaded only with the bot's token, so SocialAPI keeps a copy for you. Fetch the URL with your API key. The same URL appears as `attachment_url` on the stored message, and it stays available for 30 days. Telegram does not let bots download files larger than 20 MB, so the URL of such a file returns `410`. [Fetch an inbound attachment](/guides/inbox#fetch-an-inbound-attachment) lists every status this URL returns. Edited and deleted messages raise no event. The full event payload is on [Webhook events](/guides/webhook-events). ## Send messages You reply with `POST /v1/inbox/conversations/{conversation_id}/messages`, the same endpoint as every other platform. The request fields are on [Direct messages](/guides/direct-messages). ```bash theme={null} curl -X POST https://api.social-api.ai/v1/inbox/conversations/{conversation_id}/messages \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "text": "Your order shipped this morning. Here is the invoice.", "attachments": [ { "source_type": "url", "source": "https://example.com/invoices/A1234.pdf", "caption": "Invoice A1234" } ] }' ``` Response (HTTP 200): ```json theme={null} { "success": true, "message_id": "sapi_dm_TT974YVQH4RAHNN2HY2DJSNY9111D7LMTP1XXPCVQVL", "message_ids": [ "sapi_dm_TT974YVQH4RAHNN2HY2DJSNY9111D7LMTP1XXPCVQVL", "sapi_dm_TT974YVQH4RAHNN2HY2DJSNY9111D7LMTP1XXPCVQVM" ] } ``` The text and each attachment are sent as separate messages, in that order. Omitting `account_id` returns `400 validation.field_required`. These rules are specific to Telegram: * **The person writes first.** Telegram does not let a bot start a conversation. Until the person sends `/start` or any message to the bot, a send fails with `403 telegram.cannot_message`. The same code is returned when the person blocked the bot. Once the person has written, there is no reply window: the bot can answer at any time. * **Photo or document.** SocialAPI sends a file whose name ends in `.jpg`, `.jpeg`, `.png`, `.gif`, or `.webp` as a photo, and any other file as a document. The `type` field of the attachment does not change this choice. * **Captions.** An attachment's `caption` is shown under the file. The message `text` is always a separate message, also with the single-file shorthand `attachment_url`. * **Interactive content is not sent.** Telegram messages carry no `buttons`, `quick_replies`, `cards`, or `list_sections`. SocialAPI sends the `text` and ignores those fields. `reply_to_message_id` and `message_tag` are ignored as well. * **No `platform_attachment_id`.** Send files as `url` or `media_id` attachments. ## Limits | Limit | Value | | - | - | | Message text | 4,096 characters | | Caption | 1,024 characters | | Photo sent by URL | 5 MB | | Other file sent by URL | 20 MB | | File a person sends that SocialAPI can copy | 20 MB | | Inbound file URL availability | 30 days | Telegram enforces the text, caption, and file limits. A request over one of them, or a file URL Telegram cannot fetch, fails with `400 validation.telegram_bad_request`, whose message carries Telegram's own description. When Telegram rate-limits the bot, the send fails with `429 platform.telegram.rate_limit`: wait the number of seconds in the `Retry-After` header before you retry. ## Scopes A Telegram bot has no OAuth scopes. The bot token gives full control of the bot, and SocialAPI uses it to receive the bot's messages and to send replies. The account supports the `messages.read` and `messages.write` [permissions](/guides/permissions), which SocialAPI alone enforces. ## Related * [Direct messages](/guides/direct-messages): the request fields for sending DMs and attachments * [Webhook events](/guides/webhook-events): the full `dm.received` payload * [Connect accounts](/guides/connect-accounts): brands, reconnecting, and disconnecting * [Errors](/guides/errors): every Telegram error code * [Supported platforms](/platforms/overview): what each platform supports # Threads API Source: https://docs.social-api.ai/platforms/threads Connect Threads accounts to publish text, image, video, and carousel posts, read and moderate replies, and read mentions and insights through one API. The Threads API in SocialAPI connects Threads accounts. It publishes text, image, video, and carousel posts, reads, answers, and moderates replies, reads mentions and insights, and, in beta, looks up public profiles. Threads has no direct messages in the API. SocialAPI's own Meta Threads app connects the account, so you register nothing: see [Platform apps](/guides/platform-credentials). ## At a glance | Field | Value | | - | - | | Platform slug | `threads` | | Auth | OAuth 2.0 (Meta Threads) | | Platform API | Threads API (Meta) | | Status | Available. Profile discovery is in beta | ## Capabilities An unsupported call returns `501 resource.not_supported`. A Beta row depends on a Threads scope that Meta has not granted to SocialAPI's app for every account: see [Beta scopes](#beta-scopes). Reply moderation, mentions, and insights need a scope that an account connected before 2026-10-17 does not hold: reconnect such an account to use them. | Feature | Status | Condition | | - | - | - | | Publish text, image, video, carousel | Yes | See [Threads publishing](/platforms/threads/publishing) | | Reply chains with `segments` | No | A Threads target publishes the first post only and ignores `segments` | | Schedule a post | Yes | `scheduled_at` on the post | | First comment on a post | No | `first_comment` is not published on Threads | | Update a published post | No | `PATCH /v1/posts/{pid}` returns `409 post.state_invalid` for a published post. A `draft`, `scheduled`, or `failed` post can be edited before it publishes. To correct it, delete the post and publish a new one | | Delete a published post | Yes | | | List posts | Yes | Text posts, images, videos, carousels | | List replies | Yes | Replies to a post, and replies to a reply | | Reply to a reply | Yes | | | Delete a reply | Conditional | Only replies your account wrote. `can_delete` is `true` on every reply, including other accounts' replies, and deleting another account's reply fails | | Hide, unhide a reply | Yes | Only replies from other accounts (`can_hide` is `false` on your own replies). Needs the `threads_manage_replies` scope. Without it the call returns `403` with a `platform.threads.` error code, where `` is Meta's error code | | Like a reply | No | `501 resource.not_supported` | | Private reply | No | `501 resource.not_supported` | | DMs | No | Threads has no messaging API. `501 resource.not_supported` | | Reviews | No | `GET /v1/inbox/reviews/{account_id}` returns `422 account.platform_unsupported` | | Mentions | Yes | Needs `threads_manage_mentions`. List, reply, and the `mention.received` event | | Insights | Yes | Needs `threads_manage_insights`. Post-level and account-level metrics. Without it, `likes` and `comments` in the `metrics` of a Threads post stay `0` | | Profile discovery | Beta | Needs `threads_profile_discovery`. Public profile lookup by exact username | | Automations | No | Automations run on Instagram and Facebook accounts only. `422 automation.platform_unsupported` | | Webhook events | Yes | See [Webhook events](#webhook-events) | The cross-platform matrix is on [Platforms](/platforms/overview). ## Connect an account Connect with `POST /v1/accounts/connect` and `"platform": "threads"`. The shared flow, request fields, and result are in [Connect accounts](/guides/connect-accounts), and the redirect parameters are in [Redirect parameters](/guides/connect-accounts#redirect-parameters). ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/connect \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "threads", "redirect_uri": "https://app.example.com/oauth/callback", "state": "session_abc123" }' ``` Response (HTTP 202): ```json theme={null} { "auth_url": "https://www.threads.com/oauth/authorize?client_id=...&state=...", "state": "4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2", "message": "Open this link to connect your threads account. Once you've authorized, the account will be connected automatically." } ``` A Threads access token lasts about 60 days. SocialAPI refreshes it. If a call returns `401 platform.threads.auth`, the person revoked access or the refresh failed: reconnect the account. See [Reconnect an account](/guides/connect-accounts#reconnect-an-account). ## Replies List the replies to a post with `GET /v1/inbox/comments/{postId}`, where `postId` is the post's platform ID. Reply and delete work as described in [Comments](/guides/comments). Threads calls a comment a reply. * **Hide and unhide:** only replies from other accounts. They need the scope `threads_manage_replies`. * **Delete:** only replies your account wrote, and it works without `threads_manage_replies`. A reply from another account cannot be deleted: hide it instead. * A like and a private reply return `501 resource.not_supported`. ## Mentions A mention is a public Threads post that tags your account. `GET /v1/accounts/{id}/mentions` lists mentions, and `POST /v1/accounts/{id}/mentions/reply` replies to one. The `mention.received` event delivers a mention as it happens, and the list returns the same mention with the same `id`, so you can match them. Details are in [Mentions](/guides/mentions). ```bash theme={null} curl "https://api.social-api.ai/v1/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/mentions?limit=20" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "id": "sapi_mnt_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "platform": "threads", "type": "mention", "author": { "username": "janesmith" }, "content": { "text": "Great thread, @acmecorp" }, "metadata": null, "received_at": "2026-10-13T14:35:00Z", "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform_post_id": "18012345678901234", "permalink": "https://www.threads.com/@janesmith/post/DAbCdEfGhIj" } ], "count": 1 } ``` | Parameter | Description | | - | - | | `since` | An RFC 3339 timestamp. Only mentions after it are returned. A `since` before 2023-07-05T07:00:00Z is treated as 2023-07-05T07:00:00Z | | `limit` | Mentions in the response, 1 to 100. The default is 20 | | `cursor` | Ignored on Threads. The list is 1 page and never returns `next_cursor`: raise `limit` or move `since` to reach other mentions | Reply to a mention: ```bash theme={null} curl -X POST "https://api.social-api.ai/v1/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/mentions/reply" \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{"media_id": "sapi_mnt_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "text": "Thanks for the mention!"}' ``` `media_id` is the `id` from the list response. An account connected without `threads_manage_mentions` gets `403` with a `platform.threads.` error code on the list, where `` is Meta's error code. Reconnect the account to add the scope. ## Insights `GET /v1/platforms/threads/insights` returns post-level or account-level metrics. It needs `threads_manage_insights`. An account without it returns `403 platform.threads.insights_scope_missing` until it is reconnected. Post-level metrics for one post: ```bash theme={null} curl "https://api.social-api.ai/v1/platforms/threads/insights?account_id=acc_01HZ9X3Q4R5M6N7P8V2K0W1J&level=media&post_id=17895695668004550" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Account-level metrics over a window: ```bash theme={null} curl "https://api.social-api.ai/v1/platforms/threads/insights?account_id=acc_01HZ9X3Q4R5M6N7P8V2K0W1J&level=account&since=1760000000" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200) for `level=account`: ```json theme={null} { "data": { "metrics": { "followers_count": { "value": 1280 }, "clicks": { "value": 47 }, "views": { "values": [ { "end_time": "2026-10-11T07:00:00Z", "value": 910 }, { "end_time": "2026-10-12T07:00:00Z", "value": 1204 } ] } } } } ``` Each metric comes back as a scalar (`value`), a dated series (`values`), or, for `follower_demographics`, a `breakdown`. Account-level `views` is a series, `follower_demographics` is a breakdown, and the other account-level metrics are scalars. | Level | Metrics | | - | - | | `media` | `views`, `likes`, `replies`, `reposts`, `quotes`, `shares` | | `account` | `views`, `likes`, `replies`, `reposts`, `quotes`, `clicks`, `followers_count`, `follower_demographics` | | Parameter | Required | Description | | - | - | - | | `account_id` | Yes | The connected Threads account | | `level` | Yes | `media` or `account` | | `post_id` | At `level=media` | The post's platform ID | | `metric` | No | Comma-separated metric names from the table above. Default: every metric valid for the `level` except `follower_demographics` | | `breakdown` | With `follower_demographics` | `country`, `city`, `age`, or `gender`. Valid only with `follower_demographics` | | `since` | No | Unix timestamp in seconds, not earlier than `1712991600` (2024-04-13). Not accepted at `level=media` | | `until` | No | Unix timestamp in seconds, not earlier than `1712991600` (2024-04-13). Not accepted at `level=media` | * `follower_demographics` needs exactly one `breakdown` and rejects `since` and `until`. * `follower_demographics` on an account with fewer than 100 followers returns `422 platform.threads.801`. * `follower_demographics` returns `breakdown`, a list of `{ "dimension", "value" }` rows, for example `{ "dimension": "US", "value": 540 }` with `breakdown=country`. `dimension` is a country code, a city, an age range, or a gender, matching the `breakdown` you sent. * Metrics for a new post appear after a delay, and a deleted post returns nothing. Both return an empty result, not an error. The `400` codes for these parameters are in [Errors](/guides/errors). ## Profile discovery `GET /v1/platforms/threads/profile-lookup` returns the public profile for an exact username. A leading `@` is optional. It needs `threads_profile_discovery`. ```bash theme={null} curl "https://api.social-api.ai/v1/platforms/threads/profile-lookup?account_id=acc_01HZ9X3Q4R5M6N7P8V2K0W1J&username=meta" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": { "username": "meta", "name": "Meta", "profile_picture_url": "https://scontent.cdninstagram.com/v/t51.2885-19/meta.jpg", "biography": "Giving people the power to build community.", "follower_count": 5400000, "likes_count": 12800000, "quotes_count": 41000, "reposts_count": 96000, "views_count": 310000000, "is_verified": true } } ``` * The lookup matches the exact username. There is no prefix or fuzzy search. * Only profiles with 100 or more followers are discoverable. * `404 resource.not_found` means the username is not discoverable. Threads gives the same answer when the account does not exist, has fewer than 100 followers, or is outside the access tier, so the three cases look the same. * Threads allows 1,000 lookups per user per rolling 24 hours. More returns `429 platform.threads.rate_limit`. ## Webhook events Threads accounts emit these events to your webhook endpoints. Payloads are in [Webhook events](/guides/webhook-events). | Event | When it fires | | - | - | | `comment.received` | A person replies to one of your posts | | `mention.received` | Another account mentions yours. See [Mentions](#mentions) | | `account.connected` | You finished connecting or reconnecting the account | | `account.disconnected` | The person revoked access to the account on Threads, or SocialAPI flagged the account for reconnection (`reconnect_required: true`, see `reason`) | | `account.updated` | The account lost [permissions](/guides/permissions) you did not remove yourself (`change` is `permissions_reduced`) | | `post.published`, `post.partial`, `post.failed` | A post to this account finished: see [Delivery](/guides/delivery) | Every `post.*` event in [Webhook events](/guides/webhook-events#posts) is emitted for this platform. ## Limits | Limit | Value | | - | - | | Post text | 500 characters and 1 hashtag | | Carousel | 2 to 20 items | | Image | 8 MB: JPEG, PNG, GIF, or WebP | | Video | 1 GB: MP4 or QuickTime | | Links in a post | 5 unique links | | Posts | 250 per 24 hours and 25 per hour for each account: see [Posting velocity caps](/guides/rate-limits#posting-velocity-caps) | | Replies | 1,000 per rolling 24 hours, set by Threads | | Threads' own rate limit | `429 platform.threads.rate_limit`. Credit rules: see [when a credit comes back](/guides/plans#when-a-credit-comes-back) | `GET /v1/accounts/{id}/limits` returns `posts_remaining`, `posts_total`, `replies_remaining`, and `replies_total`. ## Scopes SocialAPI's Meta Threads app requests these scopes at connect. You apply for no app review. Each scope belongs to the account [permissions](/guides/permissions) in the last column, and the consent screen asks for the scopes of the permissions you selected. `threads_basic` is always requested. | Scope | Used for | Permissions | | - | - | - | | `threads_basic` | Profile and posts | Every account | | `threads_content_publish` | Publish posts and replies | `posts.write`, `comments.write` | | `threads_delete` | Delete posts and replies | `posts.write`, `comments.write` | | `threads_read_replies` | Read replies | `comments.read` | | `threads_manage_mentions` | Mentions | `comments.read` | | `threads_manage_replies` | Hide and unhide replies, `reply_control` on a post | `comments.write` | | `threads_manage_insights` | Insights, and the like and reply counts on posts | `analytics.read` | Threads asks for publishing and deleting as 1 pair of scopes shared by `posts.write` and `comments.write`, so the consent screen can show more than you selected. The account stays limited to the permissions you selected. `threads_manage_mentions`, `threads_manage_replies`, and `threads_manage_insights` are requested since 2026-10-17. An account connected before that date does not hold them: an insights call returns `403 platform.threads.insights_scope_missing`, and any other call that needs one returns `403` with a `platform.threads.` error code, where `` is Meta's error code. Reconnect the account to add them. ### Beta scopes These further scopes are requested only for accounts enrolled in the beta: | Scope | Used for | Permissions | | - | - | - | | `threads_profile_discovery` | Profile discovery | Every account | | `threads_location_tagging` | `location_id` on a post | `posts.write` | | `threads_share_to_instagram` | `crossreshare_to_ig` on a post | `posts.write` | | `threads_keyword_search` | Requested with `threads_profile_discovery`. No SocialAPI endpoint uses it | Every account | Until Meta grants a scope for every account, a call that needs it from an account without it returns `403` with a `platform.threads.` error code, where `` is Meta's error code. A beta scope is added when the account connects: reconnect the account to add one granted later. ## Related * [Threads publishing](/platforms/threads/publishing): fields, media rules, and quirks for publishing * [Connect accounts](/guides/connect-accounts): the shared connect flow * [Platforms](/platforms/overview): the capability matrix * [Errors](/guides/errors): every error code # Threads publishing Source: https://docs.social-api.ai/platforms/threads/publishing Publish text, image, video, and carousel posts to Threads accounts through SocialAPI, with platform-specific fields, limits, and error recovery. ## 1. Overview A Threads post goes live once Threads has accepted it. A text post needs no media. A post with media waits for Threads to process the file, for about 4 minutes at most. | Field | Value | | - | - | | Platform slug | `threads` | | Auth type | OAuth 2.0 (Meta Threads) | | Platform API | Threads API (Meta) | | Create post | Yes | | Update post | Not after publishing (`409 post.state_invalid`). `draft`, `scheduled`, and `failed` posts can be edited | | Delete post | Yes | | Schedule | Yes (deferred publish via `scheduled_at`) | | First comment | No (`first_comment` is not published on Threads) | | Reply chains | No (`segments` are ignored: only the first post publishes) | | Posting caps | 250 posts per 24 hours and 25 per hour for each account: see [Posting velocity caps](/guides/rate-limits#posting-velocity-caps) | *** ## 2. Supported media | Content type | How to trigger | Notes | | - | - | - | | Text only | Omit `media` | Unlike Instagram, a text post needs no media | | Single image | One `media` entry that is an image | JPEG, PNG, GIF, or WebP, at most 8 MB | | Single video | One `media` entry that is a video | MP4 or QuickTime, at most 1 GB. SocialAPI does not check the duration | | Carousel | 2 to 20 `media` entries | Images and videos mixed | SocialAPI reads the media type from the file extension in the URL path, so query parameters (for example on a presigned upload URL) do not affect it. **Text limit:** 500 characters and 1 hashtag. More returns `400 publishing.validation_failed`. **Carousel limit:** 2 to 20 items. A `media` list of 2 or more entries is a carousel. **File limits** apply to media you upload to SocialAPI (`source_type: "media_id"`). A file outside them fails validation with `400 publishing.validation_failed`. For a `url` item, SocialAPI checks the file type against these limits, and the size when the host reports it. The duration of a video sent by URL is not checked in advance. Threads accepts a pre-built container only as the single `media` item (`source_type: "platform_attachment_id"`). Combining it with other media returns `422 publishing.media.mixing_unsupported`. *** ## 3. Create post Use `POST /v1/posts` with `targets` targeting a Threads account. The `text` field becomes the post body. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "Shipping updates for April. Full details on our site.", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "platform_data": { "threads": { "topic_tag": "productupdates" } } }' ``` Response (HTTP 201): ```json theme={null} { "id": "6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "text": "Shipping updates for April. Full details on our site.", "status": "publishing", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "media_ids": ["f47ac10b-58cc-4372-a567-0e02b2c3d479"], "hidden": false, "retry_count": 0, "created_at": "2026-10-13T09:00:00Z", "updated_at": "2026-10-13T09:00:00Z", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "threads", "status": "pending", "text": "Shipping updates for April. Full details on our site.", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "media_ids": ["f47ac10b-58cc-4372-a567-0e02b2c3d479"] } ] } ``` SocialAPI publishes in the background. The target moves from `pending` to `publishing`, then to `published` or `failed`. Poll `GET /v1/posts/{pid}` or subscribe to the `post.published`, `post.partial`, and `post.failed` [webhook events](/guides/webhook-events). Publishing or scheduling costs 1 post credit; a draft is free (see [Plans and credits](/guides/plans)). ### Platform data fields Send these keys in `platform_data.threads` on the post, which applies them to every Threads target. To set them for one target, send them flat in that target's `platform_data` (`targets[].platform_data`, with no `threads` key). When both set the same key, the target's value wins. | Key | Type | Notes | | - | - | - | | `link_attachment` | string (URL) | Attaches a link preview. A post holds at most 5 unique links: more fails the target with `422 platform.threads.THREADS_API__LINK_LIMIT_EXCEEDED` | | `poll_attachment` | object | Text-only posts. 2 to 4 options `option_a` to `option_d`, each 1 to 25 characters | | `alt_text` | string | Accessibility text for an image or video, up to 1,000 characters | | `topic_tag` | string | 1 to 50 characters, with no periods or ampersands | | `quote_post_id` | string | Publishes a quote of this Threads post | | `allowlisted_country_codes` | array of ISO 3166-1 alpha-2 codes | Restricts the post to these countries | | `is_spoiler_media` | boolean | Marks the image, video, or carousel as a spoiler | | `text_entities` | object | Spoiler regions in the text | | `gif_attachment` | `{gif_id, provider}` | `provider` must be `"GIPHY"`. Any other provider is not sent | | `is_ghost_post` | boolean | Text-only posts. The post archives itself after 24 hours. Not valid together with `enable_reply_approvals` | | `enable_reply_approvals` | boolean | Replies need your approval before they appear publicly | | `reply_control` | string | Who can reply: `everyone`, `accounts_you_follow`, `mentioned_only`, `parent_post_author_only`, or `followers_only`. On an account that holds `threads_manage_replies`, any other value fails validation. On any other account the key is not sent and the response carries a warning: see below | | `location_id` | string | Needs the beta scope `threads_location_tagging` | | `crossreshare_to_ig` | boolean | Shares the post to the linked Instagram account as a Story. Needs the beta scope `threads_share_to_instagram`. `crossreshare_to_ig_dark_mode` sets the dark variant | SocialAPI forwards any additional keys in `platform_data.threads` that the table above lists for the post type you publish, and no others. Any other key is not sent, and the response lists it as a validation warning naming the key. `reply_control` needs the `threads_manage_replies` scope. An account holds it when its last connect was on or after 2026-10-17 and included the `comments.write` [permission](/guides/permissions), or sent no `permissions`. On any other account, `reply_control` is not sent: the post publishes with Threads' default, where everyone can reply, and the response carries a validation warning on `reply_control`. Reconnect the account with `comments.write` to apply it. ### Scheduling Set `scheduled_at` to an ISO 8601 timestamp (UTC) to defer publishing: ```json theme={null} { "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "Big announcement coming tomorrow.", "scheduled_at": "2026-11-15T10:00:00Z" } ``` *** ## 4. Update post A published Threads post cannot be edited through SocialAPI. `PATCH /v1/posts/{pid}` returns `409 post.state_invalid` for any post whose status is not `draft`, `scheduled`, or `failed`, with the message "Only draft, scheduled, or failed posts can be edited". A post in `draft`, `scheduled`, or `failed` status can be edited with `PATCH /v1/posts/{pid}` before it publishes. To correct a published post, delete it with `DELETE /v1/posts/{pid}` and publish a new post. *** ## 5. Delete post ```bash theme={null} curl -X DELETE https://api.social-api.ai/v1/posts/6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d \ -H "Authorization: Bearer $SOCAPI_KEY" ``` To remove only the Threads target of a cross-platform post, unpublish it by `account_id`. `DELETE /v1/posts/{pid}` always removes every target. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts/6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d/unpublish \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{"account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J"}' ``` See [Delete a post](/guides/delivery#delete-a-post) for how a cross-platform post is handled. *** ## 6. Retrieve posts Use `GET /v1/posts` to list posts. Filter by platform or account: ```bash theme={null} curl "https://api.social-api.ai/v1/posts?platform=threads&account_ids=acc_01HZ9X3Q4R5M6N7P8V2K0W1J&limit=20" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Each post includes a `targets` array with per-platform status and engagement metrics: ```json theme={null} { "id": "6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "text": "Shipping updates for April.", "status": "published", "targets": [ { "platform": "threads", "platform_post_id": "1234567890123456", "status": "published", "permalink": "https://www.threads.com/@yourhandle/post/ABC123", "metrics": { "likes": 0, "comments": 0, "shares": 0, "saves": 0, "extra": null }, "metrics_synced_at": null } ] } ``` **Metrics notes:** Likes and replies come from Threads insights, which need the `threads_manage_insights` scope. An account connected before 2026-10-17 does not hold it, so `likes` and `comments` stay `0` until the account is reconnected. Threads does not report shares or saves, so those stay `0`. Account and post insights are in [Threads insights](/platforms/threads#insights). *** ## 7. Quirks, errors, and recovery ### Media processing delay A post with media waits for Threads to process the file. If Threads has not finished after about 4 minutes, the target fails with `504 platform.threads.container_timeout`. If Threads rejects or expires the media, the target fails with `422 platform.threads.ERROR` or `422 platform.threads.EXPIRED`. A text post has no delay. ### No post editing A published Threads post cannot be edited. `PATCH /v1/posts/{pid}` returns `409 post.state_invalid`. Delete the post and publish a new one: see [4. Update post](#4-update-post). ### Text-only posts Omit `media` to publish a post with a text body only. ### Reply chains are not published `segments` on a post are ignored for Threads targets. Only the first post publishes. ### Who can reply Everyone can reply to a post unless `reply_control` sets otherwise. `reply_control` applies only on an account that holds `threads_manage_replies`: see "Platform data fields". ### Token expiry A Threads access token lasts about 60 days. SocialAPI refreshes it. When a call returns `401 platform.threads.auth`, reconnect the account through the OAuth flow. ### Posting caps SocialAPI refuses a publish that would pass 25 posts in an hour or 250 posts in 24 hours for one account, with `429 publishing.velocity_limit`. A scheduled post is held and published when the window clears. When Threads refuses first, the target fails with `429 platform.threads.rate_limit`. See [Posting velocity caps](/guides/rate-limits#posting-velocity-caps). ### Errors on a failed target A publish that fails after the request returned puts a structured error on the target, in `targets[].error`: ```json theme={null} { "code": "platform.threads.ERROR", "message": "The media could not be processed", "category": "platform", "caused_by": "platform" } ``` * `401 platform.threads.auth`: the access token expired or was revoked. Reconnect the account. * `429 platform.threads.rate_limit`: Threads' own limit. Wait, then retry. * `422 platform.threads.THREADS_API__LINK_LIMIT_EXCEEDED`: the post holds more than 5 unique links. * `422 platform.threads.ERROR`, `422 platform.threads.EXPIRED`: Threads rejected or expired the media. * `504 platform.threads.container_timeout`: Threads did not finish processing the media after about 4 minutes. * `platform.threads.`: Threads refused the request with its own error code and message. The HTTP status is `502` unless the code has its own mapping, such as `422 platform.threads.801`. Read `message`. * `502 platform.threads.api_error`: a failure reaching Threads, or a response with no usable error. Every code is in [Errors](/guides/errors). ### Recovery * **Auth error.** Reconnect the Threads account through the OAuth flow to get a fresh token. * **Media timeout.** A large video or a slow media URL causes it. Upload the media again and retry. * **API error.** Read `message`, fix the cause, and send `POST /v1/posts/{pid}/retry`. See [Delivery](/guides/delivery). *** ## 8. Full worked example The following example publishes a video post with a topic tag, then checks its status. **Step 1: Upload media** ```bash theme={null} # Get a presigned upload URL curl "https://api.social-api.ai/v1/media/upload-url?filename=clip.mp4&media_type=video/mp4" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response: ```json theme={null} { "upload_url": "https://upload.example.com/media/clip.mp4?X-Amz-Signature=...", "media_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479" } ``` Upload the file directly to the presigned URL, then verify: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/media/f47ac10b-58cc-4372-a567-0e02b2c3d479/verify \ -H "Authorization: Bearer $SOCAPI_KEY" ``` **Step 2: Publish the post** ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "Our April product update is live. Check the link in bio for full details.", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "platform_data": { "threads": { "topic_tag": "productupdates" } } }' ``` Response (publishing continues in the background): ```json theme={null} { "id": "6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "status": "publishing", "targets": [ { "platform": "threads", "status": "publishing" } ] } ``` **Step 3: Check status** ```bash theme={null} curl "https://api.social-api.ai/v1/posts/6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response once published: ```json theme={null} { "id": "6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "text": "Our April product update is live. Check the link in bio for full details.", "status": "published", "targets": [ { "platform": "threads", "platform_post_id": "1234567890123456", "status": "published", "permalink": "https://www.threads.com/@yourhandle/post/ABC123", "metrics": { "likes": 0, "comments": 0, "shares": 0, "saves": 0, "extra": null }, "metrics_synced_at": null } ], "created_at": "2026-04-10T09:00:00Z", "published_at": "2026-04-10T09:02:05Z" } ``` *** ## 9. Required OAuth scopes SocialAPI's Meta Threads app requests these scopes during the OAuth flow. You configure nothing. The beta scopes are in [Threads beta scopes](/platforms/threads#beta-scopes). | Scope | Purpose | | - | - | | `threads_basic` | Read basic account info and profile | | `threads_content_publish` | Create and publish media containers | | `threads_read_replies` | Read replies on posts | | `threads_delete` | Delete published posts | | `threads_manage_insights` | Like and reply counts on posts. Requested since 2026-10-17 | | `threads_manage_replies` | `reply_control` on a post. Requested since 2026-10-17 with `comments.write` | The scopes a scoped API key needs, the credit cost of a post, and the MCP tools that publish are on [Publishing](/guides/publishing). # TikTok API Source: https://docs.social-api.ai/platforms/tiktok Connect TikTok accounts to publish videos and photo carousels, read and moderate comments, list mentions, and read profile counts and post metrics. The TikTok API in SocialAPI connects a TikTok account through TikTok's authorization screen, using TikTok API for Business. It publishes videos and photo carousels, lists the account's posts with their counts, reads, replies to, and moderates comments, lists mentions, sends `comment.received` events, and returns profile counts. A [creator connection](#creator-connection) uses TikTok's Content Posting API instead, and has no comments or mentions. SocialAPI's own TikTok app connects the account, so you register nothing: see [Platform apps](/guides/platform-credentials). ## At a glance | Field | Value | | - | - | | Platform slug | `tiktok` | | Auth | OAuth 2.0 (TikTok authorization screen) | | Platform API | TikTok API for Business (Accounts API). TikTok Content Posting API with a [creator connection](#creator-connection) | | Status | Available | ## Capabilities An unsupported call returns `501 resource.not_supported`. This table is for the default connection. A [creator connection](#creator-connection) has its own table. | Feature | Status | Condition | | - | - | - | | Publish a video | Yes | Public on TikTok. See [TikTok publishing](/platforms/tiktok/publishing) | | Publish a photo carousel | Yes | 2 to 35 images | | Send a post to the account's TikTok drafts | Yes | `post_mode: "inbox"`. Video or photo carousel. The creator finishes the post in the TikTok app | | Schedule a post | Yes | `scheduled_at` on the post | | First comment on a post | No | `first_comment` is ignored. Comment on the post with [Comments](#comments) after it is published | | Update a published post | No | `PATCH /v1/posts/{pid}` returns `409 post.state_invalid` for a published post, and TikTok gives apps no edit call | | Delete a published post | No | `DELETE /v1/posts/{pid}` returns HTTP 200 with `results[].outcome: "not_supported"` for the TikTok target, and the post stays live. `POST /v1/posts/{pid}/unpublish` returns `501 resource.not_supported`. Delete the post in the TikTok app | | List posts | Yes | Public videos and photo posts with likes, comments, shares, and views. See [Sync videos](#sync-videos) | | Live post metrics | Yes | `GET /v1/posts/{pid}/metrics`, for the account's 20 most recent posts. See [Sync videos](#sync-videos) | | Creator info | Yes | Whether the account can post, its longest video, and its interaction locks. See [Creator info](#creator-info) | | Account summary | Yes | Profile and counts. See [Account summary](#account-summary) | | Comments, replies, and comments on a post | Yes | See [Comments](#comments) | | Hide and unhide a comment | Conditional | Comments written by other people only | | Delete a comment | Conditional | Comments written by the connected account only | | Like a comment | Yes | | | Private reply to a commenter | No | `501 resource.not_supported` | | DMs | No | `501 resource.not_supported` | | Mentions | Conditional | TikTok Business Accounts only. See [Mentions](#mentions) | | Reply to a mention | Conditional | Mentions made in a comment only | | Reviews | No | TikTok has no reviews. `GET /v1/inbox/reviews/{account_id}` returns `422 account.platform_unsupported` | | Automations | No | `422 automation.platform_unsupported` | | Analytics export | Partial | Captions and metrics. On the default connection, `include_transcript` or `include_vision` returns `400 account.export_enrichment_not_supported`. A [creator connection](#creator-connection) accepts both | | Webhook events | Partial | `comment.received`, `mention.received` when you list mentions, the `post.*` events, `account.connected`, and `account.disconnected`. See [Webhook events](#webhook-events) | The cross-platform matrix is on [Platforms](/platforms/overview). ## Connect an account Connect with `POST /v1/accounts/connect` and `"platform": "tiktok"`. The shared flow, request fields, and result are in [Connect accounts](/guides/connect-accounts), and the redirect parameters are in [Redirect parameters](/guides/connect-accounts#redirect-parameters). ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/connect \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "tiktok", "redirect_uri": "https://app.example.com/oauth/callback", "state": "session_abc123" }' ``` Response (HTTP 202): ```json theme={null} { "auth_url": "https://www.tiktok.com/v2/auth/authorize?client_key=...&scope=...&response_type=code&redirect_uri=...&state=4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2&disable_auto_auth=1", "state": "4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2", "message": "Open this link to connect your tiktok account. Once you've authorized, the account will be connected automatically." } ``` TikTok-specific rules: * **Consent screen.** The person always sees TikTok's consent screen, including when they authorized SocialAPI's app before. * **Account ID.** The connected account has an `acc_` ID. Its `name` is the TikTok display name, `username` is the TikTok handle, and `bio` is the profile bio. Profile counts are not on the account: read them with [Account summary](#account-summary). * **Account type.** Listing mentions needs a TikTok Business Account: see [Mentions](#mentions). * **Connection type.** `metadata.connection_type` is `"business"` (the default) or `"creator"`: see [Creator connection](#creator-connection). Without `connection_type` the connect uses `"business"`. Any other value, including an empty string or a non-string, returns `400 validation.connection_type_invalid`. * **Reconnect.** Reconnect with the same flow: see [Reconnect an account](/guides/connect-accounts#reconnect-an-account). * **Accounts connected before 2026-10-13.** [Comment](#comments) and [mention](#mentions) calls on them return `501 resource.not_supported` until you reconnect them. ### When TikTok authorization ends This section covers the default connection. A [creator connection](#creator-connection) is removed when the person removes SocialAPI's app. The authorization ends when the person removes SocialAPI's app in TikTok's settings, or when TikTok refuses to renew it. TikTok sends SocialAPI no notice of a removal, so SocialAPI learns it from the next call that reaches TikTok. * **First call.** It returns `401 platform.tiktok.auth`. * **Later calls.** They return `403 account.reconnection_required` until you reconnect the account. * **`401 platform.tiktok.unauthorized`.** TikTok refused one call as unauthorized without saying the authorization is invalid. The account stays usable. Reconnect it if the error repeats. * **`account.disconnected` event.** When SocialAPI's own refresh of the account's videos meets the ended authorization before one of your calls does, it sends `account.disconnected` with `reconnect_required: true` and `reason: "posts_sync_auth_failed"`. A removal in TikTok's settings does not delete the account from SocialAPI. ## Creator connection A creator connection connects the TikTok account through TikTok's Content Posting API instead of TikTok API for Business. It works with a personal or creator TikTok account, which does not need to become a TikTok Business Account. Send `"connection_type": "creator"` in `metadata`: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/connect \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "tiktok", "metadata": { "connection_type": "creator" } }' ``` The response has the same shape as in [Connect an account](#connect-an-account), and the connected account's `platform` is `tiktok`. | Feature | Creator connection | | - | - | | Publish a video or photo carousel | Yes. `platform_data.privacy_level` is required on a published video and on a published photo carousel (not on a draft), and must be one of the account's `privacy_level_options` from [Creator info](#creator-info). A missing value returns `400 validation.privacy_level_required`, and a value not listed returns `400 validation.invalid_privacy_level`. When TikTok lists only `SELF_ONLY`, every post is private. When TikTok lists no level, `can_post` is `false` and a post returns `400 validation.posting_limit`. `brand_content_toggle: true` with `SELF_ONLY` returns `400 validation.branded_content_private` | | Send a post to the account's TikTok drafts | Video only. `post_mode: "inbox"`. A photo carousel sent to drafts fails validation on `post_mode` | | List posts | Yes. Videos, with likes, comments, shares, and views | | Creator info | Yes. `privacy_level_options` lists every level TikTok allows for the account | | Account summary | Yes | | Analytics export | Yes, including `include_transcript` and `include_vision` | | Comments, `comment.received`, and mentions | No. `501 resource.not_supported` | A creator connection asks on TikTok's consent screen for `user.info.basic` and `user.info.profile`, plus the scopes of the [permissions](/guides/permissions) you select: | Permission | Scopes | | - | - | | `posts.read` | `video.list` | | `posts.write` | `video.publish`, `video.upload` | | `analytics.read` | `user.info.stats` | When the person removes SocialAPI's app in TikTok's settings, SocialAPI removes the creator connection's account and sends `account.disconnected`. **Switching connection type.** Connecting the same TikTok username with the other `connection_type` moves the account to that connection type. It keeps its `acc_` ID, brand, and history while the account's authorization on the previous connection type still works. If that authorization no longer works, the connect creates a new account. A reconnect without `connection_type` uses `"business"`, so send `"creator"` on every reconnect of a creator connection. ## Creator info `GET /v1/accounts/{id}/creator-info` returns the settings TikTok applies to the account's next post. Read it before you publish. It needs the `accounts:read` scope. ```bash theme={null} curl https://api.social-api.ai/v1/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/creator-info \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "platform": "tiktok", "nickname": "Acme Corp", "avatar_url": "https://p16-sign.tiktokcdn-us.com/avatar.jpeg", "can_post": true, "max_video_duration_sec": 600, "privacy_level_options": ["PUBLIC_TO_EVERYONE"], "interaction_settings": { "comment_disabled": false, "duet_disabled": false, "stitch_disabled": false } } ``` | Field | Description | | - | - | | `can_post` | `true` when TikTok lists `PUBLIC_TO_EVERYONE` for the account, `false` when it does not. On a [creator connection](#creator-connection): `true` when TikTok lists any level | | `max_video_duration_sec` | The longest video this account can post, in seconds. Absent when TikTok returns no value | | `privacy_level_options` | `["PUBLIC_TO_EVERYONE"]`. Absent when `can_post` is `false`. It never lists another level, because every video publishes publicly. Photo carousels follow a different rule: see [TikTok publishing](/platforms/tiktok/publishing#privacy-level-differs-for-videos-and-photos). On a [creator connection](#creator-connection) it lists every level TikTok allows for the account | | `interaction_settings` | `true` for a setting the creator turned off in TikTok. A post cannot turn that interaction back on | | `avatar_url` | A temporary link from TikTok. Download the image if you need it later | Other platforms return `501 resource.not_supported` from this endpoint. ## Account summary `GET /v1/platforms/tiktok/accounts/{id}/summary` reads the account's profile and counts from TikTok at the time of the call. It needs the `accounts:read` scope, and the account needs the `analytics.read` [permission](/guides/permissions): without it the call returns `403 account.permission_missing`. ```bash theme={null} curl https://api.social-api.ai/v1/platforms/tiktok/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/summary \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "display_name": "Acme Corp", "username": "acmecorp", "avatar_url": "https://p16-sign.tiktokcdn-us.com/avatar.jpeg", "follower_count": 12400, "following_count": 87, "likes_count": 980000, "video_count": 47, "bio_description": "Spring collection out now.", "profile_deep_link": "https://www.tiktok.com/@acmecorp", "is_verified": false } ``` When the account's authorization lacks a scope for these fields, TikTok refuses the call and SocialAPI returns `403` with the TikTok code in `platform.tiktok.` (`40001`, `40118`, or `40125`). Reconnect the account to grant the scope. ## Sync videos `GET /v1/posts?platform=tiktok&account_ids={id}` lists the TikTok posts SocialAPI has stored for the account: public videos and photo posts, including posts published outside SocialAPI. Each target carries `permalink`, `media_type` (`VIDEO` or `PHOTO`), and `metrics`. SocialAPI refreshes an account's stored posts on its own, no more than once every 15 minutes. The account needs the `posts.read` [permission](/guides/permissions). Without it, both sync endpoints return `403 account.permission_missing` and SocialAPI refreshes nothing on its own. `POST /v1/platforms/tiktok/accounts/{id}/sync` refreshes them at once from the 20 most recent posts on TikTok. It needs the `accounts:manage` scope and has no cooldown. `POST /v1/accounts/{id}/sync` reads the same 20 posts and allows 1 sync per account every 5 minutes: see [Analytics](/guides/analytics#refresh-an-accounts-posts). ```bash theme={null} curl -X POST https://api.social-api.ai/v1/platforms/tiktok/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/sync \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "success": true } ``` The two endpoints that return counts use different keys for the view count: | Endpoint | Likes, comments, shares | Views | | - | - | - | | `GET /v1/posts` and `GET /v1/posts/{pid}` | `metrics.likes`, `metrics.comments`, `metrics.shares`. `metrics.saves` is always `0` | `metrics.extra.view_count`. Absent while the count is `0` | | `GET /v1/posts/{pid}/metrics` | The same three keys, with `metrics.saves` always `0` | `metrics.views` and `metrics.extra.views`, with the same value. Absent while the count is `0` | TikTok updates likes, comments, shares, and views with a delay of 24 to 48 hours. A call reads TikTok's latest figures, so a new post shows `0` until TikTok publishes them. Reach, saves, and watch time are not available. `GET /v1/posts/{pid}/metrics` reads the account's 20 most recent posts and reuses that read for 10 minutes. For an older post, the target returns `404 resource.not_found`. Examples are in [TikTok publishing](/platforms/tiktok/publishing#6-retrieve-posts). ## Comments SocialAPI reads, writes, and moderates comments on the account's own TikTok posts through the shared [Comments](/guides/comments) endpoints. This section holds only what differs on TikTok. * **IDs.** `{postId}` is the TikTok video ID, which is the target's `platform_post_id`. A comment's `platform_id` is `{video_id}_{comment_id}`, and its `id` is the `sapi_cmt_` ID. Use `id` or `platform_id` as `{commentId}`. Hide, unhide, reply, and list replies need that full form: a bare TikTok comment ID returns `400 validation.comment_id_invalid`. Delete and like accept either form. * **List.** `GET /v1/inbox/comments/{postId}` returns up to 30 comments from TikTok per page of results, and the replies endpoint returns up to 30 replies per page of results. TikTok leaves hidden comments out of its list. Hidden comments added as described below are in addition to TikTok's items, so the first page of results can hold more than 30. A top-level comment that appeared in this list and that you then hid with `POST /v1/inbox/comments/{postId}/{commentId}/hide` is added at the end of the first page of results, with `is_hidden: true`, when all three hold: that page has `pagination.has_more: false`, the request sends no `q`, and `order` and `moderation_status` are absent or set to their defaults (`order=recent`, `moderation_status=published`). That page can then hold more items than `limit`. Send `limit=30`, the largest page TikTok returns: if the first page still has `pagination.has_more: true`, hidden comments are not listed on any page. A comment hidden in the TikTok app is not added back. Keep a comment's `id` or `platform_id` when you hide it: where hidden comments are not listed, that ID is how you unhide it through [Hide and unhide a comment](/guides/comments#hide-and-unhide-a-comment). A reply to a hidden comment cannot be listed. Comment text only: images in comments are not returned. * **`GET /v1/inbox/comments`.** It lists the account's posts that have comments, with their comment counts. A read refreshes that list when it is older than 1 hour. * **Reply.** `POST /v1/inbox/comments/{postId}` with `comment_id` replies to that comment. Without `comment_id` it comments on the post as the connected account. The text holds up to 1,200 characters, and a reply costs 1 interaction credit. * **Hide and unhide.** For comments written by other people. `capabilities.can_hide` is `false` on the connected account's own comments. * **Delete.** For comments the connected account wrote. `capabilities.can_delete` is `false` on other people's comments. Hide those instead. * **Like.** `POST` and `DELETE` on `/v1/inbox/comments/{postId}/{commentId}/like` like and unlike as the connected account. `liked_by_viewer` is `true` when the connected account liked the comment. * **Private replies.** TikTok has none: `501 resource.not_supported`, and `can_private_reply` is `false`. * **Author fields.** `author_id` is TikTok's unique identifier for the person, `author_name` is the display name (the handle when TikTok gives none), and `author_username` is the handle. * **Replies.** A reply has `parent_id` set to the `platform_id` of the comment it answers. TikTok limits each account to 40 requests per minute for each TikTok call. Beyond that, calls return `429 platform.tiktok.rate_limit`. ## Mentions `GET /v1/accounts/{id}/mentions` lists the public posts that mention the account in the caption and the comments that mention it. TikTok serves mentions for TikTok Business Accounts only. For another account, TikTok refuses the call and SocialAPI returns the TikTok code as `platform.tiktok.`. The shared endpoints are in [Mentions](/guides/mentions). * **Delay.** TikTok makes a mention available 24 to 48 hours after it is made, and 24 hours after the account is connected. TikTok asks apps to read each mention list at most once per account per day, after 07:00 UTC. * **Page size.** A page holds up to 20 mentions, split between caption mentions and comment mentions. Newest first within a page. Follow `next_cursor` for the rest. * **Item fields.** `author.username` is the handle of the person, `content.text` is the caption or comment text, `platform_post_id` is the TikTok video ID, and `permalink` is the video link. `metadata` is `null`. A comment mention's `id` carries both the video and the comment. * **Reply.** `POST /v1/accounts/{id}/mentions/reply` works for a mention made in a comment. Send the mention's `sapi_mnt_` ID as `media_id`, or send the video ID as `media_id` with the comment's `platform_id` as `comment_id`. For a mention in a caption, TikTok has no reply: the call returns `400 validation.mention_reply_unsupported`. A reply costs 1 interaction credit. * **Events.** TikTok sends SocialAPI no mention notifications. Each call that returns at least one mention sends a `mention.received` event: see [Mentions you list](/guides/webhook-events#mentions-you-list). ## Webhook events | Event | When it fires | | - | - | | `comment.received` | Another person comments on, or replies under, a TikTok post of the account. See [`comment.received`](/guides/webhook-events#comment-received) | | `post.published`, `post.partial`, `post.failed` | A post to this account finished: see [Delivery](/guides/delivery). A TikTok post can finish after the request that created it returned | | `account.connected` | You connected a TikTok account | | `account.disconnected` | SocialAPI flagged the account for reconnection: see [When TikTok authorization ends](#when-tiktok-authorization-ends). On a [creator connection](#creator-connection), the person removed SocialAPI's app and SocialAPI removed the account | | `account.updated` | The account lost [permissions](/guides/permissions) you did not remove yourself (`change` is `permissions_reduced`) | `comment.received` details on TikTok: * **Author.** `author.id` is TikTok's unique identifier for the person. `author.name`, `author.username`, and `author.avatar_url` come from the comment. * **Post.** `platform_post_id` is the TikTok video ID. `post_id` and `permalink` are not set. * **Not sent.** Comments written by the connected account, and TikTok's deletions and visibility changes. A comment that TikTok no longer lists when SocialAPI reads it, for example one deleted within seconds, produces no event. * **Timing.** TikTok reports a new comment to SocialAPI within 5 minutes of the comment. * **Raw payload.** `raw_payload` holds the TikTok notification. TikTok has no DM or review events. Listing mentions sends `mention.received`: see [Mentions you list](/guides/webhook-events#mentions-you-list). Every event is described in [Webhook events](/guides/webhook-events). ## Limits | Limit | Value | | - | - | | Video caption | 2,200 UTF-16 characters | | Photo title (the post `text`) | 90 UTF-16 characters | | Video file | MP4, MOV, or WebM, 3 seconds to the account's `max_video_duration_sec`, up to 1 GB | | Photo carousel | 2 to 35 images, JPEG or WebP, up to 20 MB each | | Posts, SocialAPI's cap | 13 per 24 hours and 25 per hour for each account: see [Posting velocity caps](/guides/rate-limits#posting-velocity-caps) | | Posts, TikTok's own limit | 6 per minute and 15 per day for each account, for the video call and for the photo call. SocialAPI does not enforce the 6 per minute: TikTok refuses a 7th post within a minute | | Comment or reply text | 1,200 characters | | Posts per sync | 20 | | Comments or replies per page | 30 | | Mentions per page | 20 | | TikTok's request limit | 40 requests per minute for each account and each TikTok call. `429 platform.tiktok.rate_limit`. Wait 1 minute, then retry. Credit rules: see [when a credit comes back](/guides/plans#when-a-credit-comes-back) | ## Scopes SocialAPI's TikTok app asks the person for these scopes on TikTok's consent screen. You apply for no app review. On the default connection, the consent screen is the same whichever [permissions](/guides/permissions) you select, and the account is limited to your selection. A creator connection asks for other scopes: see [Creator connection](#creator-connection). | Scope | Used for | | - | - | | `user.info.basic` | Display name and avatar | | `user.info.username` | Handle | | `user.info.profile` | Bio, profile link, and verification | | `user.info.stats` | Follower, following, likes, and video counts | | `user.account.type` | Whether the account is a TikTok Business Account | | `user.insights` | TikTok's daily account insights. No SocialAPI endpoint returns them | | `video.list` | List the account's posts and their counts | | `video.insights` | TikTok's per-post insights, such as watch time. No SocialAPI endpoint returns them | | `video.publish` | Publish videos and photo carousels as the account | | `video.upload` | Send a post to the account's TikTok drafts | | `comment.list` | Read comments and replies, and receive `comment.received` | | `comment.list.manage` | Reply to, hide, like, and delete comments, and reply to comment mentions | | `biz.brand.insights` | List mentions | ## Related * [TikTok publishing](/platforms/tiktok/publishing): fields, media rules, and quirks for publishing * [Comments](/guides/comments): the shared comment endpoints * [Mentions](/guides/mentions): the shared mention endpoints * [Connect accounts](/guides/connect-accounts): the shared connect flow * [Track post delivery](/guides/delivery): statuses and the `publishing` state * [Platforms](/platforms/overview): the capability matrix * [Errors](/guides/errors): every error code # TikTok publishing Source: https://docs.social-api.ai/platforms/tiktok/publishing Full reference for publishing TikTok videos and photo carousels via SocialAPI, including privacy rules, drafts, metrics, and the publishing status. ## 1. Overview TikTok accepts a post as a job and finishes it later, so a TikTok target stays `publishing` until TikTok reports the post complete or failed. A video publishes publicly. A photo carousel needs a `privacy_level`. A [creator connection](/platforms/tiktok#creator-connection) follows other privacy and draft rules, listed on that section. | Field | Value | | - | - | | Platform slug | `tiktok` | | Auth type | OAuth 2.0 (TikTok) | | API | TikTok API for Business (Accounts API) | | Create post | Yes (video, photo carousel, or a draft in the TikTok app) | | Update post | No (`409 post.state_invalid` on a published post) | | Delete post | No (`outcome: not_supported`) | | Schedule | Yes (deferred publish via `scheduled_at`) | | First comment | No (`first_comment` is ignored) | | Posting caps | 13 posts per 24 hours and 25 per hour for each account: see [Posting velocity caps](/guides/rate-limits#posting-velocity-caps) | *** ## 2. Supported media | Content type | How to trigger | Notes | | - | - | - | | Single video | `media_type: "video"`, or omit it for one video | One `media` entry. MP4, MOV, or WebM. Publishes publicly | | Photo carousel | `media_type: "photo"`, or omit it for 2 or more images | 2 to 35 images. JPEG or WebP only. Needs `privacy_level` | | Draft in the TikTok app | `post_mode: "inbox"` | Video or photo carousel. The creator finishes the post in the TikTok app | Single images are not supported, and a post cannot mix a video with images. When you omit `media_type`, 2 or more images with no video publish as a photo carousel, and anything else publishes as a video. **File limits for media you send to SocialAPI** (`source_type: "media_id"` or `"url"`): videos must be MP4, MOV, or WebM, at most 1 GB, and at least 3 seconds long. Images must be JPEG or WebP and at most 20 MB each. A file outside these limits fails validation with `400 publishing.validation_failed`. The longest video depends on the account: read `max_video_duration_sec` from [Creator info](/platforms/tiktok#creator-info). SocialAPI checks that limit when it knows the video's duration, and a longer video fails validation the same way. **TikTok's own media rules.** SocialAPI does not check these before it sends the post. A file outside them fails the target with `422 platform.tiktok.publish_failed`, and `message` holds TikTok's reason, such as `frame_rate_check_failed` or `picture_size_check_failed`. | Media | Rule | | - | - | | Video | Width and height of at least 360 pixels. Frame rate from 23 to 60 FPS | | Photo | Each image at most 1080 x 1920 or 1920 x 1080 pixels | **Text limits:** a video caption holds 2,200 UTF-16 characters, with at most 30 mentions. For a photo carousel, the post `text` is the title and holds 90 UTF-16 characters. > [!WARNING] > > Attach media through the `media` array. Each item sets a `source_type`: `media_id` for a file you uploaded to SocialAPI, or `url` for a public `http(s)` URL. TikTok pulls the file while the post publishes, so keep the file reachable until the target is `published`. A `platform_attachment_id` source returns `422 publishing.media.unsupported_source` on TikTok. To upload first, see [Media](/guides/media). *** ## 3. Create post Use `POST /v1/posts` with `targets` pointing at a TikTok account and `publish_now: true`. The `text` field becomes the caption. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "My latest video #fyp", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "publish_now": true, "platform_data": { "tiktok": { "disable_duet": false, "disable_stitch": false, "video_cover_timestamp_ms": 3000 } } }' ``` Response (HTTP 201): ```json theme={null} { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "text": "My latest video #fyp", "status": "publishing", "hidden": false, "retry_count": 0, "created_at": "2026-10-13T09:00:00Z", "updated_at": "2026-10-13T09:00:00Z", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "tiktok", "status": "publishing" } ] } ``` The target settles later: see [Slow publishes and the `publishing` status](#slow-publishes-and-the-publishing-status). ### Photo carousel ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "Swipe through the highlights", "media": [ { "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }, { "source_type": "media_id", "source": "8d5e9b2c-1a3f-4c6d-9e8b-2f7a4c1d6e0b" }, { "source_type": "media_id", "source": "3b1f7a90-6c2d-4e85-b9a1-0d4f8c3e2a17" } ], "publish_now": true, "platform_data": { "tiktok": { "media_type": "photo", "privacy_level": "PUBLIC_TO_EVERYONE", "disable_comment": false, "photo_cover_index": 1 } } }' ``` ### Draft in the TikTok app Set `post_mode` to `"inbox"` to send a video or a photo carousel to the creator's TikTok drafts. TikTok then asks the creator for the audience, interaction settings, and disclosures in the app, so SocialAPI ignores `privacy_level`, the `disable_*` fields, `auto_add_music`, the branding fields, `is_aigc`, and `video_cover_timestamp_ms` on a draft. A video draft carries no caption: the creator writes it in the app. A photo draft keeps the post `text` as its title. A draft needs the `video.upload` scope, and it returns no public post ID and no permalink. ```json theme={null} { "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "publish_now": true, "platform_data": { "tiktok": { "post_mode": "inbox" } } } ``` ### Platform data fields Send these keys in `platform_data.tiktok` on the post, which applies them to every TikTok target. To set them for one target, send them flat in that target's `platform_data` (`targets[].platform_data`, with no `tiktok` key). When both set the same key, the target's value wins. | Key | Type | Required | Applies to | Notes | | - | - | - | - | - | | `privacy_level` | string | Photo only. Video and photo on a creator connection. Not required on a draft | video, photo | Video: omit it or send `PUBLIC_TO_EVERYONE`. Any other value fails with `400 validation.privacy_level_unsupported`. Photo: required, and it must be a level TikTok allows for the account. See [Privacy level differs for videos and photos](#privacy-level-differs-for-videos-and-photos). Ignored on a draft. On a [creator connection](/platforms/tiktok#creator-connection), required on a published video and on a published photo carousel | | `media_type` | string | No | video, photo | `"video"` or `"photo"`, case-insensitive. Any other value fails validation on the `media_type` field | | `post_mode` | string | No | video, photo | `"direct"` (default) publishes the post. `"inbox"` sends a draft. Case-insensitive. Any other value fails with `400 validation.post_mode` | | `brand_content_toggle` | boolean | No | video, photo | Marks the post as paid partnership content. A photo cannot combine it with `SELF_ONLY` | | `brand_organic_toggle` | boolean | No | video, photo | Marks the post as promoting the creator's own brand | | `disable_comment` | boolean | No | video, photo | Turns comments off. When omitted: `false` (comments on) on the default connection, `true` (comments off) on a [creator connection](/platforms/tiktok#creator-connection). A creator who turned comments off in TikTok forces `true` | | `disable_duet` | boolean | No | video | Turns Duet off. When omitted: `false` on the default connection, `true` on a creator connection. A creator who turned Duet off forces `true`. Ignored on a photo carousel | | `disable_stitch` | boolean | No | video | Turns Stitch off. When omitted: `false` on the default connection, `true` on a creator connection. A creator who turned Stitch off forces `true`. Ignored on a photo carousel | | `is_aigc` | boolean | No | video | Labels the video as AI-generated content. Ignored on a photo carousel | | `video_cover_timestamp_ms` | number, 0 or more | No | video | Millisecond offset of the cover frame. An offset past the end of the video fails the target | | `photo_cover_index` | number, 0 or more | No | photo | 0-based index of the cover image. Defaults to `0` | | `auto_add_music` | boolean | No | photo | Adds music TikTok recommends | SocialAPI forwards any additional keys in `platform_data.tiktok` that the table above lists, and no others. TikTok has no other publish options that SocialAPI passes through: an unlisted key is ignored without a warning. ### Creator info and validation The longest video and the interaction locks differ per account. Call `GET /v1/accounts/{id}/creator-info` first ([Creator info](/platforms/tiktok#creator-info)), then dry-run the post with `POST /v1/posts/validate`: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts/validate \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "My post caption", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }] }' ``` Response (HTTP 200): ```json theme={null} { "valid": true, "errors": [], "warnings": [] } ``` When TikTok cannot be reached for the check, `warnings` holds one entry on the field `creator_info`, and the photo privacy check and the video duration check are skipped. The media rules and the `post_mode` and `privacy_level` checks still run. ### Scheduling Set `scheduled_at` to an ISO 8601 timestamp (UTC) instead of `publish_now`. SocialAPI holds the post and publishes it at that time. A photo carousel still needs `privacy_level`. On a creator connection, a video needs it too. ```json theme={null} { "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "Scheduled video", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "scheduled_at": "2026-11-15T10:00:00Z" } ``` *** ## 4. Update post TikTok gives apps no edit call. `PATCH /v1/posts/{pid}` edits only a post in `draft`, `scheduled`, or `failed` status, so a published TikTok post returns `409 post.state_invalid`. Delete the post in the TikTok app and publish a new one. *** ## 5. Delete post TikTok gives apps no deletion call. `DELETE /v1/posts/{pid}` returns HTTP 200 with `results[].outcome: "not_supported"` for the TikTok target, and the post stays on TikTok. `POST /v1/posts/{pid}/unpublish` returns `501 resource.not_supported` for a TikTok target. Delete the post in the TikTok app. For a cross-platform post, the other platforms are still deleted or unpublished. See [Delete a post](/guides/delivery#delete-a-post). *** ## 6. Retrieve posts `GET /v1/posts?platform=tiktok&account_ids={id}` returns the posts SocialAPI has stored for the account, including posts published outside SocialAPI. A post published through SocialAPI appears when its target is `published`. `POST /v1/platforms/tiktok/accounts/{id}/sync` reads the 20 most recent posts from TikTok immediately: see [Sync videos](/platforms/tiktok#sync-videos). Response (HTTP 200) for one stored video: ```json theme={null} { "data": [ { "id": "3f1c9a7e-8b2d-4c5e-9a61-7d0e2b4f8a13", "text": "My latest video #fyp", "status": "published", "hidden": false, "retry_count": 0, "published_at": "2026-10-12T15:20:00Z", "created_at": "2026-10-13T09:00:00Z", "updated_at": "2026-10-13T09:00:00Z", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "tiktok", "media_type": "VIDEO", "status": "published", "platform_post_id": "7380000000000000001", "permalink": "https://www.tiktok.com/@acmecorp/video/7380000000000000001", "published_at": "2026-10-12T15:20:00Z", "metrics": { "likes": 342, "comments": 18, "shares": 27, "saves": 0, "extra": { "view_count": 5120 } }, "metrics_synced_at": "2026-10-13T09:00:00Z" } ] } ], "pagination": { "limit": 25, "has_more": false } } ``` `GET /v1/posts/{pid}/metrics` reads the current counts from TikTok. It reads the account's 20 most recent posts and reuses that read for 10 minutes. For a post outside those 20, the target returns `404 resource.not_found`. A draft has no `platform_post_id`, so its target is not in the response. ```json theme={null} { "data": { "post_id": "3f1c9a7e-8b2d-4c5e-9a61-7d0e2b4f8a13", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "tiktok", "status": "", "metrics": { "likes": 342, "comments": 18, "shares": 27, "saves": 0, "views": 5120, "extra": { "views": 5120 } }, "metrics_synced_at": "2026-10-13T12:00:00Z" } ] } } ``` The view count has a different key on each endpoint: `metrics.extra.view_count` on `GET /v1/posts`, and `metrics.views` with `metrics.extra.views` on `GET /v1/posts/{pid}/metrics`. Each is absent while the count is `0`. TikTok updates the counts with a delay of 24 to 48 hours. Reach, saves, and watch time are not available. *** ## 7. Quirks, errors, and recovery ### Privacy level differs for videos and photos This section covers the default connection. A [creator connection](/platforms/tiktok#creator-connection) requires `privacy_level` on every published post, from the account's `privacy_level_options`. A draft needs none. Every video publishes publicly, and TikTok takes no privacy setting for it. Omit `privacy_level` on a video, or send `PUBLIC_TO_EVERYONE`. Any other value, or a value that is not a string, fails with `400 validation.privacy_level_unsupported`, and nothing is sent to TikTok. `meta.privacy_level` carries the value you sent. A photo carousel needs `privacy_level`. A photo without it fails with `400 validation.privacy_level_required`. `PUBLIC_TO_EVERYONE` is always the safe choice. TikTok can allow other levels for the account (`MUTUAL_FOLLOW_FRIENDS`, `FOLLOWER_OF_CREATOR`, `SELF_ONLY`), and a photo carousel accepts those. [Creator info](/platforms/tiktok#creator-info) lists only `PUBLIC_TO_EVERYONE`, so it does not show them. A level the account does not allow fails with `400 validation.invalid_privacy_level`, which carries the value you sent in `meta.privacy_level`. A draft needs no `privacy_level`. ### Photo carousels need 2 to 35 images A photo post with fewer than 2 or more than 35 images fails with `400 validation.media_count`. TikTok has no single-image post. ### Media type must match the file TikTok will not build a video out of an image. A single image with no `media_type` resolves to a video, and a post like that fails with `400 validation.media_type_mismatch` before anything is sent. Set `media_type` to `"photo"` and attach 2 or more JPEG or WebP images. A `media_type` that is neither `video` nor `photo` fails with `400 validation.unsupported_media_type`. A video post with no media fails with `400 validation.media_required`. ### Photo posts accept only JPEG and WebP A photo carousel that holds a PNG, AVIF, GIF, or a video fails with `400 validation.unsupported_photo_format`, which names the offending format. Convert the images to JPEG or WebP before uploading. ### Photo titles hold 90 characters A photo carousel whose `text` is longer than 90 UTF-16 characters fails validation on the field `title`. ### disable\_duet and disable\_stitch are ignored on photo posts `disable_duet` and `disable_stitch` set to `true` on a photo carousel have no effect. Validation returns a warning on the field `interactions`, not an error. ### Branded content cannot be private On a photo carousel, `brand_content_toggle: true` with `privacy_level: "SELF_ONLY"` fails with `400 validation.branded_content_private`. ### Posting caps SocialAPI caps each account at 13 posts per 24 hours and 25 per hour: a post beyond either cap returns `429 publishing.velocity_limit`. See [Posting velocity caps](/guides/rate-limits#posting-velocity-caps). TikTok also limits each account to 6 posts per minute and 15 per day, for the video call and for the photo call. SocialAPI does not enforce the 6 per minute: TikTok refuses a 7th post within a minute. A post TikTok refuses fails the target: read `targets[].error.code` and `targets[].error.message`. ### Slow publishes and the `publishing` status TikTok often settles after the request that created the post returned, either because it is still processing the file or because it has not yet exposed the post ID. In both cases the target stays `status: "publishing"` with `platform_post_id` and `permalink` absent. SocialAPI then settles it on its own: `published` with its ID and permalink, or `failed` if TikTok rejected it. `publishing` is not terminal, so treat it as "still working": * **Do not confirm success on it.** Only `published` is a completed publish. * **Do not retry or republish it.** The post can already be live on TikTok, so a retry risks a duplicate post. * **Keep reading `GET /v1/posts/{pid}`, or wait for the webhook.** The `post.published`, `post.partial`, and `post.failed` events fire when the post reaches its final status, including after the original request returned. If TikTok reports the post live but has not returned its ID 2 hours after publishing started, the target settles at `published` without `platform_post_id` or `permalink`. A target TikTok still reports as processing 2 hours after publishing started fails with `500 system.internal`. Check the TikTok profile before retrying such a target: the post can still have gone live. A draft settles at `published` without `platform_post_id` or `permalink` once TikTok has placed it in the creator's TikTok inbox. ### Errors on a failed target A publish that fails puts a structured error on the target, in `targets[].error`, with its `code`. Every code is in [Errors](/guides/errors). * `401 platform.tiktok.auth`: TikTok's authorization for the account ended. Reconnect the account. Later calls return `403 account.reconnection_required`. * `401 platform.tiktok.unauthorized`: TikTok refused the call as unauthorized without saying the authorization ended. The account stays usable. Retry, and reconnect if it repeats. * `403 platform.tiktok.40001`, `403 platform.tiktok.40118`, `403 platform.tiktok.40125`: the authorization lacks a scope, such as `video.publish`. Reconnect the account. * `429 platform.tiktok.rate_limit`: TikTok's own request limit. Wait 1 minute, then retry. * `400 platform.tiktok.40002`: TikTok refused a parameter or a media file. The same status comes with the codes `40000`, `40011`, `40904` to `40908`, `40914`, and `40915`. `message` carries TikTok's reason. Fix the field it names. * `422 platform.tiktok.publish_failed`: TikTok rejected the post while processing it, or could not report the outcome. `message` carries TikTok's reason. * `502 platform.tiktok.api_error`: TikTok returned no usable answer, or accepted the post without a publish ID. Check the account in the TikTok app before retrying. * Any other TikTok code returns `502`, with that code after `platform.tiktok.` and TikTok's reason in `message`. ### Recovery * **Validation error.** Fix the field the issue names, then send the post again. * **Auth error.** Reconnect the TikTok account through the OAuth flow. * **Failed target.** Read `targets[].error`, then send `POST /v1/posts/{pid}/retry`. See [Delivery](/guides/delivery). *** ## 8. Full worked example This example reads creator info, validates the post, creates it, and reads the result. **Step 1: Read creator info** ```bash theme={null} curl https://api.social-api.ai/v1/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/creator-info \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Check `can_post` and `max_video_duration_sec`. A locked interaction in `interaction_settings` forces that setting off. **Step 2: Upload media** Upload the video and verify it as described in [Media](/guides/media). The result is a `media_id`, here `f47ac10b-58cc-4372-a567-0e02b2c3d479`. **Step 3: Validate** ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts/validate \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "Behind the scenes of our product shoot #bts #brand", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "platform_data": { "tiktok": { "brand_organic_toggle": true, "disable_comment": false, "disable_duet": true, "disable_stitch": true, "video_cover_timestamp_ms": 2000 } } }' ``` Response (HTTP 200): `{"valid": true, "errors": [], "warnings": []}` **Step 4: Create the post** ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "Behind the scenes of our product shoot #bts #brand", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "publish_now": true, "platform_data": { "tiktok": { "brand_organic_toggle": true, "disable_comment": false, "disable_duet": true, "disable_stitch": true, "video_cover_timestamp_ms": 2000 } } }' ``` Response (HTTP 201): the post with `status: "publishing"`, as in [Create post](#3-create-post). **Step 5: Check the status** ```bash theme={null} curl https://api.social-api.ai/v1/posts/7c9e6679-7425-40de-944b-e07fc1f90ae7 \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200) once TikTok finished: ```json theme={null} { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "text": "Behind the scenes of our product shoot #bts #brand", "status": "published", "hidden": false, "retry_count": 0, "published_at": "2026-10-13T09:00:47Z", "created_at": "2026-10-13T09:00:00Z", "updated_at": "2026-10-13T09:00:47Z", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "tiktok", "status": "published", "platform_post_id": "7380000000000000001", "permalink": "https://www.tiktok.com/@acmecorp/video/7380000000000000001", "published_at": "2026-10-13T09:00:47Z" } ] } ``` If TikTok rejects the file while processing, the target is `failed` with `error`. Read it, fix the cause, then send `POST /v1/posts/7c9e6679-7425-40de-944b-e07fc1f90ae7/retry`. *** ## 9. Required OAuth scopes SocialAPI's TikTok app requests these scopes during the OAuth flow. You configure nothing. The full list is in [TikTok scopes](/platforms/tiktok#scopes). Publishing uses `video.publish`, `user.info.username`, and, for `post_mode: "inbox"`, `video.upload`. Retrieving posts uses `video.list`. | Scope | Purpose | | - | - | | `video.publish` | Publish videos and photo carousels | | `video.upload` | Send a video or photo carousel to the creator's TikTok drafts | | `video.list` | Retrieve posts and their counts: see [Retrieve posts](#6-retrieve-posts) | | `user.info.username` | Build the permalink of a published post | The scopes a scoped API key needs, the credit cost of a post, and the MCP tools that publish are on [Publishing](/guides/publishing). # Trustpilot API Source: https://docs.social-api.ai/platforms/trustpilot Trustpilot is not available in SocialAPI: you cannot connect a Trustpilot account. Google Business Profile reads and replies to reviews. SocialAPI does not support Trustpilot. You cannot connect a Trustpilot account, and no endpoint reads or answers Trustpilot reviews. The platform slug `trustpilot` is reserved. ## At a glance | Field | Value | | - | - | | Platform slug | `trustpilot` | | Auth | None. Trustpilot cannot be connected | | Status | Not available | ## Capabilities | Feature | Status | Condition | | - | - | - | | Connect a Trustpilot account | No | `POST /v1/accounts/connect` with `platform: "trustpilot"` returns `403 billing.platform_not_available` | | Read Trustpilot reviews | No | No account to read from | | Reply to Trustpilot reviews | No | No account to reply from | | Publishing, comments, DMs, mentions | No | Trustpilot has no such surface in SocialAPI | ## Reviews on other platforms Google Business Profile and App Store Connect read reviews and send replies through SocialAPI: see [Google Business Profile reviews](/platforms/google/reviews) and [App Store Connect](/platforms/app-store-connect#reviews). [Supported platforms](/platforms/overview) shows which platforms support reviews. ## Related * [Supported platforms](/platforms/overview): what each platform supports * [Google Business Profile reviews](/platforms/google/reviews): reading and replying to reviews * [Connect accounts](/guides/connect-accounts): the platform slugs you can connect # X / Twitter API Source: https://docs.social-api.ai/platforms/twitter Connect X accounts through your own X app to publish posts and threads with media, read and moderate replies, send DMs, and read mentions. The X / Twitter API in SocialAPI connects an X account through an X developer app that you create. It publishes posts and threads with images, video, or a GIF, reads and moderates replies, reads and sends direct messages (DMs), and reads mentions. SocialAPI has no shared X app: X bills usage to the developer account that owns the app, so every call on your X accounts is billed to you by X. See [Why X uses your own app](/guides/platform-credentials#why-x-uses-your-own-app). ## At a glance | Field | Value | | - | - | | Platform slug | `twitter`. `x` is accepted as the same slug | | Auth | OAuth 2.0 with PKCE, through your own X app | | Platform API | X API v2 | | Status | Available | ## Capabilities An unsupported call returns `501 resource.not_supported`. | Feature | Status | Condition | | - | - | - | | Publish a post | Yes | Text, up to 4 images, 1 video, or 1 GIF. See [X publishing](/platforms/twitter/publishing) | | Publish a thread | Yes | `segments`. Continuation posts are text only | | Publish a reply to a post | Yes | `platform_data.twitter.in_reply_to_tweet_id` | | Schedule a post | Yes | `scheduled_at` on the post | | First comment on a post | No | `first_comment` is ignored. Publish a thread or a reply instead | | Update a published post | No | `PATCH /v1/posts/{pid}` returns `409 post.state_invalid` for a published post | | Delete a published post | Yes | | | List posts | Yes | The account's posts and replies, without reposts, with like and reply counts | | List comments and replies | Yes | Replies to a post from the last 7 days. Older replies are not returned | | Reply to a comment | Yes | | | Hide or unhide a comment | Yes | A reply to the account's own post | | Delete a comment | Conditional | Only a reply the account wrote. X refuses to delete anyone else's post | | Like a comment | No | X refuses like writes for apps on self-serve access. `can_like` is `false` | | Private reply to a comment | No | `501 resource.not_supported` | | DMs: list, read, send | Yes | Text only. See [Direct messages](#direct-messages) | | Mentions | Yes | Posts that mention the account. See [Mentions](/guides/mentions) | | Reply to a mention | No | `501 resource.not_supported` | | Reviews | No | X has no reviews. `GET /v1/inbox/reviews/{account_id}` returns `422 account.platform_unsupported` | | Automations | No | `422 automation.platform_unsupported` | | Analytics | Partial | Post metrics: Yes. Stored history: Partial. Live account summary: No. See [Analytics](/guides/analytics) | | Webhook events | Partial | `account.connected`, `account.updated` when the account loses [permissions](/guides/permissions) you did not remove yourself, and the `post.*` events. X sends SocialAPI no comment or DM events: read them with the inbox endpoints. Listing mentions sends `mention.received` ([Mentions you list](/guides/webhook-events#mentions-you-list)) | The cross-platform matrix is on [Platforms](/platforms/overview). ## Connect an account Save your X app's OAuth 2.0 Client ID and Client Secret in the SocialAPI dashboard first: [Use your own X app](/platforms/twitter/own-app). Then connect with `POST /v1/accounts/connect` and `"platform": "twitter"`. The shared flow, request fields, and result are in [Connect accounts](/guides/connect-accounts), and the redirect parameters are in [Redirect parameters](/guides/connect-accounts#redirect-parameters). ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/connect \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "twitter", "redirect_uri": "https://app.example.com/oauth/callback", "state": "session_abc123" }' ``` Response (HTTP 202): ```json theme={null} { "auth_url": "https://x.com/i/oauth2/authorize?response_type=code&client_id=Zk1hYmNkZWY&state=4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2", "state": "4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2", "message": "Open this link to connect your twitter account. Once you've authorized, the account will be connected automatically." } ``` X-specific rules: * **Saved app credentials.** Without them, `POST /v1/accounts/connect` returns `412 byok.credentials_missing`. When X rejects the saved credentials, or refuses your app access to its API, the connection ends on your redirect URI with the `error` parameter set to `byok.credentials_invalid` or `byok.app_access_denied`. * **One app per brand.** A brand can use a different X app from your default one: see [Use your own X app](/platforms/twitter/own-app#use-a-different-app-for-one-brand). * **Reconnect.** X access tokens last 2 hours and SocialAPI renews them. If renewal fails, for example because the person revoked access, the first call that fails returns `401 platform.twitter.auth` and the account is marked `token_expired`. Every later call that acts on the account returns `403 account.reconnection_required` until you reconnect it. See [Reconnect an account](/guides/connect-accounts#reconnect-an-account). * **Media scope.** Uploading media needs the `media.write` scope. An account connected without it fails an upload with `403 platform.twitter.insufficient_scope` until the person reconnects. ## Comments List replies to a post with `GET /v1/inbox/comments/{postId}`, where `postId` is the post's platform ID (`targets[].platform_post_id`). Reply, hide, unhide, and delete work as described in [Comments](/guides/comments). X-specific rules: * **7-day window.** X returns replies posted in the last 7 days. A reply to a post older than that does not appear. * **Hide.** Hide and unhide work on replies to the connected account's own posts. * **Delete.** Deleting works only on a reply the connected account wrote. * **Like.** X removed like writes for self-serve apps. Read `can_like` on each comment rather than assuming. ## Direct messages List conversations with `GET /v1/inbox/conversations`, read one with its messages, and send with the DM endpoints in [Direct messages](/guides/direct-messages). X-specific rules: * **Text only.** A DM sends text. An attachment is rejected before anything is sent. * **Recipient.** A send goes to the other person in the conversation. A group conversation has no single recipient, so a send to it is refused. * **X's own limit.** X allows 15 DM read requests per 15 minutes for each connected account. Past it, calls return `429 platform.twitter.rate_limit`. ## Limits | Limit | Value | | - | - | | Post text | 280 characters | | Media per post | 4 images, or 1 video, or 1 GIF | | Image | 5 MB. JPEG, PNG, GIF, or WebP | | Video | 512 MB and 140 seconds. MP4 | | Thread | Up to 25 continuation posts, each up to 280 characters | | Posts | 25 per hour and 50 per 24 hours for each account: see [Posting velocity caps](/guides/rate-limits#posting-velocity-caps) | | X's own rate limit | `429 platform.twitter.rate_limit`. Credit rules: see [when a credit comes back](/guides/plans#when-a-credit-comes-back) | | X credits and spending cap | `402 platform.twitter.usage_capped` when your X app is out of credits or reached its spending limit. Add credits in the X Developer Console | ## Scopes You choose the app permissions (Read and write, and Direct message) on your own X app, as described in [Use your own X app](/platforms/twitter/own-app). SocialAPI requests these scopes when the person connects the account. Each scope belongs to the account [permissions](/guides/permissions) in the last column, and the consent screen asks for the scopes of the permissions you selected. | Scope | Used for | Permissions | | - | - | - | | `tweet.read` | Read posts, replies, and mentions | Every account | | `tweet.write` | Publish and delete posts, reply to comments | `posts.write`, `comments.write` | | `tweet.moderate.write` | Hide and unhide replies | `comments.write` | | `users.read` | Read profiles | Every account | | `dm.read` | Read DM conversations and messages | `messages.read` | | `dm.write` | Send DMs | `messages.write` | | `like.write` | Like and unlike posts. X refuses these calls for self-serve apps | `comments.write` | | `media.write` | Upload images, video, and GIFs for a post | `posts.write` | | `offline.access` | Keep the connection without asking the person again | Every account | ## Related * [X publishing](/platforms/twitter/publishing): fields, media rules, threads, and quirks for publishing * [Use your own X app](/platforms/twitter/own-app): create the X app and save its credentials * [Platform apps](/guides/platform-credentials): which platforms use SocialAPI's app and which use yours * [Platforms](/platforms/overview): the capability matrix * [Errors](/guides/errors): every error code # Use your own X app Source: https://docs.social-api.ai/platforms/twitter/own-app Create an X developer app, save its OAuth 2.0 credentials in SocialAPI, and connect X accounts, with the exact settings and fixes for common errors. Connecting X to SocialAPI needs an X developer app that you create. SocialAPI has no shared X app, because X bills API usage to the developer account that owns the app, in pay-per-use credits, and X's free API tier is closed to new developer accounts. Every read and write on your X accounts is billed to your X developer account. See [Why X uses your own app](/guides/platform-credentials#why-x-uses-your-own-app). SocialAPI uses the app's OAuth 2.0 Client ID and Client Secret. When you create the app, X first shows a Bearer Token and OAuth 1.0 keys (Consumer Key, Access Token). SocialAPI does not use them. The OAuth 2.0 Client ID and Client Secret appear only after you finish step 2. ## Before you start * An X account with a verified email address and phone number. * A payment method for X pay-per-use credits. X sells no free tier to new developer accounts. * Access to the SocialAPI dashboard. Saving X credentials is a dashboard action: no API endpoint saves them. ## Set up the app **1. Create the developer account and the app** Sign in at [console.x.com](https://console.x.com) with your X account and accept the Developer Agreement. Describe your use case, add a payment method, and buy credits. Create a Project, then create an App inside it. X shows each credential once. Copy it when it appears, because X issues a new one instead of showing it again. **2. Set up user authentication** Open the app, find **User authentication settings**, and click **Set up**. Set all of these on the same form: * **App permissions:** Read and write and Direct message. * **Type of App:** Web App, Automated App or Bot. Both give the app a Client Secret. * **Callback URI / Redirect URL:** `https://api.social-api.ai/oauth/callback/twitter`. It must match exactly, with no trailing slash. * **Website URL:** your own site, or `https://social-api.ai`. Click **Save**. SocialAPI requests these scopes from each person who connects: `tweet.read`, `tweet.write`, `tweet.moderate.write`, `users.read`, `dm.read`, `dm.write`, `like.write`, `media.write`, and `offline.access`. The app permissions above let X grant them. Changing the app permissions later means each person reconnects. **3. Copy the OAuth 2.0 credentials** After you save, an **OAuth 2.0** section appears in the app. Copy the **Client ID** and the **Client Secret**. Store the secret somewhere safe. **4. Save the credentials in SocialAPI** In the SocialAPI dashboard, open **Settings** > **Integrations** > **Twitter integration**. Under **Default credentials**, click **Add credentials**. Paste the Client ID and the Client Secret, click **Test credentials** to check them with X, then click **Save and continue**. SocialAPI never returns the Client Secret from any endpoint. It shows only the start of the Client ID. **5. Connect an account** Call `POST /v1/accounts/connect` with `"platform": "twitter"`, as in [Connect an X account](/platforms/twitter#connect-an-account). ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/connect \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "twitter", "redirect_uri": "https://app.example.com/oauth/callback", "state": "session_abc123" }' ``` Send the person to the returned `auth_url`. Without saved credentials, `POST /v1/accounts/connect` returns `412 byok.credentials_missing`. ## Use a different app for one brand Save a second X app for a single brand, for example one client's brand in an agency account, from the same dashboard page. Under **Brand overrides**, click **Override** on the brand's row, paste that app's Client ID and Client Secret, then click **Save and continue**. A brand with its own app connects through that app. Every other brand uses your default app. Click **Remove** on the brand's row to return that brand to the default. ## Replace the client secret After you regenerate the Client Secret in X, click **Rotate** under **Default credentials**, or on the brand's row under **Brand overrides**, and save the new secret the same way as in step 4. Until you do, a new connection ends on your redirect URI with `error=byok.credentials_invalid`. ## Troubleshooting > [accordion] Test credentials fails, or connecting ends with error=byok.credentials\_invalid > > X rejected the saved Client ID and Client Secret. Check that you pasted the OAuth 2.0 **Client ID** and **Client Secret** from the app's **OAuth 2.0** section, not the Consumer Key, the API Secret, or the Bearer Token that X shows when you create the app. If you regenerated the secret in X, save the new one. > [accordion] POST /v1/accounts/connect returns 412 byok.credentials\_missing > > No X app is saved for the brand and no default is saved. Save the credentials as in step 4, then connect again. > [accordion] Connecting ends with error=byok.app\_access\_denied > > X refused your app access to its API. The app's access level on X does not allow the call. Check your X developer account, its credits, and the app's access level in the X Developer Console. > [accordion] X shows a callback URL mismatch > > The callback URL on your X app differs from the one SocialAPI sends. Set it to `https://api.social-api.ai/oauth/callback/twitter` exactly, with no trailing slash and no extra path. > [accordion] The connection works but calls return 403 platform.twitter.insufficient\_scope > > The app permissions on X are lower than Read and write and Direct message, or the account connected before you raised them. Set the permissions as in step 2, then reconnect the account. See [Reconnect an account](/guides/connect-accounts#reconnect-an-account). > [accordion] Calls return 402 platform.twitter.usage\_capped > > Your X developer account is out of credits or reached its spending limit. Add credits or raise the spending limit in the X Developer Console. > [accordion] Calls return 429 platform.twitter.rate\_limit > > X's own rate limit applies to each connected account. Wait, then retry. SocialAPI's own limits are in [Rate limits](/guides/rate-limits). ## Related * [X / Twitter API](/platforms/twitter): capabilities, limits, and scopes * [Platform apps](/guides/platform-credentials): which platforms use SocialAPI's app and which use yours * [Connect accounts](/guides/connect-accounts): the shared connect flow * [Errors](/guides/errors): every error code # X / Twitter publishing Source: https://docs.social-api.ai/platforms/twitter/publishing Full reference for publishing X posts via SocialAPI, including images, video, GIFs, threads, replies, reply settings, and platform-specific parameters. ## 1. Overview X posts publish through your own X app, so X bills each write to your X developer account. A post carries text and up to 4 images, 1 video, or 1 GIF. A thread chains text-only posts after the first one. | Field | Value | | - | - | | Platform slug | `twitter` (`x` is accepted as the same slug) | | Auth type | OAuth 2.0 with PKCE, through your own X app | | API | X API v2 | | Create post | Yes | | Update post | No (`409 post.state_invalid` on a published post) | | Delete post | Yes | | Schedule | Yes (deferred publish via `scheduled_at`) | | First comment | No (`first_comment` is ignored) | | Posting caps | 25 posts per hour and 50 per 24 hours for each account: see [Posting velocity caps](/guides/rate-limits#posting-velocity-caps) | *** ## 2. Supported media | Content type | How to trigger | Notes | | - | - | - | | Text only | No `media` | Up to 280 characters | | Images | 1 to 4 image entries in `media` | JPEG, PNG, GIF, or WebP, at most 5 MB each | | Video | One MP4 entry in `media` | At most 512 MB and 140 seconds. A video takes the whole post | | GIF | One GIF entry in `media` | A GIF takes the whole post | | Thread | `segments` on the post | Each continuation post is text only | A post holds up to 4 images, or exactly one video, or exactly one GIF, never a mix. More than 4 items, a file type X does not accept, or a file too large to upload returns `400 publishing.validation_failed`. With `skip_validation: true`, the target fails at publish time with `400 validation.media_count`, `400 validation.media_type`, or `400 validation.media_size`. **File limits for media you upload to SocialAPI** (`source_type: "media_id"`): the limits in the table apply, and a file outside them fails validation with `400 publishing.validation_failed`. SocialAPI uploads the media to X before it creates the post. If an upload fails, the whole post fails: SocialAPI never publishes the text without its media. > [!WARNING] > > Attach media through the `media` array. Each item sets a `source_type`: `media_id` for a file you uploaded to SocialAPI, `url` for a public `http(s)` URL SocialAPI fetches at publish time, or `platform_attachment_id` for a media ID you created through X yourself. To upload first, see [Media](/guides/media). Uploading needs the `media.write` scope: an account connected without it fails with `403 platform.twitter.insufficient_scope` until the person reconnects. *** ## 3. Create post Use `POST /v1/posts` with `targets` pointing at an X account and `publish_now: true`. The `text` field becomes the post text. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "Spring collection is live.", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "publish_now": true, "platform_data": { "twitter": { "reply_settings": "following", "made_with_ai": false } } }' ``` Response (HTTP 201): ```json theme={null} { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "text": "Spring collection is live.", "status": "publishing", "hidden": false, "retry_count": 0, "created_at": "2026-10-13T09:00:00Z", "updated_at": "2026-10-13T09:00:00Z", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "twitter", "status": "publishing" } ] } ``` ### Platform data fields Send these keys in `platform_data.twitter` on the post, which applies them to every X target. To set them for one target, send them flat in that target's `platform_data` (`targets[].platform_data`, with no `twitter` key). When both set the same key, the target's value wins. All keys are optional. | Key | Type | Notes | | - | - | - | | `reply_settings` | string | Who can reply. One of `following`, `mentionedUsers`, `subscribers`, `verified`. Omit it to let everyone reply | | `geo_place_id` | string | Attaches a location by X place ID | | `community_id` | string | Posts into an X Community | | `for_super_followers_only` | boolean | Restricts the post to Super Followers | | `nullcast` | boolean | Publishes a promoted-only post that does not appear in the timeline | | `made_with_ai` | boolean | Labels the post as AI-generated | | `paid_partnership` | boolean | Discloses branded content | | `in_reply_to_tweet_id` | string | Publishes the post as a reply to this post | | `auto_populate_reply_metadata` | boolean | Adds the @mentions of the post you reply to. Needs `in_reply_to_tweet_id` | | `exclude_reply_user_ids` | array of strings | User IDs to leave out of the auto-added @mentions. Needs `in_reply_to_tweet_id` | | `thread` | array of objects | Continuation posts as `{"text": "..."}` objects. Equivalent to `segments`: send one of the two | SocialAPI forwards any additional keys in `platform_data.twitter` that the table above lists, and no others. X has no other publish options that SocialAPI passes through: an unlisted key is not sent, and validation returns a warning naming the key. A value of the wrong type, an unlisted `reply_settings` value, or a thread entry that is empty or longer than 280 characters fails validation on that field, and `POST /v1/posts/validate` reports it before anything is published. `auto_populate_reply_metadata` or `exclude_reply_user_ids` without `in_reply_to_tweet_id` returns a warning, because it has no effect. ### Threads Set `segments` to publish a thread. The post's `text` is the first post, and each segment is posted as a reply to the one before it. A thread holds up to 25 continuation posts, each up to 280 characters. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "1/ A thread about our launch.", "segments": [ { "text": "2/ First, the backstory." }, { "text": "3/ And here is what is next." } ], "publish_now": true }' ``` Continuation posts are text only: media on a segment is dropped, with a validation warning. The `platform_data.twitter` settings apply to the first post only. The target's `platform_post_id` is the first post's ID. Each post in a thread is a separate X write that X bills to you. ### Reply to a post Set `platform_data.twitter.in_reply_to_tweet_id` to publish a reply: ```json theme={null} { "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "Thanks for asking: yes, it ships worldwide.", "publish_now": true, "platform_data": { "twitter": { "in_reply_to_tweet_id": "1846123456789012345" } } } ``` ### Scheduling Set `scheduled_at` to an ISO 8601 timestamp (UTC) instead of `publish_now`. SocialAPI holds the post and publishes it at that time. ```json theme={null} { "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "Scheduled post", "scheduled_at": "2026-11-15T10:00:00Z" } ``` *** ## 4. Update post SocialAPI offers no edit of a published X post. `PATCH /v1/posts/{pid}` edits only a post in `draft`, `scheduled`, or `failed` status, and returns `409 post.state_invalid` for a published post. To correct a published post, delete it and publish a new one. *** ## 5. Delete post `DELETE /v1/posts/{pid}` deletes the post from X when its X target is `published`. For a thread it deletes the first post only: the continuation posts stay on X until you delete them there. See [Delete a post](/guides/delivery#delete-a-post). *** ## 6. Retrieve posts `GET /v1/posts?platform=twitter&account_ids={id}` lists the account's posts and replies, without reposts. Each published target carries `platform_post_id`, `permalink`, and `metrics` with `likes` and `comments` (the reply count). `GET /v1/posts/{pid}/metrics` reads the current counts from X. X reports no other counts through SocialAPI. *** ## 7. Quirks, errors, and recovery ### Text limit Text holds 280 characters, counted as Unicode characters. A longer text fails with `400 publishing.validation_failed`, and the issue names the field `text`. ### A thread can stop partway If a continuation post fails, publishing stops and the target fails. The posts already published stay on X, because X has no batch rollback. Check the account's X timeline before you retry: `POST /v1/posts/{pid}/retry` publishes the whole thread again. ### Media must finish processing X processes a video or GIF after upload. SocialAPI waits for X to finish before it creates the post, and a post with a large video takes longer to reach `published`. ### X bills links at a higher rate X charges more for a post that contains a URL than for a post without one. The charge is on your X developer account, not on SocialAPI. ### Errors on a failed target A publish that fails after the request returned puts a structured error on the target, in `targets[].error`, with its `code`. Every code is in [Errors](/guides/errors). * `401 platform.twitter.auth`: the access token is no longer valid and renewal failed. The account is marked `token_expired`, and every later call that acts on the account returns `403 account.reconnection_required` until you reconnect it. * `403 platform.twitter.insufficient_scope`: the account's access lacks a scope, for example `media.write`. Reconnect the account. * `402 platform.twitter.usage_capped`: your X app is out of credits or reached its spending limit. Add credits or raise the limit in the X Developer Console, then retry. * `429 platform.twitter.rate_limit`: X's own limit. Wait, then retry. ### Recovery * **Rate limit.** Wait for the seconds in `Retry-After`, or at least 60 seconds when it is absent, then send `POST /v1/posts/{pid}/retry`. See [Delivery](/guides/delivery). * **Auth or scope error.** Reconnect the X account through the OAuth flow. * **Usage cap.** Add X credits, then send `POST /v1/posts/{pid}/retry`. *** ## 8. Full worked example This example publishes a post with an image and a 2-post thread, then reads the result. **Step 1: Upload media** ```bash theme={null} curl "https://api.social-api.ai/v1/media/upload-url?filename=launch.jpg&media_type=image/jpeg" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Upload the file to the returned `upload_url`, then call `POST /v1/media/{media_id}/verify`. The steps and the response are in [Media](/guides/media). The result is a `media_id`, here `f47ac10b-58cc-4372-a567-0e02b2c3d479`. **Step 2: Publish the post** ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "text": "1/ Our spring launch is here.", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "segments": [ { "text": "2/ Every piece is made to order." }, { "text": "3/ Shop the collection: https://example.com/spring" } ], "publish_now": true, "platform_data": { "twitter": { "reply_settings": "following" } } }' ``` Response (HTTP 201): the post with `status: "publishing"`, as in [Create post](#3-create-post). **Step 3: Check the status** ```bash theme={null} curl https://api.social-api.ai/v1/posts/7c9e6679-7425-40de-944b-e07fc1f90ae7 \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200) once published: ```json theme={null} { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "text": "1/ Our spring launch is here.", "status": "published", "hidden": false, "retry_count": 0, "published_at": "2026-10-13T09:00:09Z", "created_at": "2026-10-13T09:00:00Z", "updated_at": "2026-10-13T09:00:09Z", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "twitter", "status": "published", "platform_post_id": "1846123456789012345", "permalink": "https://x.com/i/status/1846123456789012345", "published_at": "2026-10-13T09:00:09Z" } ] } ``` *** ## 9. Required OAuth scopes SocialAPI requests these scopes when the person connects the account, through your own X app. The full list is in [X scopes](/platforms/twitter#scopes). | Scope | Purpose | | - | - | | `tweet.write` | Publish and delete posts | | `media.write` | Upload images, video, and GIFs | | `tweet.read` | Read the published post back | | `users.read` | Identify the account | | `offline.access` | Renew the access token | The scopes a scoped API key needs, the credit cost of a post, and the MCP tools that publish are on [Publishing](/guides/publishing). # WhatsApp Source: https://docs.social-api.ai/platforms/whatsapp Connect a WhatsApp Business phone number to receive and reply to direct messages through SocialAPI. **Messaging connector.** WhatsApp is a DM-only connector: you connect a WhatsApp Business phone number through Meta's Embedded Signup, then use our unified inbox to receive and reply to the messages that number gets. There are no posts, comments, reviews, or mentions. ## At a glance | Field | Value | | - | - | | Platform slug | `whatsapp` | | Auth type | Embedded Signup (OAuth), Meta-hosted | | API | WhatsApp Cloud API | | Access | Live. Available on all plans, except that sending template messages needs a paid plan (see [Plans](/guides/plans)) | | Best for | A unified inbox for the messages your WhatsApp Business number receives. | ## Capabilities | Feature | Supported | Notes | | - | - | - | | Receive DMs | Yes | Inbound messages arrive in real time via webhooks | | Free-form reply | Yes | Text, media, or interactive, and only inside the open 24-hour service window (see below) | | Template messages | Yes | Create, list, edit, delete, and validate templates via the API, and send approved templates (including outside the 24-hour window). Sending a template needs a paid plan: on the Free plan, `send-template` returns `403 whatsapp.paid_plan_required`. See [Plans](/guides/plans) | | Interactive messages | Yes | Up to 3 reply buttons, or a list of up to 10 rows. Taps arrive on your webhook as `dm.received` | | Conversation list / history | Partial | There is no history API, so the inbox is built from webhooks received after you connect. A number connected from the WhatsApp Business app also brings the chat history the business shares (see [Keep using the WhatsApp Business app](#keep-using-the-whatsapp-business-app)) | | Phone-number management | Yes | Request a code, verify, register, deregister, and check status of the business number | | Business profile | Yes | Read and update the WhatsApp business profile | | Publishing (posts) | No | WhatsApp has no posts concept | | Comments / reviews / mentions | No | Not applicable | ## What you need * A **WhatsApp Business phone number** that is not registered on the consumer WhatsApp app. A number the business uses in the WhatsApp Business app can keep running there, see [Keep using the WhatsApp Business app](#keep-using-the-whatsapp-business-app). During Embedded Signup you can select an existing number or create one. * **Admin access to a Meta Business** (business.facebook.com). You do **not** need to submit your own Meta App Review or complete business verification to connect. We are the Tech Provider, so the app review and provider-side setup are handled on our side. The person connecting signs in with their Meta Business account, picks their WhatsApp Business Account and phone number, and the number is connected. ## Connecting WhatsApp connects like every other platform with a consent screen: your backend asks for an `auth_url`, you send your customer there, and they come back to your `redirect_uri` with the connected account. Meta's signup runs on Meta's own pages, so there is nothing to embed or host on your side and no domain to register: it works from any domain. Your API key stays on your backend the whole time. (You can also connect from our dashboard.) ### 1. Get the signup link (your backend) ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/connect \ -H "Authorization: Bearer $SOCIALAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "whatsapp", "redirect_uri": "https://app.example.com/oauth/callback", "state": "session_abc123" }' ``` Response (HTTP 202): ```json theme={null} { "auth_url": "https://www.facebook.com/v26.0/dialog/oauth?client_id=...", "state": "4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2", "message": "Open this link to connect your whatsapp account. Once you've authorized, the account will be connected automatically." } ``` Always set `redirect_uri`: it is where your customer lands with the result, and the only way to receive the number choice described below. See [Redirect URI rules](/guides/connect-accounts#redirect-uri-rules). `state` is optional and comes back unchanged on the redirect. `onboarding` picks the signup Meta shows: | `onboarding` | Meta shows | Result | | - | - | - | | `cloud_api` (default) | The WhatsApp Business Account and phone number picker | The number is used through the API only. Use it for a new number or one already on the Cloud API. | | `business_app` | "Connect your existing WhatsApp Business app" | The number keeps working in the WhatsApp Business app too. See [Keep using the WhatsApp Business app](#keep-using-the-whatsapp-business-app). | ### 2. Send your customer to `auth_url` (browser) Redirect the browser to `auth_url`, or open it in a new tab. Your customer signs in with Facebook, picks or creates their business portfolio, WhatsApp Business Account and phone number, and verifies the number if it is new. ### 3. Read the result on your `redirect_uri` On success, your customer lands on your `redirect_uri` with the account appended: ``` https://app.example.com/oauth/callback?account_id=acc_01HZ9X3Q4R5M6N7P8V2K0W1J&platform=whatsapp&state=session_abc123&status=success ``` `account_id` is the ID every WhatsApp call takes from here on. The number's messages now flow into your inbox. On failure, the redirect carries `status=error` with `error` and `error_description`. The WhatsApp-specific values are: | `error` | Meaning | | - | - | | `whatsapp.no_waba` | No WhatsApp Business Account was shared during signup. Start a new connection. | | `whatsapp.no_phone_number` | The WhatsApp Business Account has no phone number yet. Add one in Meta Business Manager, then connect again. | | `whatsapp.number_banned` | Meta has banned this number. Connecting again does not help until Meta lifts the ban. | See [Redirect parameters](/guides/connect-accounts#redirect-parameters) for the full list. ### Choose a number When the WhatsApp Business Account holds more than one phone number, the redirect carries `status=selection_required` and a `connection_id` instead of an `account_id`. Read the numbers with `GET /v1/accounts/pending/{connection_id}`: ```bash theme={null} curl https://api.social-api.ai/v1/accounts/pending/pc_4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2 \ -H "Authorization: Bearer $SOCIALAPI_KEY" ``` ```json theme={null} { "connection_id": "pc_4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2", "platform": "whatsapp", "expires_at": "2026-10-26T12:30:00Z", "profiles": [ { "platform_account_id": "106540352242922", "display_name": "Acme Support", "username": "+1 555-010-0199", "profile_picture_url": "", "bio": "" }, { "platform_account_id": "106540352242923", "display_name": "Acme Sales", "username": "+1 555-010-0200", "profile_picture_url": "", "bio": "" } ] } ``` Each entry in `profiles` is one number: `platform_account_id` is its phone number ID, `display_name` the verified business name, and `username` the phone number. Connect one with `POST /v1/accounts/pending/{connection_id}/select`: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/pending/pc_4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2/select \ -H "Authorization: Bearer $SOCIALAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform_account_id": "106540352242922" }' ``` The response (HTTP 201) is the connected account. Choose within 30 minutes; after that the connection expires and returns 404, and your customer runs the signup again. Only the number you choose is connected. ### Keep using the WhatsApp Business app To connect a number the business already uses in the WhatsApp Business app, and keep using it there, send `"onboarding": "business_app"` in step 1. Meta then walks your customer through linking the app, including scanning a QR code with their phone. The phone needs WhatsApp Business app 2.24.17 or later, and Meta decides during signup whether the number is eligible. Once the number is connected: * The chat history the business chooses to share in the app (up to six months) appears in its conversations as WhatsApp delivers it. Imported messages raise no webhook events, and their attachments carry `attachment_type` without an `attachment_url`. * Messages the business sends from the phone app appear in the conversation and raise `dm.sent`, like messages sent through the API. * Sending is capped at 20 messages per second, Meta's fixed limit for these numbers. * The number is already registered, so you never call `register` for it. `status` reports it as registered, and `register` refuses it with `409 whatsapp.business_app_number`. * The `deregister` endpoint does not work for these numbers. The business disconnects the number from the WhatsApp Business app's settings. ## The 24-hour service window WhatsApp only lets a business send free-form messages within **24 hours** of the customer's most recent message. Each new inbound message from that customer resets the 24-hour timer. * **Inside the window:** you can send a free-form text reply from the inbox. * **Outside the window:** free-form replies are rejected with `whatsapp.window_closed`. To reach the customer, send an approved message template with `send-template` (see [Message templates](#message-templates) below), which is allowed at any time. ## Message templates Templates are pre-approved messages you can send outside the 24-hour window (order updates, authentication codes, and the like). You create, manage, and send them entirely through the API, scoped to a connected account. ### Create a template ```bash theme={null} curl -X POST https://api.social-api.ai/v1/platforms/whatsapp/accounts/{account_id}/templates \ -H "Authorization: Bearer $SOCIALAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "order_confirmation", "category": "UTILITY", "language": "en_US", "components": [ { "type": "BODY", "text": "Hello {{1}}, your order {{2}} is confirmed.", "example": { "body_text": [["Jordan", "A1234"]] } } ] }' ``` A new template starts in `PENDING`; Meta reviews it and the status moves to `APPROVED` or `REJECTED` asynchronously. `category` is one of `UTILITY`, `MARKETING`, or `AUTHENTICATION` (marketing content in a `UTILITY` template is auto-rejected). Call `POST /v1/platforms/whatsapp/templates/validate` first to catch field-level problems before you submit. ### Send a template ```bash theme={null} curl -X POST https://api.social-api.ai/v1/platforms/whatsapp/accounts/{account_id}/send-template \ -H "Authorization: Bearer $SOCIALAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": "15551234567", "template_name": "order_confirmation", "language": "en_US", "components": [ { "type": "body", "parameters": [ { "type": "text", "text": "Jordan" }, { "type": "text", "text": "A1234" } ] } ] }' ``` `to` is E.164 without the leading `+`. The template must already be `APPROVED` and `language` must match an approved variant. This send is allowed whether or not the 24-hour window is open, and the message is recorded in the inbox thread. On the Free plan the request returns `403 whatsapp.paid_plan_required` and nothing is sent. Replies to people who messaged you work on every plan. See [Plans](/guides/plans). The response includes `message_id`, the SocialAPI message ID for the send, alongside WhatsApp's own `messages[].id` (the `wamid`). `message_id` is the same value that arrives as `id` on the `dm.status.sent`, `dm.status.delivered`, `dm.status.read`, and `dm.status.failed` webhooks for this message, so you can bind a delivery receipt to the row you created without matching on the `wamid`. Listing, editing, and deleting templates, plus phone-number management (`request-code`, `verify-code`, `register`, `deregister`, `status`) and the business profile, are also available. See the [API reference](/api-reference/introduction) for the full set of `/v1/platforms/whatsapp` endpoints and their schemas. ## Receiving and replying WhatsApp has no message-history API, so the inbox covers messages from connect time onward. The exception is a number connected from the WhatsApp Business app, which also brings the chat history the business chooses to share (see [Keep using the WhatsApp Business app](#keep-using-the-whatsapp-business-app)). ### Contacts with a WhatsApp username A contact who uses a WhatsApp username can hide their phone number. WhatsApp then identifies them by a business-scoped user ID, a string such as `CO.1648230076976170` that contains a dot, instead of a phone number. That ID is the conversation's `participant_id`, the `author.id` on `dm.received`, and the `recipient_id` on status events. Reply to the conversation as usual. If the contact later shares their phone number, their new messages start a separate conversation. ### List DM conversations ```bash theme={null} curl "https://api.social-api.ai/v1/inbox/conversations?account_id={id}" \ -H "Authorization: Bearer $SOCIALAPI_KEY" ``` ### Reply to a message ```bash theme={null} curl -X POST https://api.social-api.ai/v1/inbox/conversations/{conversation_id}/messages \ -H "Authorization: Bearer $SOCIALAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "{account_id}", "text": "Thanks for reaching out!" }' ``` `account_id` is required. The reply only succeeds while the 24-hour service window is open. ### Sending media Attach media with the [`attachments` array](/guides/direct-messages#attachments), either a `url` WhatsApp can fetch or a `media_id` from your media library: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/inbox/conversations/{conversation_id}/messages \ -H "Authorization: Bearer $SOCIALAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "{account_id}", "attachments": [ { "source_type": "media_id", "source": "{media_id}", "caption": "The damaged panel" } ] }' ``` WhatsApp has no generic attachment type, so `file` and anything we cannot classify are sent as a document, with the filename taken from the URL's last path segment. A `caption` is supported on image, video, and document; on audio it is rejected, since WhatsApp has no caption there. A `url` attachment is fetched by WhatsApp's own servers, so it must be publicly reachable without authentication (a `media_id` always is). WhatsApp's limits apply: images 5 MB (JPEG, PNG), video 16 MB (MP4, 3GPP), audio 16 MB (AAC, AMR, MP3, M4A, OGG), documents 100 MB. A file it cannot fetch, or one over a limit, returns `whatsapp.media_download_failed`. `source_type: "platform_attachment_id"` is not supported on WhatsApp. ### Interactive messages Send up to 3 reply buttons, or a list of up to 10 rows, so the recipient answers by tapping instead of typing. Both need `text`, which becomes the message body. Every button carries a `payload` (a row carries an `id`) that you choose. WhatsApp never interprets it: it is echoed back to you untouched when the recipient taps, so use it as your own routing key. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/inbox/conversations/{conversation_id}/messages \ -H "Authorization: Bearer $SOCIALAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "{account_id}", "text": "Your weekly digest is ready. What next?", "buttons": [ { "type": "postback", "title": "Review drafts", "payload": "menu:review_drafts" }, { "type": "postback", "title": "Skip this week", "payload": "menu:skip" } ] }' ``` For more than 3 options, send a list instead. `list_button` is the label on the button that opens it: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/inbox/conversations/{conversation_id}/messages \ -H "Authorization: Bearer $SOCIALAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "{account_id}", "text": "Which location?", "list_button": "View locations", "list_sections": [ { "title": "Your locations", "rows": [ { "id": "loc:1", "title": "Downtown", "description": "3 reviews waiting" }, { "id": "loc:2", "title": "Airport" } ] } ] }' ``` Limits: button titles 20 characters, row titles 24, row descriptions 72, `list_button` 20, and 10 rows in total across at most 10 sections. Payloads and row ids must be unique within a message, since a tap echoes only the id. Buttons and `list_sections` are mutually exclusive, and neither can be combined with an attachment. `quick_replies` and `cards` are Instagram and Facebook only; sending them to WhatsApp returns a validation error rather than silently dropping them. A url button is available on [templates](#message-templates), not on interactive messages. Interactive messages are free-form, so they are subject to the 24-hour service window. To open a menu outside it, send an approved template with `QUICK_REPLY` buttons instead; taps on those reach your webhook the same way. ### Receiving taps A tap is delivered as an ordinary `dm.received` event, not a separate event type: on WhatsApp the tapped title is posted into the conversation as the contact's own message, and it opens the 24-hour window like any inbound message. The payload you set rides on the event's metadata: ```json theme={null} { "content": { "text": "Review drafts" }, "metadata": { "platform_id": "wamid.xxx", "interactive_type": "button_reply", "payload": "menu:review_drafts" } } ``` `interactive_type` is `button_reply` (a reply button), `list_reply` (a list row), or `button` (a template quick reply). Both fields are absent on an ordinary message, so switch on `metadata.payload` when it is present and treat the message as text otherwise. ### Inbound media WhatsApp exposes inbound media only to the connected account, never to an external caller, so it cannot be linked directly. For WhatsApp (and Telegram), `messages.attachment_url` is a SocialAPI URL (`/v1/inbox/media/{id}`) that you retrieve with your API key, unlike the other connectors where `attachment_url` is the platform's own URL. See [Inbox media](/guides/inbox#fetch-an-inbound-attachment) for the endpoint and its response codes. ## Errors | HTTP | Code | When it occurs | | - | - | - | | 400 | `whatsapp.window_closed` | The 24-hour service window has closed. Free-form replies are no longer allowed; send an approved template with `send-template` instead. | | 404 | `whatsapp.template_not_found` | The named template does not exist on this account's WABA. | | 400 | `whatsapp.template_paused` | The template is paused for quality reasons and cannot be sent. | | 400 | `whatsapp.media_download_failed` | WhatsApp could not download the attachment. The URL is unreachable, the format is unsupported, or the file exceeds the size limit for its type. | | 400 | `whatsapp.media_upload_failed` | WhatsApp rejected the media file. | | 400 | `validation.caption_unsupported` | A caption was set on an audio attachment. WhatsApp audio messages have no caption. | | 400 | `whatsapp.undeliverable` | The recipient is not a WhatsApp user. | | 403 | `whatsapp.paid_plan_required` | A template was sent on the Free plan. Template sends need a paid plan: see [Plans](/guides/plans). Replies inside the 24-hour window work on every plan. | | 403 | `whatsapp.user_opted_out` | The recipient has opted out of messages from this business. | | 409 | `whatsapp.business_app_number` | `register` was called for a number that stays on the WhatsApp Business app. It is already registered: do not retry. | | 429 | `whatsapp.frequency_cap` | The recipient has hit their daily marketing-message limit. | | 429 | `whatsapp.pair_rate_limit` | Too many messages sent to this same recipient. | | 429 | `whatsapp.throughput_exceeded` | The number's messaging throughput limit was exceeded. | | 401 | `platform.whatsapp.auth` | The connection is no longer authorized. Reconnect the account. | See [Errors](/guides/errors) for the full catalog. ## Related * [Supported platforms](/platforms/overview) * [Inbox](/guides/inbox) * [Errors](/guides/errors) # YouTube API Source: https://docs.social-api.ai/platforms/youtube Connect YouTube channels to publish and edit videos, read and moderate comments, and read channel stats through one API. The YouTube API in SocialAPI connects a YouTube channel. It uploads, schedules, edits, and deletes videos, reads and moderates comments, posts replies and top-level comments, and reads channel stats. SocialAPI's own Google app connects the channel, so you register nothing: see [Platform apps](/guides/platform-credentials). ## At a glance | Field | Value | | - | - | | Platform slug | `youtube` | | Auth | OAuth 2.0 with Google | | Platform API | YouTube Data API v3 | | Status | Available | ## Capabilities An unsupported call returns `501 resource.not_supported`. | Feature | Status | Condition | | - | - | - | | Publish a video | Yes | 1 video per post. See [YouTube publishing](/platforms/youtube/publishing) | | Schedule a post | Yes | 2 ways: `scheduled_at` makes SocialAPI upload the video at that time, and `platform_data.youtube.publish_at` makes YouTube publish an uploaded private video at that time | | First comment on a video | Yes | Posted after the upload. A failure does not fail the post | | Add a video to a playlist | Yes | `platform_data.youtube.playlist_id`. A failure does not fail the post | | Update a video | Yes | Before publishing: `PATCH /v1/posts/{pid}`. After publishing: `PATCH /v1/platforms/youtube/accounts/{id}/videos/{videoId}` | | Delete a video | Yes | Removes the video from YouTube | | List posts | Yes | Every video in the channel's uploads, including videos uploaded outside SocialAPI | | List comments and replies | Yes | Comments are read per video | | Reply to a comment | Yes | | | Post a top-level comment | Yes | Omit `comment_id` when you reply | | Hide, unhide a comment | Yes | Hiding sets the comment to `rejected` | | Hold a comment for review, ban its author | Yes | `POST /v1/platforms/youtube/comments/{commentId}/moderate` | | Delete a comment | Conditional | Only comments your channel wrote. Another person's comment returns `422 platform.youtube.processing_failure`: hide it instead | | Edit your own comment | Yes | `PATCH /v1/platforms/youtube/comments/{commentId}` | | Like a comment | No | `501 resource.not_supported`. YouTube has no comment-like API | | Private reply | No | `501 resource.not_supported` | | DMs | No | `501 resource.not_supported` | | Mentions | No | `501 resource.not_supported` | | Reviews | No | `GET /v1/inbox/reviews/{account_id}` returns `422 account.platform_unsupported` | | Automations | No | A YouTube account returns `422 automation.platform_unsupported` | | Channel summary | Yes | Subscribers, views, and video count | | Analytics | Yes | Live post metrics (`likes`, `comments`, `views`, `extra.views`), stored post history, and a live channel summary. SocialAPI stores no channel history. See [Analytics](/guides/analytics) | | Webhook events | Partial | Post and account events only. YouTube sends no comment events | The cross-platform matrix is on [Platforms](/platforms/overview). ## Connect an account Connect with `POST /v1/accounts/connect` and `"platform": "youtube"`. The shared flow, request fields, and result are in [Connect accounts](/guides/connect-accounts), and the redirect parameters are in [Redirect parameters](/guides/connect-accounts#redirect-parameters). ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/connect \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "youtube", "redirect_uri": "https://app.example.com/oauth/callback", "state": "session_abc123" }' ``` Response (HTTP 202): ```json theme={null} { "auth_url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...&state=...", "state": "4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2", "message": "Open this link to connect your youtube account. Once you've authorized, the account will be connected automatically." } ``` YouTube-specific rules: * **One account per channel.** Each connected `youtube` account is 1 YouTube channel. When the Google login manages more than 1 channel, one authorization connects all of them, and the redirect carries their IDs in `account_ids`. Without `brand_id`, each channel connects into its own new brand. With a `brand_id`, the brand takes one channel and each further channel ends with `error=account.already_linked`: see [Reconnect an account](/guides/connect-accounts#reconnect-an-account). * **A login with no channel.** A Google login that has no YouTube channel ends with `status=error` and `error=platform.youtube.no_channels` on your `redirect_uri`. * **Reconnect.** When YouTube no longer accepts the saved connection, calls return `401 platform.youtube.auth`. See [Reconnect an account](/guides/connect-accounts#reconnect-an-account). ## Read videos and channel stats `GET /v1/posts?platform=youtube&account_ids={account_id}` lists the channel's videos with their `permalink` and engagement metrics: see [YouTube publishing](/platforms/youtube/publishing#6-retrieve-posts). `GET /v1/platforms/youtube/accounts/{id}/summary` returns the channel's aggregate stats: ```bash theme={null} curl https://api.social-api.ai/v1/platforms/youtube/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/summary \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "title": "Acme Corp", "handle": "@acmecorp", "thumbnail_url": "https://yt3.ggpht.com/abc", "subscriber_count": 12400, "view_count": 980000, "video_count": 47 } ``` `POST /v1/platforms/youtube/accounts/{id}/sync` re-reads the channel's videos at once and returns `{"success": true}`. Use it after you upload a video outside SocialAPI. ## Comments YouTube comments are read per video. A video is identified by its YouTube video ID, which is the `{postId}` in the comment paths. For the cross-platform endpoints, see [Comments](/guides/comments). This section lists what is specific to YouTube. ```bash theme={null} curl "https://api.social-api.ai/v1/inbox/comments/dQw4w9WgXcQ?account_id=acc_01HZ9X3Q4R5M6N7P8V2K0W1J&order=relevance" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "id": "sapi_cmt_TT974YVQH4RAHNN2HY2DJSNY9111D7LMTP1XXPCVQVL", "platform_id": "UgxK3m9pQ2", "platform": "youtube", "text": "Great video!", "author_id": "UCxyz123", "author_name": "Jane Smith", "author_username": "Jane Smith", "author_picture": "https://yt3.ggpht.com/abc", "is_owner": false, "like_count": 5, "reply_count": 2, "has_replies": true, "liked_by_viewer": false, "is_hidden": false, "moderation_status": "published", "parent_id": null, "created_at": "2026-03-15T10:30:00Z", "capabilities": { "can_reply": true, "can_delete": true, "can_hide": true, "can_like": false, "can_private_reply": false } } ], "pagination": { "has_more": false, "next_cursor": "" } } ``` These query parameters apply to YouTube only: | Parameter | Description | | - | - | | `moderation_status` | `published` (default), `held_for_review`, or `likely_spam`. The last two list comments waiting for your review. Another value returns `400 validation.field_invalid` | | `order` | `recent` (default) or `relevance`. Another value returns `400 validation.field_invalid` | | `q` | Keyword search across comment text, up to 256 characters | YouTube-specific behavior: * **Videos with comments.** `GET /v1/inbox/comments` lists commented videos only when you pass `account_id`. The list is 1 page with no cursor. * **Comments turned off.** Listing the comments of a video with comments turned off returns `422 platform.youtube.comments_disabled`. Retrying does not help: stop polling the video until the channel owner turns comments back on in YouTube Studio. After that, the error can continue for up to 24 hours. * **Reply or top-level comment.** `POST /v1/inbox/comments/{postId}` with `comment_id` replies to that comment. Without `comment_id`, it posts a new top-level comment on the video. Each costs 1 interaction credit. * **Hide.** `POST /v1/inbox/comments/{postId}/{commentId}/hide` sets the comment to `rejected`, which removes it from public view. `DELETE` on the same path sets it back to `published`. * **Moderate.** `POST /v1/platforms/youtube/comments/{commentId}/moderate` sets the status explicitly. * **Delete.** `DELETE /v1/inbox/comments/{postId}/{commentId}` removes a comment your channel wrote. `capabilities.can_delete` is `true` on every YouTube comment, including other people's. Deleting another person's comment returns `422 platform.youtube.processing_failure`. Hide it instead. * **Like and private reply.** Not available. `can_like` and `can_private_reply` are `false`. ### Moderate a comment ```bash theme={null} curl -X POST https://api.social-api.ai/v1/platforms/youtube/comments/UgxK3m9pQ2/moderate \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "post_id": "dQw4w9WgXcQ", "status": "rejected", "ban_author": true }' ``` Response (HTTP 200): ```json theme={null} { "success": true } ``` | Field | Description | | - | - | | `account_id` | Required. The connected YouTube account | | `post_id` | Required. The YouTube video ID the comment is on | | `status` | Required. `published`, `held_for_review`, or `rejected`. Another value returns `400 validation.field_invalid` | | `ban_author` | Optional. `true` also bans the author from commenting on your channel. Valid only with `status` `rejected`, otherwise `400 validation.field_invalid` | A missing `account_id` or `post_id` returns `400 validation.field_required`. ### Edit your own comment ```bash theme={null} curl -X PATCH https://api.social-api.ai/v1/platforms/youtube/comments/UgxK3m9pQ2 \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "post_id": "dQw4w9WgXcQ", "text": "Updated comment text" }' ``` Response (HTTP 200): ```json theme={null} { "success": true } ``` `account_id`, `post_id`, and `text` are all required. A missing one returns `400 validation.field_required`. YouTube accepts the edit only on a comment your channel wrote. ## Webhook events YouTube sends nothing to SocialAPI, so it emits no comment events: read comments with the endpoints above. Every `post.*` event in [Webhook events](/guides/webhook-events#posts) is emitted for videos you publish through SocialAPI. You also receive `account.connected` when you connect a channel, `account.disconnected` when SocialAPI flags the channel, and `account.updated` when the account loses [permissions](/guides/permissions) you did not remove yourself. See [Webhook events](/guides/webhook-events). ## Suspended or closed channels When YouTube suspends or closes the account or its channel, every call on it returns `403 platform.youtube.account_suspended`. The saved connection is still valid, so reconnecting changes nothing: the owner resolves the suspension with YouTube. SocialAPI sets the account's `reconnect_reason` to `youtube_account_suspended`, and sends an `account.disconnected` event with that `reason`. ## Limits | Limit | Value | | - | - | | Video title | 100 characters | | Video description | 5,000 bytes | | Video file | 256 GB | | YouTube's own limits | A call that reaches one returns `429 platform.youtube.rate_limit`. Wait, then retry | The publishing rules are in [YouTube publishing](/platforms/youtube/publishing#2-supported-media). ## Scopes SocialAPI's Google app requests this scope at connect. You apply for no Google app review. The consent screen is the same whichever [permissions](/guides/permissions) you select. | Scope | Used for | | - | - | | `https://www.googleapis.com/auth/youtube.force-ssl` | Upload, edit, and delete videos, read channel and video data, and read, post, moderate, and edit comments | YouTube asks for 1 scope that covers reading and writing, so the consent screen shows it whatever you select. The account is still limited to the permissions you selected: an account connected with `comments.read` alone cannot reply to comments. A call the granted scope does not permit returns `403 platform.youtube.insufficientPermissions`. Reconnect the account to grant it again. ## Related * [YouTube publishing](/platforms/youtube/publishing): fields, scheduling, and quirks for publishing videos * [Comments](/guides/comments): the cross-platform comment endpoints * [Connect accounts](/guides/connect-accounts): the shared connect flow * [Webhook events](/guides/webhook-events): the post and account events * [Platforms](/platforms/overview): the capability matrix * [Errors](/guides/errors): every error code # YouTube publishing Source: https://docs.social-api.ai/platforms/youtube/publishing Publish, schedule, edit, and delete YouTube videos with title, description, tags, visibility, a first comment, and a playlist, and read their metrics. ## 1. Overview A YouTube post uploads 1 video to 1 connected YouTube channel and sets its metadata: title, description, visibility, tags, and category. YouTube is the only platform where you can edit a published post's metadata through SocialAPI, and it has 2 ways to schedule. For everything outside publishing, see the [YouTube API](/platforms/youtube) page. | Feature | Supported | Notes | | - | - | - | | Create post | Yes | 1 video per post. A title is required | | Update post | Yes | Before publishing: `PATCH /v1/posts/{pid}`. After publishing: `PATCH /v1/platforms/youtube/accounts/{id}/videos/{videoId}` | | Delete post | Yes | Removes the video from YouTube | | Schedule | Yes | `scheduled_at` makes SocialAPI upload the video at that time. `platform_data.youtube.publish_at` uploads it now as private and makes YouTube publish it at that time | | First comment | Yes | `first_comment` is posted after the upload. A failure does not fail the post | | Add to playlist | Yes | `platform_data.youtube.playlist_id`. A failure does not fail the post | | Posting caps | Yes | 25 posts per hour and 50 per 24 hours for each account: see [Posting velocity caps](/guides/rate-limits#posting-velocity-caps) | *** ## 2. Supported media | Content type | How to trigger | Notes | | - | - | - | | Video | 1 video in `media`, as a `media_id` or `url` source | MP4 (`video/mp4`), MOV (`video/quicktime`), AVI (`video/x-msvideo`), or WebM (`video/webm`), up to 256 GB | * **One video.** A post takes exactly 1 video. Images are not accepted: an image fails pre-publish validation with `400 publishing.validation_failed`, and so does a second media item. Images appear only as thumbnails when you retrieve posts. * **Title.** A title is required and holds up to 100 characters. SocialAPI does not check the length: YouTube refuses a longer title and the target fails with `platform.youtube.api_error`. * **Description.** `text` becomes the description and holds up to 5,000 bytes. A longer `text` fails pre-publish validation with `400 publishing.validation_failed`. * **Media sources.** Use a `media_id` (a file in your [media library](/guides/media)) or a public `http(s)` `url` that SocialAPI fetches when it uploads. There is no `video_url` field. A `media_id` must be `ready`: a file you uploaded with the presigned flow stays `pending` until you call `POST /v1/media/{id}/verify`. A `platform_attachment_id` item returns `422 publishing.media.unsupported_source`. * **Unreachable files.** A `url` that does not answer fails the target with `validation.video_url`. A file whose size cannot be read fails it with `validation.video_size`. *** ## 3. Create post Send `POST /v1/posts` with a YouTube account in `targets`. * `title` is the video title. It is required. `platform_data.youtube.title` is accepted when `title` is empty, and `title` wins when both are set. SocialAPI does not derive a title from the file name or the URL. * `text` is the video description. It is required. * `visibility` is `public`, `unlisted`, or `private`. It defaults to `public`, so a published video is watchable at once. When `platform_data.youtube.publish_at` is set and `visibility` is empty, it defaults to `private`. * Without `publish_now` or `scheduled_at`, SocialAPI saves the post as a draft. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "title": "Our spring collection walkthrough", "text": "A full tour of the new lineup. Chapters in the description below.", "visibility": "public", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "publish_now": true, "platform_data": { "youtube": { "category_id": "22", "tags": ["fashion", "spring", "lookbook"], "made_for_kids": false, "notify_subscribers": true } } }' ``` Response (HTTP 201): ```json theme={null} { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "title": "Our spring collection walkthrough", "text": "A full tour of the new lineup. Chapters in the description below.", "status": "publishing", "visibility": "public", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "media_ids": ["f47ac10b-58cc-4372-a567-0e02b2c3d479"], "hidden": false, "retry_count": 0, "created_at": "2026-10-13T09:00:00Z", "updated_at": "2026-10-13T09:00:00Z", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "youtube", "status": "pending", "title": "Our spring collection walkthrough", "text": "A full tour of the new lineup. Chapters in the description below.", "visibility": "public", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "media_ids": ["f47ac10b-58cc-4372-a567-0e02b2c3d479"] } ] } ``` SocialAPI uploads in the background. The target moves from `pending` to `publishing`, then to `published` or `failed`. Poll `GET /v1/posts/{pid}` or subscribe to the `post.published`, `post.partial`, and `post.failed` [webhook events](/guides/webhook-events). Publishing or scheduling costs 1 post credit; a draft is free (see [Plans and credits](/guides/plans)). ### Where to put platform data Pass the YouTube fields in 1 of 2 places: * **Post level**, keyed by platform: `platform_data.youtube`. It applies to every YouTube target in the post. * **Target level**, flat with no `youtube` key: the target's own `platform_data`. It applies to that target only. When both set the same field, the target-level value wins: ```json theme={null} { "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform_data": { "notify_subscribers": false } } ], "platform_data": { "youtube": { "category_id": "22", "notify_subscribers": true } } } ``` This target publishes with `category_id` `22` from the post level and `notify_subscribers` `false` from the target override. ### Platform data fields | Field | Type | Default | Description | | - | - | - | - | | `title` | string | none | The video title when the top-level `title` is empty | | `category_id` | string | YouTube's default | YouTube video category ID, for example `22` for People & Blogs. See YouTube's [category list](https://developers.google.com/youtube/v3/docs/videoCategories/list) | | `tags` | string or array | none | Video tags. A JSON array, a comma-separated string, or a single string | | `default_language` | string | none | BCP-47 language code of the title and description, such as `en` or `fr` | | `made_for_kids` | bool | `false` | The COPPA self-declaration that the video is made for children | | `embeddable` | bool | YouTube's default | Whether other sites can embed the video | | `license` | string | YouTube's default | `youtube` (standard license) or `creativeCommon` | | `public_stats_viewable` | bool | YouTube's default | Whether view and like counts are public | | `publish_at` | string | none | ISO 8601 timestamp at which YouTube publishes the video. Requires `private` visibility. See Native scheduling below | | `recording_date` | string | none | The date the video was recorded, `YYYY-MM-DD` | | `contains_synthetic_media` | bool | `false` | Declares that the video contains AI-generated or altered content | | `notify_subscribers` | bool | `true` | Whether to notify the channel's subscribers on publish | | `playlist_id` | string | none | A playlist to add the video to after the upload | SocialAPI forwards any additional keys in `platform_data.youtube` only when the table above lists them. YouTube has no other publish options that SocialAPI passes through: an unlisted key is not sent. ### Schedule a post YouTube has 2 ways to schedule, and they do different things: | | `scheduled_at` | `platform_data.youtube.publish_at` | | - | - | - | | Who waits | SocialAPI | YouTube | | When the video reaches YouTube | At `scheduled_at` | At once, as a private video | | When viewers can watch it | At `scheduled_at`, with the `visibility` you set (public by default) | At `publish_at` | | Visibility | Any | `private` | Use `scheduled_at` to have SocialAPI do the waiting. See [Scheduling](/guides/scheduling). #### Native scheduling With `publish_at`, YouTube holds the uploaded video as private and makes it public at that time: ```json theme={null} { "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "title": "Launch day announcement", "text": "Going live for everyone at 10am.", "visibility": "private", "publish_now": true, "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "platform_data": { "youtube": { "publish_at": "2026-10-20T10:00:00Z" } } } ``` `publish_at` with a `visibility` other than `private` fails pre-publish validation with `400 publishing.validation_failed`, with the issue on the `publish_at` field. An empty `visibility` is allowed: `publish_at` makes it `private`. ### First comment Set `first_comment` to the text of a top-level comment that SocialAPI posts on the video right after the upload. A failure to post it does not change the post: the target stays `published` and carries no error. ### Add to playlist Set `platform_data.youtube.playlist_id` to add the video to one of your playlists after the upload. A failure here does not change the post status either. *** ## 4. Update post Which endpoint you use depends on whether the post is published. ### Before publishing `PATCH /v1/posts/{pid}` edits a post in `draft`, `scheduled`, or `failed` status. The change applies when the post publishes: ```bash theme={null} curl -X PATCH https://api.social-api.ai/v1/posts/7c9e6679-7425-40de-944b-e07fc1f90ae7 \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Our spring collection walkthrough (updated)", "text": "Updated description with corrected chapter timestamps.", "visibility": "unlisted", "platform_data": { "youtube": { "tags": ["fashion", "spring", "lookbook", "2026"] } } }' ``` The response (HTTP 200) is the updated post. On a published post this call returns `409 post.state_invalid`: use the endpoint below. See [Scheduling](/guides/scheduling) for rescheduling and the editable fields. ### After publishing `PATCH /v1/platforms/youtube/accounts/{id}/videos/{videoId}` edits the video on YouTube. `{id}` is the connected account ID and `{videoId}` is the YouTube video ID (`platform_post_id`). ```bash theme={null} curl -X PATCH https://api.social-api.ai/v1/platforms/youtube/accounts/acc_01HZ9X3Q4R5M6N7P8V2K0W1J/videos/dQw4w9WgXcQ \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Our spring collection walkthrough (updated)", "description": "Updated description with corrected chapter timestamps.", "tags": ["fashion", "spring", "lookbook", "2026"] }' ``` Response (HTTP 200): ```json theme={null} { "success": true } ``` | Field | Type | Description | | - | - | - | | `title` | string | The video title | | `description` | string | The video description | | `visibility` | string | `public`, `unlisted`, or `private` | | `tags` | string or array | The Studio tags. A JSON array or a comma-separated string, as at create time | * **Partial edits are safe.** A field you omit keeps its current value on YouTube. An omitted field is never erased. * **Tags.** `tags` sets the Studio tags, which are separate from any `#hashtags` in the description. Editing the description does not change the tags, and setting tags does not change the description. Omitting `tags`, or sending an empty array, leaves the tags as they are. The API cannot clear tags. * **`publish_at`.** It can be set only before the video is first published. It cannot change once the video is public. * **Unknown video.** A `videoId` that does not exist on the channel returns `404 resource.not_found`. *** ## 5. Delete post `DELETE /v1/posts/{pid}` removes the video from YouTube for every published YouTube target, and removes a draft, scheduled, or failed post from SocialAPI: ```bash theme={null} curl -X DELETE https://api.social-api.ai/v1/posts/7c9e6679-7425-40de-944b-e07fc1f90ae7 \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "success": true, "deleted": true, "results": [ { "platform": "youtube", "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "outcome": "deleted" } ] } ``` If YouTube refuses the delete, or the video no longer exists on YouTube, the target's `outcome` is `failed` with the reason in `message`, and SocialAPI keeps the post (`"deleted": false`). To remove the video from 1 channel of a multi-target post and keep the post, send `POST /v1/posts/{pid}/unpublish` with that target's `account_id`: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts/7c9e6679-7425-40de-944b-e07fc1f90ae7/unpublish \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }' ``` *** ## 6. Retrieve posts `GET /v1/posts` lists posts, filtered by platform or account. It also returns videos uploaded outside SocialAPI: ```bash theme={null} curl "https://api.social-api.ai/v1/posts?platform=youtube&account_ids=acc_01HZ9X3Q4R5M6N7P8V2K0W1J&limit=20" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "title": "Our spring collection walkthrough", "text": "A full tour of the new lineup.", "status": "published", "hidden": false, "retry_count": 0, "created_at": "2026-10-13T09:00:00Z", "updated_at": "2026-10-13T09:03:22Z", "published_at": "2026-10-13T09:03:22Z", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "youtube", "status": "published", "platform_post_id": "dQw4w9WgXcQ", "permalink": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "published_at": "2026-10-13T09:03:22Z", "metrics": { "likes": 128, "comments": 17, "shares": 0, "saves": 0, "extra": { "view_count": 5432 } }, "metrics_synced_at": "2026-10-13T12:00:00Z" } ] } ], "pagination": { "limit": 20, "has_more": false } } ``` * `platform_post_id` is the YouTube video ID. * `metrics.likes` and `metrics.comments` come from YouTube. `shares` and `saves` are always `0`: YouTube does not report them. * The view count is `metrics.extra.view_count` in this list. `GET /v1/posts/{pid}/metrics` reads the metrics from YouTube at request time and returns the view count as `metrics.views` and `metrics.extra.views`. * `metrics_synced_at` is on the target, not inside `metrics`. It is when SocialAPI last refreshed the metrics. * Channel-level counts are on the [YouTube API](/platforms/youtube#read-videos-and-channel-stats) page. *** ## 7. Quirks, errors, and recovery ### Title and description are required A post without a `title` fails its YouTube target with `validation.field_required`, and `error.meta.field` is `title`. A request without `text` returns `400 validation.field_required`. ### publish\_at requires private visibility YouTube schedules private videos only. `publish_at` with `public` or `unlisted` visibility returns `400 publishing.validation_failed` before the upload starts. ### First comment and playlist add never fail the post If the `first_comment` or the playlist add fails after a successful upload, the post stays `published` and the failure is not reported on the post. Check the video on YouTube, or post the comment with `POST /v1/inbox/comments/{postId}`. ### Posting caps SocialAPI refuses a publish that would pass 25 posts in an hour or 50 posts in 24 hours for one account, with `429 publishing.velocity_limit`. A scheduled post is held and published when the window clears. See [Posting velocity caps](/guides/rate-limits#posting-velocity-caps). ### Errors on a failed target A failed target carries an `error` object: ```json theme={null} { "code": "platform.youtube.api_error", "message": "The request could not be completed", "category": "platform", "caused_by": "platform" } ``` * `platform.youtube.api_error`: YouTube refused the request. `message` carries YouTube's reason, for example a title over 100 characters, an unknown `category_id`, tags over YouTube's limit, or a channel over its upload limit. Fix the request and retry. * `platform.youtube.rate_limit`: YouTube's own limit was reached. Wait, then retry the post. * `platform.youtube.auth`: YouTube no longer accepts the connection. [Reconnect the account](/guides/connect-accounts). * `platform.youtube.insufficientPermissions`: the granted scope does not permit the action. Reconnect the account to grant it again. * `platform.youtube.account_suspended`: YouTube suspended or closed the account or its channel. Reconnecting does not help: the owner resolves it with YouTube. * `validation.video_url`, `validation.video_size`, `validation.field_required`: the video could not be fetched, its size could not be read, or the title is missing (section 2 and above). All codes, with their HTTP status, are listed in [Errors](/guides/errors). ### Recovery * Retry a `failed` or `partial` post with `POST /v1/posts/{pid}/retry`. A retry costs 1 post credit. See [Delivery](/guides/delivery). * After an auth error, reconnect the account, then retry. *** ## 8. Full worked example This example uploads a video, publishes it with a title, description, tags, and a YouTube-side scheduled publish time, and checks the result. **Step 1: Upload the video** ```bash theme={null} curl "https://api.social-api.ai/v1/media/upload-url?media_type=video/mp4&filename=walkthrough.mp4" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "media_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "upload_url": "https://storage.example.com/media/walkthrough.mp4?X-Amz-Signature=abc", "expires_at": "2026-10-13T09:15:00Z" } ``` Upload the file to `upload_url`, then verify it: ```bash theme={null} curl -X PUT "https://storage.example.com/media/walkthrough.mp4?X-Amz-Signature=abc" \ -H "Content-Type: video/mp4" \ --data-binary @walkthrough.mp4 curl -X POST https://api.social-api.ai/v1/media/f47ac10b-58cc-4372-a567-0e02b2c3d479/verify \ -H "Authorization: Bearer $SOCAPI_KEY" ``` **Step 2: Publish the video** ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "title": "Spring collection walkthrough", "text": "A full tour of the new lineup. Chapters below.", "visibility": "private", "media": [{ "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }], "first_comment": "Which piece is your favorite? Let us know below.", "publish_now": true, "platform_data": { "youtube": { "category_id": "22", "tags": ["fashion", "spring", "lookbook"], "publish_at": "2026-10-20T10:00:00Z", "notify_subscribers": true, "playlist_id": "PL0123456789abcdef" } } }' ``` The response (HTTP 201) has `"status": "publishing"` and the post `id`, as in section 3. **Step 3: Check the result** ```bash theme={null} curl https://api.social-api.ai/v1/posts/7c9e6679-7425-40de-944b-e07fc1f90ae7 \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200) once the upload finishes: ```json theme={null} { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "title": "Spring collection walkthrough", "text": "A full tour of the new lineup. Chapters below.", "status": "published", "visibility": "private", "hidden": false, "retry_count": 0, "created_at": "2026-10-13T09:00:00Z", "updated_at": "2026-10-13T09:03:22Z", "published_at": "2026-10-13T09:03:22Z", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "youtube", "status": "published", "platform_post_id": "dQw4w9WgXcQ", "permalink": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "published_at": "2026-10-13T09:03:22Z" } ] } ``` The video is on YouTube as private and becomes public at `2026-10-20T10:00:00Z`. If the target is `failed`, read `targets[].error` (section 7), fix the request, and call `POST /v1/posts/{pid}/retry`. *** ## 9. Required OAuth scopes Publishing uses the scope SocialAPI's Google app requests when a channel connects. You register no Google app and configure nothing. | Scope | Used for | | - | - | | `https://www.googleapis.com/auth/youtube.force-ssl` | Upload, edit, and delete videos, add them to playlists, and post the first comment | The scope is also listed in [Scopes](/platforms/youtube#scopes). The scopes a scoped API key needs, the credit cost of a post, and the MCP tools that publish are on [Publishing](/guides/publishing). # Zalo API Source: https://docs.social-api.ai/platforms/zalo Connect a Zalo Official Account to read and answer direct messages and to publish, schedule, and delete articles through one API, with no Zalo app to register. SocialAPI connects a Zalo Official Account (OA), the business account on Zalo, Vietnam's messaging app. It reads and answers the direct messages (DMs) that people send to the OA, and it publishes and deletes OA articles. SocialAPI's own Zalo app connects the account, so you register no Zalo app and complete no step in the Zalo developer portal: see [Platform apps](/guides/platform-credentials). ## At a glance | Field | Value | | - | - | | Platform slug | `zalo` | | Auth | OAuth 2.0 with PKCE. An administrator of the Official Account signs in to Zalo and approves | | Platform API | Zalo Official Account Open API | | Status | Available on every plan | ## Capabilities An unsupported call returns `501 resource.not_supported`. | Feature | Status | Condition | | - | - | - | | Publish an article | Yes | Text only. See [Zalo publishing](/platforms/zalo/publishing) | | Publish a video | No | A `url` or `media_id` item on a Zalo target is ignored, and the article publishes with `text` only. A `platform_attachment_id` item fails the target with `422 publishing.media.unsupported_source` | | Schedule a post | Yes | `scheduled_at` on the post | | First comment on a post | No | `first_comment` is ignored | | Update a published article | No | `PATCH /v1/posts/{pid}` returns `409 post.state_invalid` for a published article. A `draft`, `scheduled`, or `failed` post can be edited before it publishes. To correct an article, delete it and publish a new one | | Delete a published article | Yes | | | List posts | Yes | Articles of type `normal`. See [List posts](/platforms/zalo/publishing#6-retrieve-posts) | | DMs: list, read, send | Yes | Sending needs the person to have interacted with the OA within 7 days: see [The 7-day messaging window](#the-7-day-messaging-window) | | DM attachments | Partial | A `url` or `media_id` attachment is sent as an image unless its `type` is `gif` or `sticker`. A `platform_attachment_id` attachment with `type: "file"` sends a file | | Buttons and a card | Partial | SocialAPI sends `buttons` and the first entry of `cards`. It ignores `quick_replies` and `list_sections` | | Quote a message in a DM | Yes | `reply_to_message_id` on the send request. It applies to the first message sent | | React to a DM | No | `501 resource.not_supported` | | Private reply to a comment | No | Zalo has no comments | | Comments | No | Zalo has no public comments API | | Mentions | No | Zalo has no public mentions API | | Reviews | No | Zalo has no public reviews API | | Automations | No | Automations run on Instagram and Facebook accounts only. `422 automation.platform_unsupported` | | Platform quota | No | `GET /v1/accounts/{id}/limits` returns `remain` and `total`: Zalo no longer reports its message quota, so do not rely on them. See [Rate limits](/guides/rate-limits#read-a-platforms-remaining-quota) | | Webhook events | Partial | `dm.received` for text messages only. See [Webhook events](#webhook-events) | The capability matrix for every platform is on [Supported platforms](/platforms/overview). ## Connect an account A Zalo Official Account connects through the shared OAuth flow in [Connect accounts](/guides/connect-accounts): send `platform: "zalo"` to `POST /v1/accounts/connect`, send the administrator to the returned authorization URL, and read the connected account's ID from your redirect. Each Zalo authorization covers exactly one Official Account. The connected account's `username` is the Official Account's ID and its `display_name` is the OA's name. To connect a second Official Account, run the flow again. SocialAPI renews Zalo's access tokens (valid for 25 hours) for you. When renewal fails, the first call that fails returns `401 platform.zalo.auth` and the account is marked `token_expired`. Every later call that acts on the account returns `403 account.reconnection_required` until you reconnect it. Reconnect with the same `brand_id`. ## The 7-day messaging window Zalo lets an Official Account send a customer service message only to a person who interacted with the OA within the last 7 days. Messages sent within 48 hours of the person's last interaction are free of Zalo's per-message fee. Messages sent after 48 hours but within 7 days can carry a fee on Zalo's side. A send outside the window, or to a person who is not eligible (for example, a person who does not follow the OA or who blocked it), fails with `422 platform.zalo.not_deliverable`. SocialAPI does not check the window before it sends: Zalo's refusal is the signal. The person's next message to the OA reopens the window. ## Direct messages You list conversations with `GET /v1/inbox/conversations`, read a conversation with `GET /v1/inbox/conversations/{conversation_id}/messages`, and reply with `POST /v1/inbox/conversations/{conversation_id}/messages`. The request fields are on [Direct messages](/guides/direct-messages). These rules are specific to Zalo: * **Zalo returns 10 at most.** One read returns the 10 most recent conversations or the 10 most recent messages of a conversation. * **A conversation is one person.** The conversation's participant ID is the person's Zalo user ID. * **Images by URL.** A `url` or `media_id` attachment is sent as an image unless its `type` is `gif` or `sticker`. * **One card.** SocialAPI sends the first entry of `cards` as one card and drops the others. * **No message ID in the send response.** `message_id` is empty and each entry of `message_ids` is an empty string. Read the message's `platform_id` from `GET /v1/inbox/conversations/{conversation_id}/messages`. * **No `message_tag`.** Zalo has no message tags: `message_tag` is ignored. * **Cost.** A send costs 1 interaction credit, the same as on every platform: see [Plans and usage](/guides/plans). ## Webhook events Zalo delivers a person's text message to SocialAPI as it happens, and SocialAPI sends it to your webhook endpoints as `dm.received`. The event carries `author.id` (the person's Zalo user ID) and `content.text`. It has no `conversation_id` and no `platform_id`: read the conversation with `GET /v1/inbox/conversations` to find the message. Images, files, stickers, follows, and read receipts raise no event. The full payload is on [Webhook events](/guides/webhook-events). Zalo also emits the `post.*` events for articles you publish through SocialAPI, `account.connected` when you connect the account, `account.disconnected` when SocialAPI flags the account for reconnection, and `account.updated` when the account loses [permissions](/guides/permissions) you did not remove yourself. ## Limits | Limit | Value | | - | - | | Conversations or messages per read | 10 | | Articles imported when you connect or sync an account | 20 most recent | | Article title | 150 characters | | Posts | 25 per hour and 50 per 24 hours for each account: see [Posting velocity caps](/guides/rate-limits#posting-velocity-caps) | | Customer service window | 7 days from the person's last interaction | Zalo applies its own quotas per app and per Official Account. When one is used up, the call fails with `429 platform.zalo.rate_limit`. Wait for the seconds in `Retry-After`, or at least 60 seconds when it is absent, then retry. ## Scopes SocialAPI sends no scope list to Zalo. The administrator of the Official Account approves the scopes that Zalo's consent screen lists for SocialAPI's Zalo app. Disconnecting the account from SocialAPI does not revoke that authorization on Zalo: the administrator removes it on Zalo. The account supports the `posts.read`, `posts.write`, `messages.read`, and `messages.write` [permissions](/guides/permissions), which SocialAPI alone enforces. ## Related * [Zalo publishing](/platforms/zalo/publishing): articles, drafts, and asynchronous creation * [Direct messages](/guides/direct-messages): the request fields for sending DMs * [Connect accounts](/guides/connect-accounts): brands, reconnecting, and disconnecting * [Webhook events](/guides/webhook-events): the `dm.received` payload * [Errors](/guides/errors): every Zalo error code * [Supported platforms](/platforms/overview): what each platform supports # Zalo publishing Source: https://docs.social-api.ai/platforms/zalo/publishing Publish, schedule, and delete text articles on a Zalo Official Account with SocialAPI, including asynchronous creation and title rules. ## 1. Overview A Zalo post is an article on a Zalo Official Account (OA). It goes through the same `POST /v1/posts` endpoint as every other platform: Zalo has no publishing endpoints of its own. SocialAPI publishes text articles only: it sends no images or videos. | Field | Value | | - | - | | Platform slug | `zalo` | | Auth type | OAuth 2.0 with PKCE | | Platform API | Zalo Official Account Open API | | Create post | Yes (text article, created asynchronously) | | Update post | Not after publishing (`409 post.state_invalid`). `draft`, `scheduled`, and `failed` posts can be edited | | Delete post | Yes | | Schedule | Yes (deferred publish via `scheduled_at`) | | First comment | No (`first_comment` is not published on Zalo) | | Posting caps | 25 posts per hour and 50 per 24 hours for each account: see [Posting velocity caps](/guides/rate-limits#posting-velocity-caps) | *** ## 2. Supported media | Content type | How to trigger | Notes | | - | - | - | | Text article | `text`, with an optional `title` | The article body is `text`. The title is `title`, or `text` when you send no `title` | | Image, video, carousel | Not supported | A `url` or `media_id` item on a Zalo target is ignored: the article publishes with `text` only, with no warning and no error. A `platform_attachment_id` item fails the Zalo target with `422 publishing.media.unsupported_source` | **Title limit:** Zalo limits an article title to 150 characters. SocialAPI does not check the length before it sends. When `text` is longer than 150 characters, send a `title` of 150 characters or fewer, because the title falls back to `text`. **Text limit:** SocialAPI sets no text limit for Zalo. Zalo's own rules apply, and a refusal fails the target: see [Quirks, errors, and recovery](#7-quirks-errors-and-recovery). *** ## 3. Create post Use `POST /v1/posts` with `targets` that name a Zalo account. `publish_now: true` publishes immediately. Without it and without `scheduled_at`, SocialAPI saves a draft. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "New store opening", "text": "Our new store opens this weekend. Visit us from 9:00 to 21:00.", "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }] }' ``` Response (HTTP 201, delivery continues after the response): ```json theme={null} { "id": "6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "title": "New store opening", "text": "Our new store opens this weekend. Visit us from 9:00 to 21:00.", "status": "publishing", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "zalo", "status": "publishing" } ] } ``` The Official Account shows the article with Zalo's article type `normal` and status `show`, so it is visible once Zalo finishes processing it. ### Platform data fields Zalo has no `platform_data` fields. SocialAPI forwards any additional keys in `platform_data.zalo` only when a table in this section lists them. None does, so SocialAPI sends no key to Zalo and returns no warning for an unlisted key. ### Draft validation SocialAPI checks a draft before it creates the article. A draft with an empty `title` and an empty `text` fails with `400 publishing.validation_failed`. `error.meta.issues` holds one issue: field `title`, message `Zalo articles require a title`. A draft with only `text` passes, because the title falls back to `text`. ### Asynchronous creation Zalo does not return a finished article. It accepts the article, processes it, and then makes an article ID available. SocialAPI waits 10 seconds for that ID. The target becomes `published` when the ID arrives, and its `platform_post_id` holds the article ID. If Zalo is still processing after 10 seconds, the target fails with `202 platform.zalo.article_processing`. The article can still finish publishing on Zalo. Check the Official Account before you send `POST /v1/posts/{pid}/retry`, because a retry creates a second article. ### Scheduling Set `scheduled_at` to an ISO 8601 timestamp (UTC) to defer publishing: ```json theme={null} { "title": "New store opening", "text": "Our new store opens today.", "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }], "scheduled_at": "2026-10-17T02:00:00Z" } ``` See [Scheduling](/guides/scheduling) for how scheduled posts settle. *** ## 4. Update post A published Zalo article cannot be edited through SocialAPI. `PATCH /v1/posts/{pid}` returns `409 post.state_invalid` for any post whose status is not `draft`, `scheduled`, or `failed`, with the message "Only draft, scheduled, or failed posts can be edited". A post in `draft`, `scheduled`, or `failed` status can be edited with `PATCH /v1/posts/{pid}` before it publishes. To correct a published article, delete it with `DELETE /v1/posts/{pid}` and publish a new article. *** ## 5. Delete post ```bash theme={null} curl -X DELETE https://api.social-api.ai/v1/posts/6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d \ -H "Authorization: Bearer $SOCAPI_KEY" ``` To remove only the Zalo target of a cross-platform post, unpublish it by `account_id`. `DELETE /v1/posts/{pid}` always removes every target. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts/6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d/unpublish \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{"account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J"}' ``` The article is removed from the Official Account. See [Delete a post](/guides/delivery#delete-a-post) for how a cross-platform post is handled. *** ## 6. Retrieve posts Use `GET /v1/posts` to list posts. Filter by platform or account: ```bash theme={null} curl "https://api.social-api.ai/v1/posts?platform=zalo&account_ids=acc_01HZ9X3Q4R5M6N7P8V2K0W1J" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Each post includes a `targets` array with the per-platform status. Results default to `sort=scheduled_asc`: pass `sort` to change the order. ```json theme={null} { "id": "6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "title": "New store opening", "text": "Our new store opens this weekend. Visit us from 9:00 to 21:00.", "status": "published", "targets": [ { "platform": "zalo", "platform_post_id": "c3a1f4e2b6d84a7f9e21", "status": "published", "metrics": { "likes": 0, "comments": 0, "shares": 3, "saves": 0, "extra": { "view_count": 120 } }, "metrics_synced_at": "2026-10-17T03:00:00Z" } ] } ``` **Metrics notes:** Zalo reports a view count and a share count for each article. `shares` holds the share count and `extra.view_count` holds the view count in `GET /v1/posts`. `GET /v1/posts/{pid}/metrics` returns the view count as `metrics.views` and `metrics.extra.views`. Zalo reports no likes, comments, or saves, so those stay `0`. **Articles published outside SocialAPI:** SocialAPI imports the 20 most recent articles of type `normal` when you connect the account and when you call `POST /v1/accounts/{id}/sync`. Older articles and articles of type `video` do not appear. *** ## 7. Quirks, errors, and recovery ### Text only SocialAPI publishes the text of an article and nothing else. A `url` or `media_id` item is ignored without an error, so a post meant to carry a video publishes as text. A `platform_attachment_id` item fails the Zalo target with `422 publishing.media.unsupported_source`. ### Title falls back to text The article title is the `title` you send, or `text` when you send none. A `text` longer than 150 characters needs its own `title`: Zalo refuses a longer title and the target fails. ### Asynchronous creation A new article can take longer than 10 seconds to process. See "Asynchronous creation" in section 3. The target fails with `202 platform.zalo.article_processing`, and the article can still appear on the Official Account. ### Posting caps SocialAPI refuses a publish that would pass 25 posts in an hour or 50 posts in 24 hours for one account, with `429 publishing.velocity_limit`. A scheduled post is held and published when the window clears. See [Posting velocity caps](/guides/rate-limits#posting-velocity-caps). ### Errors on a failed target A publish that fails after the request returned puts a structured error on the target, in `targets[].error`: ```json theme={null} { "code": "platform.zalo.article_processing", "message": "Article is still processing; try again shortly", "category": "platform", "caused_by": "platform" } ``` * `202 platform.zalo.article_processing`: Zalo was still processing after 10 seconds. Check the Official Account before you retry. * `401 platform.zalo.auth`: the access token expired or was revoked, or the Official Account is no longer authorized. The account is marked `token_expired`, and every later call that acts on the account returns `403 account.reconnection_required` until you reconnect it. * `429 platform.zalo.rate_limit`: Zalo's quota for the app or the Official Account is used up. Wait, then retry. * `400 validation.platform_param`: Zalo rejected a parameter, for example a title over 150 characters. `error.message` carries Zalo's reason. * `502 platform.zalo.e`: Zalo returned an error SocialAPI does not map. `` is the absolute value of Zalo's numeric code. Every code is in [Errors](/guides/errors). ### Recovery * **Still processing.** Open the Official Account. If the article is there, do nothing. If it is not, send `POST /v1/posts/{pid}/retry`. See [Delivery](/guides/delivery). * **Auth error.** Connect the account again with `POST /v1/accounts/connect`, using the same `platform` and `brand_id`. See [Connect accounts](/guides/connect-accounts). * **Parameter error.** Fix the field that `error.message` names, then retry. *** ## 8. Full worked example Zalo articles carry text only, so there is no upload step. The example publishes an article, checks its status, and deletes it. **Step 1: Publish the article** ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "New store opening", "text": "Our new store opens this weekend. Visit us from 9:00 to 21:00.", "publish_now": true, "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }] }' ``` Response (HTTP 201, delivery continues after the response): ```json theme={null} { "id": "6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "status": "publishing", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "zalo", "status": "publishing" } ] } ``` **Step 2: Check the status** ```bash theme={null} curl "https://api.social-api.ai/v1/posts/6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200) once Zalo finished processing: ```json theme={null} { "id": "6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "title": "New store opening", "text": "Our new store opens this weekend. Visit us from 9:00 to 21:00.", "status": "published", "targets": [ { "platform": "zalo", "platform_post_id": "c3a1f4e2b6d84a7f9e21", "status": "published" } ], "created_at": "2026-10-16T09:00:00Z", "published_at": "2026-10-16T09:00:04Z" } ``` **Step 3: Delete the article** ```bash theme={null} curl -X DELETE https://api.social-api.ai/v1/posts/6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "success": true, "deleted": true, "results": [ { "platform": "zalo", "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "outcome": "deleted" } ] } ``` *** ## 9. Required OAuth scopes SocialAPI's Zalo app requests no scope list. The administrator of the Official Account approves the scopes on Zalo's consent screen when connecting. You configure nothing. See [Zalo scopes](/platforms/zalo#scopes). The scopes a scoped API key needs, the credit cost of a post, and the MCP tools that publish are on [Publishing](/guides/publishing). # Quickstart Source: https://docs.social-api.ai/quickstart Get an API key, connect an Instagram account, read and reply to a comment, and publish a post with the SocialAPI REST API in about 10 minutes. This quickstart takes you from a new SocialAPI account to a connected Instagram account, a comment you read and answer, and a post you publish, all with `curl`. It takes about 10 minutes. ## Before you start * **A SocialAPI account.** Sign up at [app.social-api.ai](https://app.social-api.ai). The Free plan is enough: this walkthrough costs 1 post credit and 1 interaction credit. See [Plans and usage](/guides/plans). * **An Instagram Business or Creator account** that you can sign in to. A personal Instagram account cannot be connected. * **A comment on one of that account's posts**, written from another Instagram account, for step 4. * **A public image URL** (JPEG) for step 5. **1. Create an API key** In the [dashboard](https://app.social-api.ai/api-configuration?tab=keys), open **API Keys**, click **Create API Key**, name it, and click **Create key**. > [!WARNING] > > The dashboard shows the key once. If you lose it, revoke it and create another. It starts with `sapi_key_`. Store it in your environment: ```bash theme={null} export SOCAPI_KEY="sapi_key_your_key_here" ``` To check that it works, list your accounts. A new SocialAPI account has none: ```bash theme={null} curl https://api.social-api.ai/v1/accounts \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [], "count": 0 } ``` A `401` means the key is wrong: see [Authentication](/guides/authentication). **2. Connect an Instagram account** Start the connection. SocialAPI's own app connects Instagram, so you register nothing with Meta: ```bash theme={null} curl -X POST https://api.social-api.ai/v1/accounts/connect \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{"platform": "instagram"}' ``` Response (HTTP 202): ```json theme={null} { "auth_url": "https://www.instagram.com/oauth/authorize/?client_id=1234567890&redirect_uri=https%3A%2F%2Fapi.social-api.ai%2Foauth%2Fcallback%2Finstagram&response_type=code&state=4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2#weblink", "state": "4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2", "message": "Open this link to connect your instagram account. Once you've authorized, the account will be connected automatically." } ``` Open `auth_url` in your browser, sign in to Instagram, and approve. You have 1 hour from this request. SocialAPI creates a brand named after the account to hold it. Check that it worked: ```bash theme={null} curl https://api.social-api.ai/v1/accounts \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "instagram", "name": "Acme Corp", "username": "acmecorp", "brand_id": "1c9f6f0e-2b3a-4c5d-8e7f-0a1b2c3d4e5f", "status": "active", "permissions": null, "effective_permissions": ["posts.read", "posts.write", "comments.read", "comments.write", "messages.read", "messages.write", "analytics.read"] } ], "count": 1 } ``` Copy the account's `id`. An empty `data` means the approval did not finish: run this step again. Other platforms, and the slug each one uses, are listed in [Platform slugs](/guides/connect-accounts#platform-slugs). X, Telegram, Bluesky, and App Store Connect use your own app or credentials: see [Platform apps](/guides/platform-credentials). **3. List the posts that have comments** ```bash theme={null} curl "https://api.social-api.ai/v1/inbox/comments?account_id=acc_01HZ9X3Q4R5M6N7P8V2K0W1J&min_comments=1" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "user_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "instagram", "id": "17895695668004550", "content": "Spring collection is live", "thumbnail": "https://scontent.cdninstagram.com/v/t51/spring.jpg", "permalink": "https://www.instagram.com/p/C8xY2AbC/", "comment_count": 4, "like_count": 87, "published_at": "2026-10-12T09:00:00Z", "created_at": "2026-10-12T09:00:05Z", "updated_at": "2026-10-13T14:30:00Z" } ], "pagination": { "has_more": false } } ``` The `id` is the platform's ID of the post. An empty `data` means no post has comments yet: write a comment from another Instagram account and run the request again. **4. Read and reply to a comment** Read the comments on that post. `account_id` is required: ```bash theme={null} curl "https://api.social-api.ai/v1/inbox/comments/17895695668004550?account_id=acc_01HZ9X3Q4R5M6N7P8V2K0W1J" \ -H "Authorization: Bearer $SOCAPI_KEY" ``` Response (HTTP 200): ```json theme={null} { "data": [ { "id": "sapi_cmt_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "platform_id": "17858893269577012", "platform": "instagram", "text": "Love this product!", "author_id": "17841400008460056", "author_name": "Jane Smith", "author_username": "janesmith", "author_picture": "", "is_owner": false, "like_count": 2, "reply_count": 1, "has_replies": true, "liked_by_viewer": false, "is_hidden": false, "parent_id": null, "created_at": "2026-10-13T14:30:00Z", "capabilities": { "can_reply": true, "can_delete": true, "can_hide": true, "can_like": false, "can_private_reply": true } } ], "pagination": { "has_more": false, "next_cursor": "" } } ``` Reply with the comment's `id`. This costs 1 interaction credit: ```bash theme={null} curl -X POST "https://api.social-api.ai/v1/inbox/comments/17895695668004550" \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "comment_id": "sapi_cmt_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I", "text": "Thank you, Jane!" }' ``` Response (HTTP 200): ```json theme={null} { "success": true, "comment_id": "sapi_cmt_TT974YVQH4RAHNN2HY2DJSNY9111D7LMTP1X" } ``` The reply is public on Instagram. `comment_id` is the new reply's ID. A `501 resource.not_supported` means the platform does not allow that action: check `capabilities.can_reply` on the comment first. **5. Publish a post** Publish an image to the account. Replace the `source` URL with a public JPEG URL. Instagram needs `content_type` when you publish, and `publish_now: true` is what publishes: without `publish_now` or `scheduled_at`, the request only saves a draft. ```bash theme={null} curl -X POST https://api.social-api.ai/v1/posts \ -H "Authorization: Bearer $SOCAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Our spring collection is live.", "publish_now": true, "media": [ { "source_type": "url", "source": "https://cdn.example.com/launch.jpg" } ], "platform_data": { "instagram": { "content_type": "feed" } }, "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" } ] }' ``` Response (HTTP 201): ```json theme={null} { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "text": "Our spring collection is live.", "status": "publishing", "hidden": false, "retry_count": 0, "created_at": "2026-10-13T12:00:00Z", "updated_at": "2026-10-13T12:00:00Z", "targets": [ { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "instagram", "status": "publishing" } ] } ``` A `201` means SocialAPI accepted the post, not that Instagram published it. Read the post until its `status` is `published`, `partial`, or `failed`: ```bash theme={null} curl https://api.social-api.ai/v1/posts/7c9e6679-7425-40de-944b-e07fc1f90ae7 \ -H "Authorization: Bearer $SOCAPI_KEY" ``` When it worked, the post's `status` is `published` and the target has a `platform_post_id` and a `permalink`. When it failed, the reason is in `targets[].error`. To upload a file instead of linking one, see [Media](/guides/media). ## What you built You now have an API key, a connected Instagram account, a comment you read and answered, and a post you published. The same calls work on every platform that supports them: [Supported platforms](/platforms/overview) shows which. ## Next steps > [card] [Core concepts](/guides/concepts) > > Brand, account, post, interaction, and capability. > [card] [Unified inbox](/guides/inbox) > > Comments, DMs, reviews, and mentions. > [card] [Automations](/guides/automations) > > Answer matching comments and DMs with no code on your side. > [card] [Publishing](/guides/publishing) > > Fields every platform shares, scheduling, and media. > [card] [Webhooks](/guides/webhooks) > > Receive new comments, messages, and post results on your server. ## Related * [Authentication](/guides/authentication) for scoped keys, key rotation, and OAuth tokens * [Connect accounts](/guides/connect-accounts) for brands, reconnecting, and letting someone else connect * [Errors](/guides/errors) for every error code and how to handle it * [Plans and usage](/guides/plans) for allowances and what costs a credit