# PostLake API reference > Markdown version of https://postlake.dev/docs/api . The canonical page for humans. > Machine-readable spec: https://api.postlake.dev/openapi.json Base URL: `https://api.postlake.dev`. Every request needs `Authorization: Bearer `. ## GET /v1/me/limits What this key may do, and what it can spend. The authenticated key's machine identity, enforced publishing policy, connected channels, credit balance, and rate limits. rateLimits.apiPerMinute and postsPerMinute are the workspace ceiling. rateLimits.profile is the per-Channels-profile cap, so one end user cannot starve the others. Networks still apply their own caps (TikTok is about 6 requests a minute per account). MCP agents receive the same view through whoami. Read this before planning a batch rather than discovering a limit by being refused. ## PATCH /v1/me/agent Set this agent's own display name. Changes only the authenticated API key's display nickname. It does not rename the key, change its permissions, or affect another agent. The account owner can lock the display name in Agent Control. Pass null to return to the key's original name. Request body (JSON): - `nickname` (string,null, required) ## GET /v1/notifications What happened across every network. Likes, replies, mentions, follows, reposts and quotes, newest first, in one normalised shape. `problems` names any network that could not be read, so an empty `items` never silently means 'we could not look'. Parameters: - `account` (query): One connected account id (acc_…). Omit for every connected network. - `cursor` (query): Opaque cursor from a previous page - `limit` (query): 1-100 ## POST /v1/notifications/seen Mark notifications as seen. On the networks that track it. Networks with no concept of 'seen' are skipped. Parameters: - `account` (query): One connected account id (acc_…). Omit for every connected network. ## GET /v1/posts/{id}/comments Replies on a post you published. Across every network the post went to. Parameters: - `id` (path, required) - `nested` (query): Read the whole thread including replies to replies. Networks that cannot go deeper answer with the top level rather than refusing. - `cursor` (query): Opaque cursor from a previous page - `limit` (query): 1-100 ## GET /v1/discover/posts Search public posts. Search the network itself rather than your own posts, so an agent can see what is already being said before it writes. Runs on every connected network that supports searching; the ones that cannot are named in `problems`. Two limits decide whether a result means anything on Instagram, and neither is a rate limit you can retry past. Instagram has NO keyword search: a multi-word query cannot match a hashtag and comes back as a `problems` entry rather than an empty list. And Meta caps this at 30 UNIQUE hashtags per account per 7 days, so a scouting run has a weekly budget. `sort=recent` reads Meta's recent_media edge, which only covers the last 24 hours, so an empty recent result means nothing was posted today rather than that the tag is unused. Instagram results also carry `authorHidden: true` and can never name their author: Meta strips personally identifiable information from them, so post search cannot be used to find someone to contact. Parameters: - `q` (query, required): What to search for - `mode` (query): Search the words (default) or a topic tag - `sort` (query): Best match (default) or newest first - `mediaType` (query) - `author` (query): Only this handle's posts - `since` (query) - `until` (query) - `account` (query): Search one connection only. Omit to search them all. - `cursor` (query) - `limit` (query): 1-100 ## GET /v1/discover/profiles/{handle} Look someone up. One network only: the same handle on two networks is usually two different people, so merging them would invent someone who does not exist. Counts are null where the network does not publish them, which is not the same as zero. Parameters: - `handle` (path, required): With or without the @ - `account` (query, required): Which connection to look on Returns: `PublicProfile`. ## GET /v1/discover/profiles/{handle}/posts Someone else's public posts. Parameters: - `handle` (path, required) - `account` (query, required) - `cursor` (query) - `limit` (query): 1-100 ## GET /v1/discover/creators Find creators to work with. Search Meta creator discovery for people a brand could partner with. A Facebook Page searches Facebook Creator Discovery directly. An Instagram account connected through Facebook searches Instagram Creator Marketplace. Entries marked `sample: true` are test data, not real outreach targets. Parameters: - `account` (query, required): A Facebook Page for Facebook Creator Discovery, or an Instagram account connected through Facebook for Instagram Creator Marketplace. - `q` (query): Free-text search. - `countries` (query): Comma-separated country codes. - `limit` (query) - `cursor` (query) ## GET /v1/discover/places Find a place to tag on a post. Pass a name, or a latitude and longitude together. The id returned goes in the post's `locationId` option, and only works on the network it came from. Parameters: - `account` (query, required) - `q` (query) - `latitude` (query): Use with longitude - `longitude` (query): Use with latitude ## POST /v1/comments/{id}/hide Hide or unhide a reply. The other half of moderating a comment section. Without it the only answer to an abusive reply is to reply to it. A failure means the reply is still visible, so treat an error as "still there" rather than assuming it worked. Parameters: - `id` (path, required): The comment id Request body (JSON): - `account` (string, required): The connection the post is on - `hidden` (boolean): false puts it back ## DELETE /v1/comments/{id} Delete a comment. Take a comment down for good. Facebook cannot hide a Page's own comment, so this is how you retract one you posted. A failure means it is still there. Parameters: - `id` (path, required): The comment id Request body (JSON): - `account` (string, required): The connection the post is on ## GET /v1/social-accounts/{id}/following Who a connected account follows. Parameters: - `id` (path, required) - `cursor` (query) - `limit` (query) ## PATCH /v1/social-accounts/{id}/profile Edit a connected account's own profile. Change the display name, bio, avatar or banner. Only the fields you send are changed. Images are given as public URLs and uploaded for you. A network that does not allow this refuses. Parameters: - `id` (path, required) Request body (JSON): - `displayName` (string) - `bio` (string) - `avatarUrl` (string) - `bannerUrl` (string) ## GET /v1/social-accounts/{id}/followers Who follows a connected account. Parameters: - `id` (path, required) - `cursor` (query): Opaque cursor from a previous page - `limit` (query): 1-100 ## POST /v1/comments/{id}/replies Reply to a comment. Answer a comment someone left, by that comment's id (from GET /v1/posts/{id}/comments). Distinct from publishing: this acts on someone else's content. Free everywhere except X, where a reply is billed as a post. Parameters: - `id` (path, required): The comment id Request body (JSON): - `account` (string, required): The connected account id (acc_…) replying - `text` (string, required): The reply ## POST /v1/engagements Like, repost, follow, block or mute. One endpoint for every engagement. Free: engaging never spends credits. A network that does not support the action refuses and names what it does support, so nothing silently does nothing. Request body (JSON): - `account` (string, required): The connected account id (acc_…) acting - `action` (string, required) - `target` (string, required): A post uri/url for like and repost; a handle for follow, block and mute ## GET /v1/conversations Direct message threads. Parameters: - `account` (query): One connected account id (acc_…). Omit for every connected network. - `cursor` (query): Opaque cursor from a previous page - `limit` (query): 1-100 ## POST /v1/conversations Open a conversation with someone. Finds or starts the thread with a handle, so a first message does not need an id that does not exist yet. Sends nothing. Request body (JSON): - `account` (string, required) - `handle` (string, required): e.g. alice.bsky.social Returns: `Conversation`. ## GET /v1/conversations/{id}/messages Messages in one thread. Parameters: - `id` (path, required) - `account` (query, required): Required: a conversation id only means something on one connection - `cursor` (query): Opaque cursor from a previous page - `limit` (query): 1-100 ## POST /v1/conversations/{id}/messages Send a direct message. Free on supported networks except X, where sending a direct message costs 6 credits. Parameters: - `id` (path, required) Request body (JSON): - `account` (string, required) - `text` (string, required) - `humanAgent` (boolean): Assert that a PERSON wrote this reply. Meta allows a reply within 24 hours of someone's last message; the Human Agent tag extends that to 7 days and is granted only for replies a human composed. Never set it for an automated reply: the account carries the penalty, not the caller. Returns: `Message`. ## POST /v1/conversations/{id}/read Mark a conversation read. Parameters: - `id` (path, required) Request body (JSON): - `account` (string, required) ## GET /v1/me Get current account. Validate API key and echo account info, including the optional default timezone. ## PATCH /v1/me Update current account. Set or clear the account default timezone used for naive scheduledAt values. Request body (JSON): - `timezone` (string): IANA name such as Europe/London. Empty string or null clears the default. ## GET /v1/account/export Export account data. GDPR data export. All data for the account (no secrets). ## GET /v1/audit Get audit log. Security audit trail (logins, key creation, disconnects). Returns up to 100 events. ## POST /v1/social-accounts/connect Connect social account. Connect a social account to PostLake with supported credentials. X requires the hosted POST /v1/connect-link OAuth flow and always uses PostLake's shared app. External X tokens are not accepted here. Request body (JSON): - `platform` (object, required) - `profile` (string): Optional profile username to attach to - `handle` (string): Platform-specific handle - `appPassword` (string): Platform-specific password (e.g. for Bluesky) ## GET /v1/social-accounts List social accounts. List connected social accounts. Cursor-paginated. Parameters: - `profile` (query): Filter by profile username - `limit` (query): Page size, 1-100 (default 50). - `cursor` (query): Opaque cursor from the previous response's `nextCursor`. ## GET /v1/social-accounts/{id}/disconnect-preview Preview channel disconnect. Read-only, on-demand preview of at most 100 affected scheduled posts. Returns a full-state snapshot. Queued posts, schedules due within 30 seconds, or more than 100 schedules block disconnect. Larger selections must first use bounded bulk destination edits. No polling or historical-post scan is required. Parameters: - `id` (path, required) ## GET /v1/social-accounts/{id} Retrieve a social account. One connected account by id. Same shape as a row in the list, so anything holding an acc_… can ask about it directly instead of paging the list and filtering. Parameters: - `id` (path, required) Returns: `SocialAccount`. ## DELETE /v1/social-accounts/{id} Disconnect a social account. Disconnect at the owner's request. Published posts stay untouched. scheduledPosts=keep (default) preserves schedules and stable identity for reconnection. remove removes this destination from scheduled posts; posts with no destinations left become drafts with their content preserved. remove requires expectedSnapshot from disconnect-preview. Confirmation is fenced against full-post edits, new affected schedules and publishing claims in one bounded transaction. Provider revocation is best effort after the guarded local transaction. Maximum 100 schedules; larger selections require bulk destination edits first. No credits are charged. Parameters: - `id` (path, required) - `scheduledPosts` (query) - `expectedSnapshot` (query): Full-state snapshot from disconnect-preview. Required for remove. ## GET /v1/social-accounts/{id}/targets List social account targets. List postable destinations within an account (Pinterest boards, Facebook Pages). This is also how you turn a board id from a post record back into a name: a post stores the id it published to, and this is the lookup. Parameters: - `id` (path, required) ## GET /v1/social-accounts/{id}/facebook-pages/search Search Facebook Pages for a mention. Resolve an eligible Facebook Page name to the numeric ID needed by platformOptions.facebook.mentionPages. The connected account must be Facebook. If Meta does not permit Page lookup on that connection, the successful response explains that callers need the Page ID instead. Parameters: - `id` (path, required): Connected Facebook account ID. - `q` (query, required): At least two characters from the Page name. ## PATCH /v1/profiles/{id} Rename profile. Rename a profile. The id is unchanged, so connected channels and posts are untouched, but the slugified identifier moves with the name. The response reports `previousUsername` and `usernameChanged` so anything addressing the profile by its old name can be updated. Request body (JSON): - `name` (string, required) ## DELETE /v1/profiles/{id} Delete profile. Delete a profile. This ALSO disconnects every channel in it, and reconnecting each one requires its owner to approve access on that network again. It therefore refuses with 400 while channels are attached, naming them, unless `?force=true` is passed. Parameters: - `force` (query): Go ahead even though channels will be disconnected. ## GET /v1/email-preferences Read email preferences. How often this account receives the attention summary: the one email PostLake sends on its own schedule, when a channel has stopped working or a scheduled post did not go out. Account and security email (verify, password reset, new sign-in, payment failed) always sends and is not covered here. Returns: `EmailPreferences`. ## PATCH /v1/email-preferences Set email preferences. Set how often the attention summary may arrive and whether optional product emails may send. Send at least one field. `daily` sends at most once a day and only on days something is actually wrong; `weekly` gathers anything still unresolved into at most one email a week; `off` stops it entirely. `onboarding` controls optional product emails. There is deliberately no MCP tool for this: these emails are how a person learns a post did not go out, and an agent that could switch them off could hide a failure it caused. Request body (JSON): - `digestFrequency` (string) - `onboarding` (boolean): Whether to receive optional product emails. Returns: `EmailPreferences`. ## POST /v1/profiles Create profile. Create a named profile. Send `name` as a person would write it (spaces and capitals are fine); it is slugified into the identifier agents address the profile by, and the original is kept for display. `username` is still accepted as an alias for `name`. Request body (JSON): - `name` (string): e.g. "Otaku Gems News", stored as otaku-gems-news - `username` (string): Alias for name, kept for compatibility. Returns: `Profile`. ## GET /v1/profiles List profiles. List profiles ## GET /v1/platforms Platform capabilities (public). Machine-readable capabilities per platform: character limits, media rules (required / video-only / max images / formats / sizes), whether publishing is asynchronous, and every platformOptions field with its valid values. No authentication required. ## GET /v1/platforms/{platform} One platform's capabilities (public). Parameters: - `platform` (path, required) Returns: `PlatformCapabilities`. ## POST /v1/posts/validate Validate a post (dry run). Runs the exact validation a publish would run, covering account resolution, media resolution and capability-registry rules, without touching any platform. Returns per-target errors (blocking) and warnings (advisory). Free to call before POST /v1/posts. Request body (JSON): - `text` (string, required): Caption for feed posts and Reels. May be empty for an Instagram Story-only post; Instagram Stories do not display captions. - `campaign` (string): Optional lowercase campaign slug, up to 80 characters. - `assetMetadata` (object): Up to 16 short caller-defined fields, such as hook, track or source filename. - `profile` (string): A profile name. Resolves to every account it owns. The simple way to address accounts; combine with `platforms` to narrow it. Provide `profile`, `accounts`, or both. - `platforms` (array): Optional filter. Keeps only these networks from the resolved set. - `accounts` (array): Connected account ids (acc_…). An alternative (or addition) to `profile`. - `media` (array) - `scheduledAt` (string) - `timezone` (string): IANA timezone (e.g. Europe/London). Interprets a naive scheduledAt as wall time in that zone. Stored fire time is always UTC. - `platformOptions` (object) ## POST /v1/social-accounts/{id}/webhook Subscribe this channel to webhooks. Some networks need the subscription on the ACCOUNT as well as the app. Facebook is the case: subscribing the app is not enough, the Page has to be subscribed too or nothing is ever delivered, however well the callback is configured. Defaults to feed, mention and messages. Parameters: - `id` (path, required) Request body (JSON): - `fields` (array): Which events to subscribe to. Defaults to feed, mention, messages. ## GET /v1/social-accounts/{id}/webhook What this channel is subscribed to. The events this channel currently receives, so "we are not getting webhooks" has an answer. A network that does not need a per account subscription returns an empty list. Parameters: - `id` (path, required) ## GET /v1/social-accounts/{id}/posts This channel's own posts, from the network. The account's own posts read live from the network, including ones published outside PostLake. Different from GET /v1/posts, which lists what PostLake itself published: an account with existing history has posts we never sent, and this is how to reach them. Parameters: - `id` (path, required) - `cursor` (query) - `limit` (query) ## GET /v1/social-accounts/{id}/tagged Posts other people tagged you in. Media published by SOMEBODY ELSE that tagged this account. Different from a mention, which names you in text, and different from your own posts. This is what other people said about you, which is usually the thing worth watching. Parameters: - `id` (path, required) - `cursor` (query) - `limit` (query) ## GET /v1/social-accounts/{id}/allowance Publishing headroom left. How many posts this channel has left in the network's own rolling window, read from the network rather than counted locally. Check it before planning a batch: the alternative is discovering the ceiling by being refused partway through one, with some posts already live and some not. Parameters: - `id` (path, required) ## GET /v1/social-accounts/{id}/ad-accounts Ad account lookup (deferred). Unavailable for Facebook and Instagram while PostLake defers Meta advertising features. Returns HTTP 501 with code meta_ads_deferred, not an empty account list. Reconnecting or using an older token will not enable it. The former success schema is retained for client compatibility. Parameters: - `id` (path, required) ## GET /v1/social-accounts/{id}/branded-partners Branded partner lookup (deferred). Unavailable for Facebook and Instagram: these partner lookups depend on deferred Meta advertising permissions. Returns HTTP 501 with code meta_ads_deferred. This does not remove non-ad branded-content publishing permissions or ordinary tagged-post reads. The former success schema is retained for client compatibility. Parameters: - `id` (path, required) ## GET /v1/social-accounts/{id}/partnership-permissions Facebook Partnership Ads permissions (deferred). Unavailable while PostLake defers Meta advertising features. Returns HTTP 501 with code meta_ads_deferred without contacting Meta. Reconnecting will not enable it. The former success schema is retained for client compatibility. Parameters: - `id` (path, required) - `direction` (query) ## POST /v1/social-accounts/{id}/partnership-permissions Act on a Facebook Partnership Ads permission (deferred). Unavailable while PostLake defers Meta advertising features. Returns HTTP 501 with code meta_ads_deferred. No request is sent to Meta and no partner is notified. The former success schema is retained for client compatibility. Parameters: - `id` (path, required) Request body (JSON): - `partnerPageId` (string, required) - `action` (string, required) ## GET /v1/social-accounts/{id}/branded-content-posts Facebook branded-content posts (deferred). Unavailable because this read requires the deferred facebook_branded_content_ads_brand permission. Returns HTTP 501 with code meta_ads_deferred without contacting Meta. Ordinary tagged-post reads are unaffected. The former success schema is retained for client compatibility. Parameters: - `id` (path, required) - `startDate` (query) - `endDate` (query) ## POST /v1/social-accounts/{id}/events Create a scheduled event. Create an event on the account, where the network allows it through the API. Instagram calls this a reminder and normally expects it to be added while composing a post in the app; this asks the API directly, and a network that refuses says so in its own words. The returned id goes in the `upcomingEventId` post option. Parameters: - `id` (path, required) Request body (JSON): - `title` (string, required) - `startsAt` (string, required) - `endsAt` (string) ## GET /v1/social-accounts/{id}/events Scheduled events on this channel. Events scheduled on the account (a launch, a drop, a live), whose id goes in the `upcomingEventId` post option to put a reminder button on the post. Events are created in the network's own app; this reads them. Instagram needs a connection made through Facebook. A network without events returns an empty list. Parameters: - `id` (path, required) ## GET /v1/social-accounts/{id}/products Shoppable products on this channel. The account's own shop catalogue, so you can find the product ids the `productIds` post option takes. Only networks with a shop answer; anything else returns an empty list rather than an error, because "nothing to tag" is a true answer to the question. Instagram needs an approved Shop and a catalogue on the same business. Parameters: - `id` (path, required) - `q` (query): Filter by product name. - `cursor` (query) - `limit` (query) ## GET /v1/social-accounts/{id}/publish-info Creator-level publish constraints. Live constraints for one connected account (TikTok creator_info): available privacy options, whether comments/duet/stitch are allowed, and this creator's max video duration. Render post UIs from this. 404 for platforms without creator-level constraints. Parameters: - `id` (path, required) ## GET /v1/social-accounts/{id}/live-broadcasts List Facebook Page live broadcasts. Lists current and recent broadcasts from Facebook. It never returns an ingest URL or stream key. Parameters: - `id` (path, required) - `limit` (query) ## POST /v1/social-accounts/{id}/live-broadcasts Create a Facebook Page live broadcast. Prepares a Facebook Page broadcast. Facebook returns a one-time secure ingest URL, and may return a separate stream key, for compatible streaming software. PostLake never stores either value. Start the encoder, then call the start endpoint to make it live. Preparing is free; a credit is used only after Facebook confirms the broadcast is live. Meta independently requires the Facebook account managing the Page to be at least 60 days old, the Page to have at least 100 followers, and the person connecting it to have Page-management permission. PostLake cannot override those checks. Parameters: - `id` (path, required) Request body (JSON): - `title` (string, required) - `description` (string) ## POST /v1/social-accounts/{id}/live-broadcasts/{liveId}/start Make a prepared Facebook Page broadcast live. Makes a prepared Facebook broadcast visible now. Start your encoder before calling this endpoint. Facebook is authoritative for whether the broadcast becomes live. Parameters: - `id` (path, required) - `liveId` (path, required) ## GET /v1/social-accounts/{id}/live-broadcasts/{liveId} Read a Facebook live broadcast. Reads Facebook's authoritative broadcast status and final replay URL when available. Ingest credentials are never returned. Parameters: - `id` (path, required) - `liveId` (path, required) ## POST /v1/social-accounts/{id}/live-broadcasts/{liveId}/end End a Facebook live broadcast. Ends the Facebook broadcast. A successful response reflects Facebook's final status and replay URL where Facebook provides one. Parameters: - `id` (path, required) - `liveId` (path, required) ## POST /v1/posts Publish, schedule or draft a post. Publish or schedule a post. Set `draft: true` to save it instead. A guarded agent's normal publish request is stored in `awaiting_approval` automatically and cannot be approved by that same agent. Neither state costs anything or contacts a network. Request body (JSON): - `text` (string, required): Caption for feed posts and Reels. May be empty for an Instagram Story-only post; Instagram Stories do not display captions. - `campaign` (string): Optional lowercase campaign slug, up to 80 characters. - `assetMetadata` (object): Up to 16 short caller-defined fields, such as hook, track or source filename. - `profile` (string): A profile name. Resolves to every account it owns. The simple way to address accounts; combine with `platforms` to narrow it. Provide `profile`, `accounts`, or both. - `platforms` (array): Optional filter. Keeps only these networks from the resolved set (e.g. profile "my-brand" + platforms ["bluesky"]). - `accounts` (array): Connected account ids (acc_…). An alternative (or addition) to `profile`. - `media` (array) - `requireAllTargets` (boolean): When true, refuse the whole request before contacting a network if any selected destination fails PostLake's deterministic capability preflight. Default false preserves partial fan-out. - `scheduledAt` (string): UTC with a trailing Z, or a naive local time together with timezone. - `timezone` (string): IANA timezone (e.g. Europe/London). Interprets a naive scheduledAt as wall time in that zone. - `draft` (boolean): Save without sending. Returns `draft`, or `awaiting_approval` when this agent requires approval. Nothing is charged and no network is contacted. - `platformOptions` (object) Returns: `Post`. ## GET /v1/posts List posts. List posts by creation time (default), or use state=scheduled and sort=scheduledAt to walk a scheduled calendar chronologically. The scheduled cursor remains valid if earlier posts are cancelled. Parameters: - `sort` (query): scheduledAt requires state=scheduled. Return posts in ascending scheduled time with stable keyset pagination. - `q` (query): Case-insensitive literal caption search, applied before pagination. - `campaign` (query): Exact campaign slug. - `targetCount` (query): Single-destination or shared multi-destination posts. - `state` (query) - `account` (query): Filter to posts that targeted this connected account id (acc_…). - `profile` (query): Filter to posts that targeted a channel in this profile (username). - `network` (query): Filter to posts that targeted this social network. - `agent` (query): Filter by stable agent id (plc_… or key_…). Use `any` for all machine-created posts or `manual` for dashboard-created posts. - `surface` (query): Filter by creation surface. This distinguishes dashboard work, direct API integrations, and agent tools. - `approval` (query): Filter the human approval workflow. - `from` (query): Include posts on or after this date. Uses published time, scheduled time, then creation time. - `to` (query): Include posts on or before this date. Uses published time, scheduled time, then creation time. - `limit` (query) - `cursor` (query) ## POST /v1/posts/cadence/preview Preview a timezone-aware publishing cadence. Returns deterministic UTC slots without creating, reserving, or charging for posts. Call POST /v1/posts/validate for each proposed post before creating it. Platform pacing limits remain authoritative at publish time. Ambiguous or nonexistent local times are rejected. Request body (JSON): - `timezone` (string, required): IANA timezone, such as Europe/London. - `startDate` (string, required) - `days` (integer, required) - `localTime` (string, required): Local HH:mm start time. - `tracks` (array, required) - `gapMinutes` (integer) - `occupiedAt` (array): Existing UTC or offset-qualified posts to avoid. No database lookup is performed. ## POST /v1/posts/calendar Preview or atomically create a scheduled calendar. Validate 1 to 200 scheduled posts as a batch. Preview with dryRun=true, then apply with the returned snapshot and a unique idempotencyKey. Either every post and the receipt are stored or none are. A cadence may assign UTC instants from startAt and everyMinutes; omit it to give each post its own scheduledAt. Credits are charged when posts publish, not when the calendar is created. Timer arming is best-effort, with the due-post runner as a backstop. Request body (JSON): - `posts` (array, required): Normal POST /v1/posts bodies with a future scheduledAt, unless cadence assigns the times. Drafts and partial destinations are not accepted. - `cadence` (object) - `dryRun` (boolean, required) - `snapshot` (string): Required for apply; return value from preview. - `idempotencyKey` (string): Required for apply; 8 to 128 letters, digits, underscores or hyphens. ## PATCH /v1/posts/bulk Preview or atomically edit scheduled posts. Select 1 to 200 exact post IDs. First send dryRun=true and inspect the per-post before/after diff. Apply with dryRun=false and the returned snapshot. A changed, missing, non-scheduled or imminent post rejects the entire edit. A UTC shift updates all scheduled times atomically in storage; timer re-arming is best-effort, with the due-post cron as a backstop. Cancellation is a separate endpoint. Request body (JSON): - `ids` (array, required) - `patch` (object, required) - `dryRun` (boolean, required) - `snapshot` (string): Required for apply; copy from the preview response. ## GET /v1/idempotency/{key} Recover a creation receipt without replaying a write. Account-scoped, read-only recovery by Idempotency-Key. originalResponse is the immutable create response; post is the current canonical resource, including approval outcomes. Cancellation does not release the key. A pending receipt or missing receipt does not prove an uncertain provider operation is safe to repeat. Parameters: - `key` (path, required) ## POST /v1/posts/bulk/cancel Preview or cancel a bounded scheduled batch. Supply 1 to 200 unique IDs, one profile, and an inclusive timezone-qualified scheduled-time window. dryRun returns a snapshot of all stored post fields and the selection scope. Send it on apply to reject any intervening change, including media/options edits. Cancellation is all-or-none, scheduled-only and never retracts published content. Legacy calls without a snapshot retain execution-time compare-and-swap protection only. Request body (JSON): - `ids` (array, required): Exact IDs of scheduled posts to preview or cancel. - `profile` (string, required): Every target of every selected post must belong to this profile. - `from` (string, required): Inclusive earliest scheduled time, with Z or an explicit offset. - `to` (string, required): Inclusive latest scheduled time, with Z or an explicit offset. - `dryRun` (boolean): Preview the selection without cancelling anything. - `snapshot` (string): Full-post and selection snapshot returned by preview. Apply rejects changed content or scope with HTTP 409. ## POST /v1/posts/{id}/cancel Cancel only a scheduled post, with optional preview fencing. Never retracts a published post. Preview with dryRun=true, then apply with the returned snapshot to fence all stored fields. Execution uses a full-post compare-and-swap even when a snapshot is omitted. Parameters: - `id` (path, required) Request body (JSON): - `dryRun` (boolean) - `snapshot` (string) ## GET /v1/posts/{id}/deletion Preview cancellation or published-post removal. No side effects. Returns the action for the current state, destination-specific support and a full-post snapshot. Removing social posts never deletes uploads or refunds spent publishing credits. Instagram and TikTok posts must be removed in their apps. Pending publication cannot be retracted until its outcome is known. Approval requests must be rejected, not silently erased. Parameters: - `id` (path, required) Returns: `PostDeletionPreview`. ## GET /v1/posts/{id} Get post. Get a single post by id Parameters: - `id` (path, required) Returns: `Post`. ## PATCH /v1/posts/{id} Edit a scheduled post or a draft. Edit a post that has not gone out. For a scheduled post, `accounts` replaces the exact destination list at least 30 seconds before publishing. Include `expectedAccounts` from your last GET so a stale edit cannot overwrite a newer destination change. Existing matching targets are preserved and new destinations inherit the publish time after preflight validation. Guardrails apply; approval-required agents must ask the owner. `targetAccounts` repairs a disconnected target by exact replacement. A draft may remain incomplete until published. Parameters: - `id` (path, required) Request body (JSON): - `text` (string) - `campaign` (string): Optional campaign slug. Set null to clear. - `assetMetadata` (object): Optional caller-defined asset facts. Set null to clear. - `textOverrides` (object) - `scheduledAt` (string) - `timezone` (string): IANA timezone (e.g. Europe/London). - `media` (array) - `mediaAlt` (array) - `mediaOverrides` (object) - `mediaAltOverrides` (object) - `platformOptions` (object): Merged per platform, so a TikTok title patch does not wipe other TikTok fields. - `accounts` (array): Exact final acc_ destination list. Scheduled posts require at least one connected account; new destinations inherit the same publish time. - `expectedAccounts` (array): Required with accounts on scheduled posts. The current target account ids from GET /v1/posts/{id}, in order. Rejects stale edits. - `targetAccounts` (object): Scheduled posts only. Map a disconnected target's old acc_ id to the exact replacement connected acc_ id. - `thread` (array): Drafts only. New follow-on post bodies. An empty array removes the thread. - `firstComment` (string): Drafts only. New first comment. An empty string removes it. - `firstCommentOverrides` (object): Drafts only. New per-platform first-comment overrides. Returns: `Post`. ## DELETE /v1/posts/{id} Cancel a scheduled post, discard a draft, or retract a live one. Cancel a future schedule, discard a personal draft, clear a failed record, or request removal of published targets where supported. Approval requests must be rejected separately. Queued/processing publication returns 409. GET /v1/posts/{id}/deletion previews the consequences; send its snapshot to reject intervening changes. Unsupported destinations stay online and keep the record visible. Accepted removals are retained on remaining targets and not repeated on retry. Uploads are kept; spent publishing credits are not refunded. Instagram and TikTok require removal in their apps. Parameters: - `id` (path, required) Request body (JSON): - `snapshot` (string): Full-post snapshot returned by the deletion preview. ## POST /v1/posts/{id}/refresh Confirm an async post's current state. Ask an asynchronous publishing platform for the current state now. Useful after a post is accepted in processing state. A provider-confirmed post is promoted to published immediately; failed transitions and credit refunds remain in the background recovery path. Parameters: - `id` (path, required) Returns: `Post`. ## POST /v1/posts/{id}/publish Publish a saved draft. Publish or schedule in place with the original ID and immutable sourceDraftId. Content, campaign, assetMetadata and platform options are preserved. Supply expectedSnapshot from GET to fence every stored field. Identical completed approvals return the existing result without reposting; a claimed or uncertain operation returns 409. Approval-required agents cannot approve themselves. Personal drafts remain editable after every destination fails; approved submissions retain a failed outcome for reconciliation. Parameters: - `id` (path, required) Request body (JSON): - `scheduledAt` (object): Publish at this time instead. Omit to keep whatever time the draft was carrying (or send now if it had none). Send null to clear the draft's time and publish immediately. - `timezone` (string): IANA timezone (e.g. Europe/London) for a naive scheduledAt. - `accounts` (array): Publish to these accounts instead of the draft's own destinations. - `expectedSnapshot` (string): Full-state snapshot from GET. Any intervening edit rejects approval with 409. - `profile` (string): Publish to every account under this profile instead of the draft's own destinations. - `platforms` (array): Optional filter applied to the resolved destinations. Returns: `Post`. ## GET /v1/posts/{id}/analytics Get post analytics. Live per-post analytics by destination. measuredAt says when the read completed. targets[].reports identifies metrics the network exposes; targets[].metrics is null when insights are unavailable, not zero. Parameters: - `id` (path, required) ## POST /v1/analytics/refresh Request background analytics refresh. Queue a workspace analytics refresh and return immediately. Requests are deduplicated while a job is active, with a five-minute start cooldown. Optional ifStale=true reuses a collection updated within fifteen minutes; the dashboard uses it once on entry without restarting collection on filter changes. Recent eligible content is checked first through bounded continuations on a separate queue. During reliability incidents, collection can be paused: new requests return 503, analytics_refresh_unavailable and Retry-After: 60. Stored reports remain available. The same explicit refresh operation is available through get_analytics with refresh=true in MCP. Request body (JSON): - `ifStale` (boolean): Reuse a recently updated collection instead of starting another job. Automatic-entry freshness interval is fifteen minutes; provider eligibility and cooldowns still apply. ## GET /v1/analytics/refresh Get analytics refresh progress. Read the most recent workspace refresh state. Null means no refresh has been requested. collectionPaused is true while collection is suspended for service reliability; an existing queued or running job is not necessarily advancing. Complete means eligible collection finished, not that every provider returned every metric. errors counts unavailable readings; prior valid readings are preserved. MCP get_analytics includes this status in its response. Completion invalidates private saved reports immediately; measuredAt remains the metric collection timestamp. ## GET /v1/analytics Get cross-platform analytics. Read private saved reports for all collected destinations, without a 50-post sample or live provider calls. Cold or invalidated reports use a globally limited read-only builder; busy admission returns 429 with Retry-After: 60 and database failures return 503. Same-filter saved results can be retained with snapshot.stale=true. measuredAt is collection time and snapshot.generatedAt is preparation time. Production reads check the durable generation directly, without an additional edge-cache delay after collection completes. Period mode reports observed counter growth, including older content; Content mode reports lifetime performance of content published in the range. Totals, current, chart and breakdown share mode and filters. Rankings contain up to twenty destinations. POST /v1/analytics/refresh queues collection; GET that route reports progress. GET /v1/analytics/methodology defines reconciliation and freshness. Parameters: - `period` (query) - `profile` (query): Profile username. Omit for the whole workspace. - `mode` (query): Observed period activity, or lifetime performance of content published in the range. - `sort` (query): Rank the complete matching destination population before limiting to 20. Unavailable readings sort last. Rates require a positive reported audience count. - `channel` (query): Connected social account ID. Shared posts contribute only matching destinations. - `network` (query): Optional platform name. - `from` (query): Inclusive first UTC date, YYYY-MM-DD. - `to` (query): Inclusive last UTC date, YYYY-MM-DD. Maximum range 365 days. - `agent` (query): Stable creator client ID, not display name. Unknown historical attribution is not inferred. - `origin` (query): Recorded interface, separate from creator identity. Account-wide supplements are omitted for actor/interface filters. ## GET /v1/analytics/methodology Read analytics methodology. Machine-readable definitions for normalised metrics, coverage, freshness, availability, comparisons, and per-network reporting. Read this before comparing metrics across networks or explaining a zero. ## GET /v1/media/policy Read uploaded-media retention policy. Applies to PostLake uploads on every plan while the account remains open, not external URLs. Minimum dates are not expiry dates. Source URLs are public; cancelling a post does not delete media. This is publishing storage, not a backup service. See https://postlake.dev/terms#media-retention. ## DELETE /v1/media/{id} Erase an uploaded file and its previews. Owner-scoped, irreversible erasure. First GET /v1/media/{id}/deletion, then send its snapshot. Blocks drafts, approval requests, queued/scheduled/processing targets, concurrent publication and more than 200 references. Replace/remove their media or cancel/reject those posts first. Terminal social posts remain online, but PostLake previews disappear. No publishing-credit refund. Storage failures retain a retryable deletion receipt. This overrides the minimum retention only at the owner's instruction. Does not erase external URLs or copies held independently by social networks. Parameters: - `id` (path, required) Request body (JSON): - `snapshot` (string, required) ## GET /v1/media/{id} Read an uploaded asset and its retention terms. Parameters: - `id` (path, required) Returns: `MediaAsset`. ## GET /v1/media/{id}/deletion Preview upload erasure and affected posts. No side effects. Up to 200 affected posts, including draft/options/cover/thumbnail references; truncated=true requires support-assisted erasure. canDelete is a preview, not a lock: apply rechecks references and in-flight publication. state can be active, deleting or deleted. No external social post is deleted. Parameters: - `id` (path, required) Returns: `MediaDeletionPreview`. ## PUT /media/upload Complete an MCP local-file upload. Upload raw file bytes to the short-lived target returned by the MCP upload_media tool when called without a URL. Send the returned upload token as `Authorization: Bearer …` and the exact returned Content-Type. This endpoint does not accept a PostLake API key and is not called until upload_media has prepared a media id and target. Returns: `MediaAsset`. ## GET /v1/media List uploaded files. Owner-scoped cursor pagination in descending media-ID order, not upload-time order. Deleted files are excluded; deleting files remain visible for retry. No post-history scan. Parameters: - `before` (query): next from the previous response - `limit` (query) ## POST /v1/media Upload media. Upload media, either as raw binary with Content-Type set, or as multipart/form-data with a file part. For images the response includes the pixel dimensions, which are checked against each network's limits at publish time. Returns: `MediaAsset`. ## POST /v1/media/batch Upload or prepare several media items. Carousel helper. JSON `{ items: [{ contentType, sizeBytes }] }` returns signed five-minute PUT targets (same as MCP local upload). Multipart with multiple file parts uploads the bytes and returns MediaAsset records. Max 12 items. Request body (JSON): - `items` (array, required) ## POST /v1/webhooks Register webhook. Register a webhook endpoint Request body (JSON): - `url` (string, required) - `events` (array) Returns: `WebhookEndpoint`. ## GET /v1/webhooks List webhooks. List webhook endpoints ## DELETE /v1/webhooks/{id} Delete webhook. Delete a webhook endpoint Parameters: - `id` (path, required) ## POST /v1/webhooks/{id}/test Send webhook test event. Sends one signed webhook.test event to this endpoint with a ten-second timeout. The receiver must return 2xx directly; redirects are not followed. A failed attempt enters the durable retry queue. HTTP success confirms receipt, not application processing. Parameters: - `id` (path, required) ## GET /v1/webhooks/deliveries List pending and dead-lettered deliveries. Parameters: - `status` (query) ## POST /v1/webhooks/deliveries/{id}/replay Replay a webhook delivery. Attempts the durably queued original event again without changing its event id, publishing content or charging credits. Generates a fresh signature. An active delivery lease returns 409 instead of starting an overlapping attempt. Post lifecycle notifications are persisted atomically with saved post state. Parameters: - `id` (path, required) ## GET /v1/changelog Read the public PostLake changelog. Returns the same versioned release records shown at postlake.dev/changelog. No API key is required. Parameters: - `limit` (query) ## POST /v1/credentials Save credentials. Save BYO platform app credentials for supported networks other than X. X always uses PostLake's shared app; customer-owned X app credentials are rejected. Request body (JSON): - `platform` (object, required) - `clientId` (string, required) - `clientSecret` (string, required) ## GET /v1/credentials List credentials. List BYO credentials ## DELETE /v1/credentials/{platform} Delete credentials. Delete BYO credentials for a platform Parameters: - `platform` (path, required) ## POST /v1/connect-link Mint connect link. Mint a short-lived link to the hosted connect/manage page. Pass profile to scope the page to one tenant. Pass returnUrl to send the browser back to your app with connect_status after OAuth (the Upload-Post / Ayrshare JWT pattern). Pass platforms to show only those networks. Do not give end users POST /v1/app-link; that mints a 12-hour dashboard session. Request body (JSON): - `profile` (string): Scope the hosted page and the new connection to this profile username. Required for tenant-safe handoffs; workspace API keys still access all profiles. - `returnUrl` (string): https callback URL without credentials or a fragment. http is allowed only for localhost. Outcome query parameters are unsigned display hints. Bind an expiring single-use state to your signed-in customer on your backend, then read their stored profile's accounts. Never authorize a tenant using the returned profile parameter. - `platforms` (array): Optional non-empty allow-list enforced on connection and disconnection routes, not just page visibility. Omit for all available networks. ## POST /v1/app-link Mint app link. Mint a session link to the full app dashboard ## POST /v1/mcp MCP Endpoint. Model Context Protocol JSON-RPC endpoint. PREFER `POST /mcp`: it is the canonical URL and now accepts either an API key or an OAuth access token, so there is no longer a reason to choose between two paths. This one is kept working for configs already pointing at it. The same tool registry covers publishing, scheduling, validation, media, notifications, comments, direct messages, engagement, profile and channel management, and analytics. See https://docs.postlake.dev/mcp for the full toolset and authentication options. Request body (JSON): - `jsonrpc` (string, required) - `method` (string, required) - `params` (object) - `id` (object) ## GET /openapi.json OpenAPI Specification. Returns this OpenAPI specification