API Documentation
The Ampost REST API lets you publish, schedule, and manage social media content across 11 platforms from a single integration. All requests use Bearer token authentication and return clean JSON.
AI / LLM ready. This documentation is also available as a machine-readable /llms.txt file, designed for LLMs and AI agents that want to understand the Ampost API.
Overview
Ampost provides a unified social media API. Instead of integrating each platform individually, you connect to Ampost once and get access to instagram, facebook, linkedin, tiktok, threads, twitter, discord, telegram, bluesky, youtube, pinterest through a single REST API.
Every endpoint is also an MCP tool with the same input, output and error codes, and both are generated from one set of schemas. The machine-readable contract is the OpenAPI 3.1 document at /openapi.json. Lists return { data, pagination } and accept limit (1-100, default 20) and offset; single resources are returned as plain objects.
Base URL
api.ampost.io
Protocol
HTTPS only
Content Type
application/json
Auth
Bearer token
Rate Limit
Tier-based
Quickstart
Make your first API call in under a minute:
Public REST and MCP traffic is hosted on https://api.ampost.io. The dashboard, OAuth callbacks, and billing/session flows stay on https://ampost.io, where the dashboard calls the same endpoints with its sign-in session. Integrations should use the API host.
Get your API key
Sign up at ampost.io, open Setup → API keys in the dashboard, and create a new key. Save the raw key — it is shown only once.
Connect a social account
Create a browser-assisted connection through POST /api/accounts/connect or use the dashboard. New accounts land in your default platform set, which a post uses when its target has no platformSetId. Call GET /api/platforms to see what each platform accepts.
Create a post
This creates a draft. Add "publishNow": true to publish it immediately.
curl -X POST https://api.ampost.io/api/posts \
-H "Authorization: Bearer amp_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"targets": [
{
"platforms": [
"threads"
]
}
],
"text": "Hello from the Ampost API!"
}'Check the result
Each account's outcome is in platformPosts of GET /api/posts/{postId}; retry a failed one with POST /api/posts/{postId}/retry. To be told instead of polling, add a webhook.
Authentication
All API requests require a valid API key passed as a Bearer token in the Authorization header. API keys are created and managed through the Ampost dashboard. Dashboard-only routes such as /api/keys, connected apps and billing do not accept Bearer API keys. Connecting, reconnecting and disconnecting accounts work with a Bearer key through the account endpoints below; social consent still happens in the user's browser.
Key format
API keys use the prefix amp_live_ followed by a base64url-encoded random secret. The raw key is shown only once at creation time.
Scopes
Each key has scopes, and each endpoint and MCP tool needs one of them (shown on the endpoint). A request with a key that lacks the scope gets 403 INSUFFICIENT_SCOPE with details.requiredScope, and MCP tools/list shows only the tools the key may call. When you create a key, choose Full access (every scope), Read only (posts:read, accounts:read, analytics:read) or your own scopes.
| Scope | Allows |
|---|---|
posts:read | List and read posts and posting queues, and read platform rules |
posts:write | Create, change, publish, retry, delete and import posts; edit posting queues |
accounts:read | List and read accounts, platform sets and connection requests |
accounts:write | Connect and disconnect accounts; create, rename and delete platform sets |
media:write | Upload media for posts |
analytics:read | Read post metrics, account overviews and follower history |
webhooks:write | Create, change, test and delete webhooks, read their delivery logs, and redeliver events |
Platform-set restriction
A key can be limited to some platform sets. It then sees only those sets, their accounts, and posts with an account in them (showing only those accounts' results); an id from another set answers 404. Creating a post, connecting an account or changing a set outside the restriction, including the default set when no set is named, answers 403 PLATFORM_SET_NOT_ALLOWED. A restricted key cannot create platform sets.
Posts
A post is one piece of content for one or more platforms. The top-level text, linkUrl and media apply to every platform; options.<platform> changes them for one platform. Without scheduledFor a new post is a draft. Set publishNow to publish it in the same call, or call the publish endpoint later.
- Media: each item in
mediais a publichttpsURL with a requiredtype(imageorvideo) and optional metadata such aswidth,height,durationSecondsandaltText. URLs that usehttp, carry credentials, or point at localhost, private, link-local or internal hosts fail withVALIDATION_FAILED. If Ampost cannot download a URL (also after up to 3 redirects, each checked the same way), the error isMEDIA_FETCH_FAILED. To upload files instead, see Media upload. - Options: every platform accepts
text; Instagram and Facebook addplacement, LinkedIn and TikTok add their own settings. Unknown keys are rejected. The full schema per platform is in GET /api/platforms. - Targets:
targetslists platform sets, each with itsplatforms; each platform goes to the set's account for it. OmitplatformSetIdto use your default set. A set holds one account per platform, so to post to several accounts of the same platform, list each account's set in one post.options.<platform>applies to every account of that platform. - Results:
platformPostshas one entry per account with its platform, set, status, published URL and error. A post whose accounts partly failed ispartial; retry the failed ones, or passaccountIdsto retry only some. - Automatic retries: when a platform rate-limits a publish, times out or has a server error (
error.retryabletrue), Ampost tries again by itself, up to 5 attempts, waiting longer each time (up to 1, 2, 4 and 8 minutes, with jitter) or until the platform's own reset time when it sends one. While it waits the account stayspendingwith the lasterror,attemptsandnextAttemptAt, and the post isposting. Only after the last attempt does the account turnfailedand thepost.failedorpost.partialwebhook fire. Errors that will not pass on their own (expired auth, invalid media) fail at once. - No duplicates: when a publish times out after the platform got it, Ampost first looks for the post on the account and publishes again only if it is not there. Where the platform has no way to look (Facebook stories and reels, LinkedIn, TikTok, X, Telegram and Discord), the account fails with
PUBLISH_OUTCOME_UNKNOWN: check the account, and retry only if the post is missing. - Platform rate limits: Ampost spreads publishes per account and per platform app so that a burst of posts due at once stays inside each platform's limits (for example Instagram's 100 posts a day, TikTok's uploads per day, LinkedIn's daily caps and X's posting window). A post held back this way waits with
nextAttemptAtand is never marked failed for it; its account stayspending. - Status: ampost.io/status shows whether publishing works on each platform right now, from real publish outcomes, and explains these guarantees.
- First comment:
firstCommentis posted on each new post right after it publishes: a comment on Instagram, Facebook, LinkedIn and YouTube, a reply on X, Threads and Bluesky. TikTok, Telegram, Discord and Pinterest take none. Set it per platform withoptions.<platform>.firstComment(""turns it off there). Each account's result is inplatformPosts[].firstComment; a failed comment leaves the post published, and retrying that account posts only the comment again. - Unpublish: the unpublish endpoint deletes a published post from the platforms. Each account it left turns
deleted; one the platform refused stayspublishedwith anerror. Instagram and TikTok have no delete API (UNPUBLISH_NOT_SUPPORTED), and Telegram bots cannot delete messages older than 48 hours. - Edit published text: the edit-published endpoint replaces the text of a published post on Facebook, LinkedIn, Telegram, Discord, YouTube and Pinterest (the video or Pin description); media and links stay. Each edited account keeps the new text in
platformPosts[].text; one the platform refused keeps anerror. Instagram, Threads, TikTok, Bluesky and X answerEDIT_NOT_SUPPORTED. Drafts and scheduled posts change with the update endpoint instead. - CSV import:
POST /api/posts/bulkcreates one post per CSV row, with the same checks asPOST /api/posts. SenddryRunfirst: every row is checked and nothing is created. The response lists each row's result and errors (with the column); a bad row never stops the others. Send anIdempotency-Keywith the import so a retry creates nothing twice. The CSV export of the dashboard uses the same columns, so an exported file imports again. - Source:
sourceis where the post was created:api,mcpordashboard. Ampost sets it from the request; posts created before it was recorded havenull.
The same LinkedIn post on two accounts, in two platform sets:
{
"targets": [
{
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"platforms": [
"linkedin"
]
},
{
"platformSetId": "9a4f2c7e-1d3b-4a6e-8c5f-2b7d9e1a3c50",
"platforms": [
"linkedin"
]
}
],
"text": "We are hiring two engineers. Apply by Friday.",
"linkUrl": "https://example.com/jobs",
"publishNow": true
}{
"id": "7f1c2a9e-4b1d-4c55-9a0e-2f6d8b3c1a10",
"status": "published",
"targets": [
{
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"platforms": [
"linkedin"
]
},
{
"platformSetId": "9a4f2c7e-1d3b-4a6e-8c5f-2b7d9e1a3c50",
"platforms": [
"linkedin"
]
}
],
"platforms": [
"linkedin"
],
"text": "We are hiring two engineers. Apply by Friday.",
"firstComment": null,
"linkUrl": "https://example.com/jobs",
"media": [],
"options": {},
"scheduledFor": null,
"queuePlatformSetId": null,
"source": "api",
"metadata": null,
"platformPosts": [
{
"platform": "linkedin",
"accountId": "5e9a1c3d-7b2f-4d8e-a6c1-3f9b2e7d4a60",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "published",
"platformPostId": "urn:li:share:7248123456789012346",
"publishedUrl": "https://www.linkedin.com/feed/update/urn:li:share:7248123456789012346/",
"text": null,
"error": null,
"warnings": [],
"attempts": 1,
"nextAttemptAt": null,
"firstComment": null,
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
},
{
"platform": "linkedin",
"accountId": "2f8c4a6e-9b1d-4e3a-a7c5-6d2f8b4e1a90",
"platformSetId": "9a4f2c7e-1d3b-4a6e-8c5f-2b7d9e1a3c50",
"status": "published",
"platformPostId": "urn:li:share:7248123456789012347",
"publishedUrl": "https://www.linkedin.com/feed/update/urn:li:share:7248123456789012347/",
"text": null,
"error": null,
"warnings": [],
"attempts": 1,
"nextAttemptAt": null,
"firstComment": null,
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
}
],
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
}A post that fails on one platform keeps the error there:
{
"id": "7f1c2a9e-4b1d-4c55-9a0e-2f6d8b3c1a10",
"status": "failed",
"targets": [
{
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"platforms": [
"tiktok"
]
}
],
"platforms": [
"tiktok"
],
"text": "Behind the scenes #buildinpublic",
"firstComment": null,
"linkUrl": null,
"media": [
{
"url": "https://cdn.example.com/launch.mp4",
"type": "video",
"mimeType": "video/mp4",
"sizeBytes": 18200000,
"width": 1080,
"height": 1920,
"durationSeconds": 21,
"altText": null,
"thumbnailUrl": null
}
],
"options": {
"tiktok": {
"privacyLevel": "PUBLIC_TO_EVERYONE"
}
},
"scheduledFor": null,
"queuePlatformSetId": null,
"source": "mcp",
"metadata": null,
"platformPosts": [
{
"platform": "tiktok",
"accountId": "8d3b6f1e-2c4a-4e7b-9f5d-1a6c8e3b2d70",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "failed",
"platformPostId": null,
"publishedUrl": null,
"text": null,
"error": {
"code": "PRIVACY_LEVEL_UNAVAILABLE",
"message": "TikTok privacy level \"PUBLIC_TO_EVERYONE\" is not available for this creator",
"retryable": false,
"platformCode": null,
"platformSubcode": null
},
"warnings": [],
"attempts": 1,
"nextAttemptAt": null,
"firstComment": null,
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
}
],
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
}A post waiting for an automatic retry after a rate limit:
{
"id": "7f1c2a9e-4b1d-4c55-9a0e-2f6d8b3c1a10",
"status": "posting",
"targets": [
{
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"platforms": [
"instagram"
]
}
],
"platforms": [
"instagram"
],
"text": "New drop at 9am",
"firstComment": null,
"linkUrl": null,
"media": [
{
"url": "https://cdn.example.com/launch.mp4",
"type": "video",
"mimeType": "video/mp4",
"sizeBytes": 18200000,
"width": 1080,
"height": 1920,
"durationSeconds": 21,
"altText": null,
"thumbnailUrl": null
}
],
"options": {},
"scheduledFor": "2026-10-06T09:00:00.000Z",
"queuePlatformSetId": null,
"source": "api",
"metadata": null,
"platformPosts": [
{
"platform": "instagram",
"accountId": "c41d8e2f-6a3b-4f9c-b1e5-7d2a9c4e6f80",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "pending",
"platformPostId": null,
"publishedUrl": null,
"text": null,
"error": {
"code": "RATE_LIMITED",
"message": "Instagram rate limit reached: Application request limit reached",
"retryable": true,
"platformCode": 4,
"platformSubcode": null
},
"warnings": [],
"attempts": 2,
"nextAttemptAt": "2026-10-06T09:02:04.000Z",
"firstComment": null,
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
}
],
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
}After unpublish, an account that cannot delete keeps its error:
{
"id": "7f1c2a9e-4b1d-4c55-9a0e-2f6d8b3c1a10",
"status": "published",
"targets": [
{
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"platforms": [
"instagram",
"linkedin"
]
}
],
"platforms": [
"instagram",
"linkedin"
],
"text": "Launch day: Ampost publishes everywhere from one API.",
"firstComment": null,
"linkUrl": null,
"media": [
{
"url": "https://cdn.example.com/launch.mp4",
"type": "video",
"mimeType": "video/mp4",
"sizeBytes": 18200000,
"width": 1080,
"height": 1920,
"durationSeconds": 21,
"altText": null,
"thumbnailUrl": null
}
],
"options": {
"instagram": {
"placement": "reel"
},
"linkedin": {
"text": "Launch day. Here is what we built and why.",
"visibility": "PUBLIC"
}
},
"scheduledFor": null,
"queuePlatformSetId": null,
"source": "api",
"metadata": null,
"platformPosts": [
{
"platform": "instagram",
"accountId": "c41d8e2f-6a3b-4f9c-b1e5-7d2a9c4e6f80",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "published",
"platformPostId": "17895695668004550",
"publishedUrl": "https://www.instagram.com/reel/C9xYz12AbCd/",
"text": null,
"error": {
"code": "UNPUBLISH_NOT_SUPPORTED",
"message": "This platform has no API to delete a published post. Delete it in the platform's own app.",
"retryable": false,
"platformCode": null,
"platformSubcode": null
},
"warnings": [],
"attempts": 1,
"nextAttemptAt": null,
"firstComment": null,
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-07T10:00:01.000Z"
},
{
"platform": "linkedin",
"accountId": "5e9a1c3d-7b2f-4d8e-a6c1-3f9b2e7d4a60",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "deleted",
"platformPostId": "urn:li:share:7248123456789012345",
"publishedUrl": null,
"text": null,
"error": null,
"warnings": [],
"attempts": 1,
"nextAttemptAt": null,
"firstComment": null,
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-07T10:00:01.000Z"
}
],
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-07T10:00:01.000Z"
}After an edit, the edited account holds its new text:
{
"id": "7f1c2a9e-4b1d-4c55-9a0e-2f6d8b3c1a10",
"status": "published",
"targets": [
{
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"platforms": [
"instagram",
"linkedin"
]
}
],
"platforms": [
"instagram",
"linkedin"
],
"text": "Launch day: Ampost publishes everywhere from one API.",
"firstComment": null,
"linkUrl": null,
"media": [
{
"url": "https://cdn.example.com/launch.mp4",
"type": "video",
"mimeType": "video/mp4",
"sizeBytes": 18200000,
"width": 1080,
"height": 1920,
"durationSeconds": 21,
"altText": null,
"thumbnailUrl": null
}
],
"options": {
"instagram": {
"placement": "reel"
},
"linkedin": {
"text": "Launch day. Here is what we built and why.",
"visibility": "PUBLIC"
}
},
"scheduledFor": null,
"queuePlatformSetId": null,
"source": "api",
"metadata": null,
"platformPosts": [
{
"platform": "instagram",
"accountId": "c41d8e2f-6a3b-4f9c-b1e5-7d2a9c4e6f80",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "published",
"platformPostId": "17895695668004550",
"publishedUrl": "https://www.instagram.com/reel/C9xYz12AbCd/",
"text": null,
"error": {
"code": "EDIT_NOT_SUPPORTED",
"message": "This platform has no API to edit a published post. Edit it in the platform's own app.",
"retryable": false,
"platformCode": null,
"platformSubcode": null
},
"warnings": [],
"attempts": 1,
"nextAttemptAt": null,
"firstComment": null,
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-07T10:00:01.000Z"
},
{
"platform": "linkedin",
"accountId": "5e9a1c3d-7b2f-4d8e-a6c1-3f9b2e7d4a60",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "published",
"platformPostId": "urn:li:share:7248123456789012345",
"publishedUrl": "https://www.linkedin.com/feed/update/urn:li:share:7248123456789012345/",
"text": "Launch day. Here is what we built and why (now with the pricing link in the comments).",
"error": null,
"warnings": [],
"attempts": 1,
"nextAttemptAt": null,
"firstComment": null,
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-07T10:00:01.000Z"
}
],
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-07T10:00:01.000Z"
}A dry run of a CSV import with one bad row:
{
"csv": "text,platforms,scheduledFor,useQueue,media,text_threads\nLaunch day,linkedin threads,2026-10-20T09:00:00Z,,https://cdn.example.com/launch.jpg,Shorter for Threads\nBehind the scenes,instagram,,true,https://cdn.example.com/team.mp4,\nDraft for later,myspace,,,,",
"dryRun": true
}{
"dryRun": true,
"summary": {
"rows": 3,
"valid": 2,
"created": 0,
"errors": 1
},
"warnings": [],
"rows": [
{
"row": 2,
"status": "valid",
"postId": null,
"scheduledFor": "2026-10-20T09:00:00Z",
"errors": []
},
{
"row": 3,
"status": "valid",
"postId": null,
"scheduledFor": "2026-10-21T07:00:00.000Z",
"errors": []
},
{
"row": 4,
"status": "error",
"postId": null,
"scheduledFor": null,
"errors": [
{
"code": "VALIDATION_FAILED",
"message": "Invalid option: expected one of \"instagram\"|\"facebook\"|\"linkedin\"|\"tiktok\"|\"threads\"|\"twitter\"|\"discord\"|\"telegram\"|\"bluesky\"|\"youtube\"|\"pinterest\"",
"column": "platforms"
}
]
}
]
}/api/postsCreate a post for one or more platforms. `targets` lists platform sets, each with its platforms; omit `platformSetId` for your default set. List several sets to reach several accounts of the same platform in one post (each set holds one account per platform); `platformPosts` then has one entry per account. Without `scheduledFor` it is a draft; set `publishNow` to publish it right away, or `scheduledFor` to publish later. `media` items are public URLs with a required `type` (REST also accepts multipart file uploads up to 4 MB per request; for larger files or files on the user's device use media_upload_create). Use `options.<platform>` for per-platform text and settings. `firstComment` is posted as a comment (a reply on X, Threads and Bluesky) on each new post right after it publishes; a failed comment leaves the post published with `platformPosts[].firstComment.status` failed. Call platforms_list (GET /api/platforms) for each platform's limits, placements and options.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| targets | object[] | Yes | Where the post goes: one entry per platform set (max 10). A set holds one account per platform, so to reach several accounts of one platform (two LinkedIn accounts, several clients' Instagram) list their sets. The post gets one platformPosts entry per account. |
| text | string | No | |
| firstComment | string | null | No | |
| linkUrl | string | null | No | |
| media | object[] | No | |
| options | object | No | |
| scheduledFor | ISO 8601 | null | No | |
| publishNow | boolean | No | Publish right after creating. Cannot be combined with `scheduledFor`. |
| useQueue | boolean | No | Schedule the post in the next free slot of its platform set's queue (see queue_get) instead of at `scheduledFor`. Needs exactly one target and a queue with slots. Cannot be combined with `scheduledFor` or `publishNow`. |
| metadata | object | No |
{
"targets": [
{
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"platforms": [
"instagram",
"linkedin"
]
}
],
"text": "Launch day: Ampost publishes everywhere from one API.",
"firstComment": "Pricing and docs: https://ampost.io",
"media": [
{
"url": "https://cdn.example.com/launch.mp4",
"type": "video",
"mimeType": "video/mp4",
"sizeBytes": 18200000,
"width": 1080,
"height": 1920,
"durationSeconds": 21
}
],
"options": {
"instagram": {
"placement": "reel"
},
"linkedin": {
"text": "Launch day. Here is what we built and why.",
"visibility": "PUBLIC"
}
},
"publishNow": true,
"metadata": {
"campaignId": "launch-2026"
}
}{
"id": "7f1c2a9e-4b1d-4c55-9a0e-2f6d8b3c1a10",
"status": "published",
"targets": [
{
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"platforms": [
"instagram",
"linkedin"
]
}
],
"platforms": [
"instagram",
"linkedin"
],
"text": "Launch day: Ampost publishes everywhere from one API.",
"firstComment": "Pricing and docs: https://ampost.io",
"linkUrl": null,
"media": [
{
"url": "https://cdn.example.com/launch.mp4",
"type": "video",
"mimeType": "video/mp4",
"sizeBytes": 18200000,
"width": 1080,
"height": 1920,
"durationSeconds": 21,
"altText": null,
"thumbnailUrl": null
}
],
"options": {
"instagram": {
"placement": "reel"
},
"linkedin": {
"text": "Launch day. Here is what we built and why.",
"visibility": "PUBLIC"
}
},
"scheduledFor": null,
"queuePlatformSetId": null,
"source": "api",
"metadata": {
"campaignId": "launch-2026"
},
"platformPosts": [
{
"platform": "instagram",
"accountId": "c41d8e2f-6a3b-4f9c-b1e5-7d2a9c4e6f80",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "published",
"platformPostId": "17895695668004550",
"publishedUrl": "https://www.instagram.com/reel/C9xYz12AbCd/",
"text": null,
"error": null,
"warnings": [],
"attempts": 1,
"nextAttemptAt": null,
"firstComment": {
"status": "published",
"commentId": "17858893269123456",
"error": null,
"updatedAt": "2026-10-06T09:00:04.000Z"
},
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
},
{
"platform": "linkedin",
"accountId": "5e9a1c3d-7b2f-4d8e-a6c1-3f9b2e7d4a60",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "published",
"platformPostId": "urn:li:share:7248123456789012345",
"publishedUrl": "https://www.linkedin.com/feed/update/urn:li:share:7248123456789012345/",
"text": null,
"error": null,
"warnings": [],
"attempts": 1,
"nextAttemptAt": null,
"firstComment": {
"status": "failed",
"commentId": null,
"error": {
"code": "PLATFORM_ERROR",
"message": "LinkedIn comment failed: the service is temporarily unavailable",
"retryable": true,
"platformCode": null,
"platformSubcode": null
},
"updatedAt": "2026-10-06T09:00:04.000Z"
},
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
}
],
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
}/api/posts/bulkCreate many posts from a CSV file: one post per row, each checked like posts_create. Columns: `text`, `platforms` (space-separated, with an optional `platformSetId`) or `accountIds`, `scheduledFor` or `useQueue` (neither: a draft), `linkUrl`, `media` (space-separated public URLs; `.mp4`, `.m4v`, `.mov` and `.webm` are videos, the rest images) and `text_<platform>` for per-platform text. The dashboard's CSV export has the same columns, so an exported file imports again (its read-only columns are skipped). Send `dryRun: true` first: it checks every row without creating anything. A bad row does not stop the others; `rows[]` gives each row's `status` (`valid`, `created` or `error`) and `errors` with the column. Unknown columns are skipped with a warning. Plans import at most 50 (Free), 1,000 (Pro), 2,500 (Max) or 5,000 (Scale) rows per request. An import and a dry run each take one token per row from the hourly `importPosts` limit (two runs of the plan's largest file an hour, e.g. a dry run and its import); a file with more rows than are left fails with 429 `RATE_LIMITED` and creates nothing. Send an idempotency key with the import: a retry with the same key and file returns the first result and creates nothing new. REST also accepts multipart with the file in a part named `file`.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| csv | string | Yes | The CSV text: a header row, then one post per row. Columns: `text`, `platforms`, `platformSetId`, `accountIds`, `scheduledFor`, `useQueue`, `linkUrl`, `firstComment`, `media` and `text_<platform>` (per-platform text, such as `text_linkedin`). Each row needs `platforms` (with an optional `platformSetId`; default: your default set) or `accountIds`. List cells separate items with spaces. Unknown columns are skipped with a warning. |
| dryRun | boolean | No | Check every row like posts_create does without creating anything. `valid` rows would be created. |
{
"csv": "text,platforms,scheduledFor,useQueue,media,text_threads\nLaunch day,linkedin threads,2026-10-20T09:00:00Z,,https://cdn.example.com/launch.jpg,Shorter for Threads\nBehind the scenes,instagram,,true,https://cdn.example.com/team.mp4,\nDraft for later,myspace,,,,",
"dryRun": true
}{
"dryRun": true,
"summary": {
"rows": 3,
"valid": 2,
"created": 0,
"errors": 1
},
"warnings": [],
"rows": [
{
"row": 2,
"status": "valid",
"postId": null,
"scheduledFor": "2026-10-20T09:00:00Z",
"errors": []
},
{
"row": 3,
"status": "valid",
"postId": null,
"scheduledFor": "2026-10-21T07:00:00.000Z",
"errors": []
},
{
"row": 4,
"status": "error",
"postId": null,
"scheduledFor": null,
"errors": [
{
"code": "VALIDATION_FAILED",
"message": "Invalid option: expected one of \"instagram\"|\"facebook\"|\"linkedin\"|\"tiktok\"|\"threads\"|\"twitter\"|\"discord\"|\"telegram\"|\"bluesky\"|\"youtube\"|\"pinterest\"",
"column": "platforms"
}
]
}
]
}/api/postsList your posts, newest first, optionally filtered by status, platform, platform set (posts with an account in it) or text.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| status | string | No | |
| platform | string | No | |
| platformSetId | string | No | |
| search | string | No | Case-insensitive match on the post text |
| limit | integer | No | Page size, 1-100. Default 20. |
| offset | integer | No | Number of items to skip. Default 0. |
{
"data": [],
"pagination": {
"limit": 20,
"offset": 0,
"total": 0,
"hasMore": false
}
}/api/posts/:postIdGet one post with its result per account (`platformPosts`).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| postId | string | Yes | Post id |
{
"id": "7f1c2a9e-4b1d-4c55-9a0e-2f6d8b3c1a10",
"status": "published",
"targets": [
{
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"platforms": [
"instagram",
"linkedin"
]
}
],
"platforms": [
"instagram",
"linkedin"
],
"text": "Launch day: Ampost publishes everywhere from one API.",
"firstComment": "Pricing and docs: https://ampost.io",
"linkUrl": null,
"media": [
{
"url": "https://cdn.example.com/launch.mp4",
"type": "video",
"mimeType": "video/mp4",
"sizeBytes": 18200000,
"width": 1080,
"height": 1920,
"durationSeconds": 21,
"altText": null,
"thumbnailUrl": null
}
],
"options": {
"instagram": {
"placement": "reel"
},
"linkedin": {
"text": "Launch day. Here is what we built and why.",
"visibility": "PUBLIC"
}
},
"scheduledFor": null,
"queuePlatformSetId": null,
"source": "api",
"metadata": {
"campaignId": "launch-2026"
},
"platformPosts": [
{
"platform": "instagram",
"accountId": "c41d8e2f-6a3b-4f9c-b1e5-7d2a9c4e6f80",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "published",
"platformPostId": "17895695668004550",
"publishedUrl": "https://www.instagram.com/reel/C9xYz12AbCd/",
"text": null,
"error": null,
"warnings": [],
"attempts": 1,
"nextAttemptAt": null,
"firstComment": {
"status": "published",
"commentId": "17858893269123456",
"error": null,
"updatedAt": "2026-10-06T09:00:04.000Z"
},
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
},
{
"platform": "linkedin",
"accountId": "5e9a1c3d-7b2f-4d8e-a6c1-3f9b2e7d4a60",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "published",
"platformPostId": "urn:li:share:7248123456789012345",
"publishedUrl": "https://www.linkedin.com/feed/update/urn:li:share:7248123456789012345/",
"text": null,
"error": null,
"warnings": [],
"attempts": 1,
"nextAttemptAt": null,
"firstComment": {
"status": "failed",
"commentId": null,
"error": {
"code": "PLATFORM_ERROR",
"message": "LinkedIn comment failed: the service is temporarily unavailable",
"retryable": true,
"platformCode": null,
"platformSubcode": null
},
"updatedAt": "2026-10-06T09:00:04.000Z"
},
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
}
],
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
}/api/posts/:postIdChange a draft or scheduled post. Send only the fields to change; `media` and `targets` replace the whole list (accounts the post keeps keep their results). `options` is merged per platform: each `options.<platform>` you send replaces that platform's options as a whole, `null` removes them, and other platforms keep theirs. Set `scheduledFor` to null to turn a scheduled post back into a draft. To change the text of a published post, use posts_edit_published.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| postId | string | Yes | Post id |
| targets | object[] | No | Replaces all targets. Accounts the post keeps also keep their results. |
| text | string | No | |
| firstComment | string | null | No | New first comment; null removes it |
| linkUrl | string | null | No | |
| media | object[] | No | Replaces all media |
| options | object | No | |
| scheduledFor | ISO 8601 | null | No | New publish time; null turns the post back into a draft |
{
"text": "Launch day (updated): Ampost publishes everywhere from one API.",
"scheduledFor": "2026-10-09T08:00:00Z"
}{
"id": "7f1c2a9e-4b1d-4c55-9a0e-2f6d8b3c1a10",
"status": "published",
"targets": [
{
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"platforms": [
"instagram",
"linkedin"
]
}
],
"platforms": [
"instagram",
"linkedin"
],
"text": "Launch day: Ampost publishes everywhere from one API.",
"firstComment": "Pricing and docs: https://ampost.io",
"linkUrl": null,
"media": [
{
"url": "https://cdn.example.com/launch.mp4",
"type": "video",
"mimeType": "video/mp4",
"sizeBytes": 18200000,
"width": 1080,
"height": 1920,
"durationSeconds": 21,
"altText": null,
"thumbnailUrl": null
}
],
"options": {
"instagram": {
"placement": "reel"
},
"linkedin": {
"text": "Launch day. Here is what we built and why.",
"visibility": "PUBLIC"
}
},
"scheduledFor": null,
"queuePlatformSetId": null,
"source": "api",
"metadata": {
"campaignId": "launch-2026"
},
"platformPosts": [
{
"platform": "instagram",
"accountId": "c41d8e2f-6a3b-4f9c-b1e5-7d2a9c4e6f80",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "published",
"platformPostId": "17895695668004550",
"publishedUrl": "https://www.instagram.com/reel/C9xYz12AbCd/",
"text": null,
"error": null,
"warnings": [],
"attempts": 1,
"nextAttemptAt": null,
"firstComment": {
"status": "published",
"commentId": "17858893269123456",
"error": null,
"updatedAt": "2026-10-06T09:00:04.000Z"
},
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
},
{
"platform": "linkedin",
"accountId": "5e9a1c3d-7b2f-4d8e-a6c1-3f9b2e7d4a60",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "published",
"platformPostId": "urn:li:share:7248123456789012345",
"publishedUrl": "https://www.linkedin.com/feed/update/urn:li:share:7248123456789012345/",
"text": null,
"error": null,
"warnings": [],
"attempts": 1,
"nextAttemptAt": null,
"firstComment": {
"status": "failed",
"commentId": null,
"error": {
"code": "PLATFORM_ERROR",
"message": "LinkedIn comment failed: the service is temporarily unavailable",
"retryable": true,
"platformCode": null,
"platformSubcode": null
},
"updatedAt": "2026-10-06T09:00:04.000Z"
},
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
}
],
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
}/api/posts/:postId/publishPublish a draft or scheduled post to all its accounts now. Check `platformPosts` for each account's result; videos may stay `processing` for a while.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| postId | string | Yes | Post id |
{
"id": "7f1c2a9e-4b1d-4c55-9a0e-2f6d8b3c1a10",
"status": "published",
"targets": [
{
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"platforms": [
"instagram",
"linkedin"
]
}
],
"platforms": [
"instagram",
"linkedin"
],
"text": "Launch day: Ampost publishes everywhere from one API.",
"firstComment": "Pricing and docs: https://ampost.io",
"linkUrl": null,
"media": [
{
"url": "https://cdn.example.com/launch.mp4",
"type": "video",
"mimeType": "video/mp4",
"sizeBytes": 18200000,
"width": 1080,
"height": 1920,
"durationSeconds": 21,
"altText": null,
"thumbnailUrl": null
}
],
"options": {
"instagram": {
"placement": "reel"
},
"linkedin": {
"text": "Launch day. Here is what we built and why.",
"visibility": "PUBLIC"
}
},
"scheduledFor": null,
"queuePlatformSetId": null,
"source": "api",
"metadata": {
"campaignId": "launch-2026"
},
"platformPosts": [
{
"platform": "instagram",
"accountId": "c41d8e2f-6a3b-4f9c-b1e5-7d2a9c4e6f80",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "published",
"platformPostId": "17895695668004550",
"publishedUrl": "https://www.instagram.com/reel/C9xYz12AbCd/",
"text": null,
"error": null,
"warnings": [],
"attempts": 1,
"nextAttemptAt": null,
"firstComment": {
"status": "published",
"commentId": "17858893269123456",
"error": null,
"updatedAt": "2026-10-06T09:00:04.000Z"
},
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
},
{
"platform": "linkedin",
"accountId": "5e9a1c3d-7b2f-4d8e-a6c1-3f9b2e7d4a60",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "published",
"platformPostId": "urn:li:share:7248123456789012345",
"publishedUrl": "https://www.linkedin.com/feed/update/urn:li:share:7248123456789012345/",
"text": null,
"error": null,
"warnings": [],
"attempts": 1,
"nextAttemptAt": null,
"firstComment": {
"status": "failed",
"commentId": null,
"error": {
"code": "PLATFORM_ERROR",
"message": "LinkedIn comment failed: the service is temporarily unavailable",
"retryable": true,
"platformCode": null,
"platformSubcode": null
},
"updatedAt": "2026-10-06T09:00:04.000Z"
},
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
}
],
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
}/api/posts/:postId/retryPublish again to the accounts that failed, or only to the given `accountIds` (from `platformPosts[].accountId`). Ampost already retries retryable failures (rate limits, timeouts, platform errors) by itself, up to 5 attempts with backoff; an account waiting for that shows `pending` with `nextAttemptAt`. Call this for an account that is `failed`, after fixing the cause. It starts `attempts` again from 0. A published account whose first comment failed (`platformPosts[].firstComment.status` failed) posts only the comment again, without publishing the post twice or counting it again.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| postId | string | Yes | Post id |
| accountIds | string[] | No | Accounts to retry (`platformPosts[].accountId`). A published account whose `firstComment.status` is failed retries only the comment; other accounts publish again. Default: every failed account and every failed first comment. |
{
"accountIds": [
"c41d8e2f-6a3b-4f9c-b1e5-7d2a9c4e6f80"
]
}{
"id": "7f1c2a9e-4b1d-4c55-9a0e-2f6d8b3c1a10",
"status": "published",
"targets": [
{
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"platforms": [
"instagram",
"linkedin"
]
}
],
"platforms": [
"instagram",
"linkedin"
],
"text": "Launch day: Ampost publishes everywhere from one API.",
"firstComment": "Pricing and docs: https://ampost.io",
"linkUrl": null,
"media": [
{
"url": "https://cdn.example.com/launch.mp4",
"type": "video",
"mimeType": "video/mp4",
"sizeBytes": 18200000,
"width": 1080,
"height": 1920,
"durationSeconds": 21,
"altText": null,
"thumbnailUrl": null
}
],
"options": {
"instagram": {
"placement": "reel"
},
"linkedin": {
"text": "Launch day. Here is what we built and why.",
"visibility": "PUBLIC"
}
},
"scheduledFor": null,
"queuePlatformSetId": null,
"source": "api",
"metadata": {
"campaignId": "launch-2026"
},
"platformPosts": [
{
"platform": "instagram",
"accountId": "c41d8e2f-6a3b-4f9c-b1e5-7d2a9c4e6f80",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "published",
"platformPostId": "17895695668004550",
"publishedUrl": "https://www.instagram.com/reel/C9xYz12AbCd/",
"text": null,
"error": null,
"warnings": [],
"attempts": 1,
"nextAttemptAt": null,
"firstComment": {
"status": "published",
"commentId": "17858893269123456",
"error": null,
"updatedAt": "2026-10-06T09:00:04.000Z"
},
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
},
{
"platform": "linkedin",
"accountId": "5e9a1c3d-7b2f-4d8e-a6c1-3f9b2e7d4a60",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "published",
"platformPostId": "urn:li:share:7248123456789012345",
"publishedUrl": "https://www.linkedin.com/feed/update/urn:li:share:7248123456789012345/",
"text": null,
"error": null,
"warnings": [],
"attempts": 1,
"nextAttemptAt": null,
"firstComment": {
"status": "failed",
"commentId": null,
"error": {
"code": "PLATFORM_ERROR",
"message": "LinkedIn comment failed: the service is temporarily unavailable",
"retryable": true,
"platformCode": null,
"platformSubcode": null
},
"updatedAt": "2026-10-06T09:00:04.000Z"
},
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
}
],
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
}/api/posts/:postId/unpublishTake a published post down from the platforms: every account where it is published, or only the given `accountIds` (from `platformPosts[].accountId`). Each account's result is in `platformPosts`: `deleted`, or still `published` with `error` saying why (`UNPUBLISH_NOT_SUPPORTED` on Instagram and TikTok, which have no delete API; the platform's own refusal otherwise, such as Telegram's 48-hour limit for bots). The post becomes `deleted` once no account has it; it stays readable in Ampost. Call platforms_list for `unpublish.supported` per platform. Deleting from X costs X units like a post.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| postId | string | Yes | Post id |
| accountIds | string[] | No | Accounts to delete the post from (`platformPosts[].accountId`). Default: every account where it is published. |
{
"accountIds": [
"c41d8e2f-6a3b-4f9c-b1e5-7d2a9c4e6f80",
"5e9a1c3d-7b2f-4d8e-a6c1-3f9b2e7d4a60"
]
}{
"id": "7f1c2a9e-4b1d-4c55-9a0e-2f6d8b3c1a10",
"status": "published",
"targets": [
{
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"platforms": [
"instagram",
"linkedin"
]
}
],
"platforms": [
"instagram",
"linkedin"
],
"text": "Launch day: Ampost publishes everywhere from one API.",
"firstComment": null,
"linkUrl": null,
"media": [
{
"url": "https://cdn.example.com/launch.mp4",
"type": "video",
"mimeType": "video/mp4",
"sizeBytes": 18200000,
"width": 1080,
"height": 1920,
"durationSeconds": 21,
"altText": null,
"thumbnailUrl": null
}
],
"options": {
"instagram": {
"placement": "reel"
},
"linkedin": {
"text": "Launch day. Here is what we built and why.",
"visibility": "PUBLIC"
}
},
"scheduledFor": null,
"queuePlatformSetId": null,
"source": "api",
"metadata": null,
"platformPosts": [
{
"platform": "instagram",
"accountId": "c41d8e2f-6a3b-4f9c-b1e5-7d2a9c4e6f80",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "published",
"platformPostId": "17895695668004550",
"publishedUrl": "https://www.instagram.com/reel/C9xYz12AbCd/",
"text": null,
"error": {
"code": "UNPUBLISH_NOT_SUPPORTED",
"message": "This platform has no API to delete a published post. Delete it in the platform's own app.",
"retryable": false,
"platformCode": null,
"platformSubcode": null
},
"warnings": [],
"attempts": 1,
"nextAttemptAt": null,
"firstComment": null,
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-07T10:00:01.000Z"
},
{
"platform": "linkedin",
"accountId": "5e9a1c3d-7b2f-4d8e-a6c1-3f9b2e7d4a60",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "deleted",
"platformPostId": "urn:li:share:7248123456789012345",
"publishedUrl": null,
"text": null,
"error": null,
"warnings": [],
"attempts": 1,
"nextAttemptAt": null,
"firstComment": null,
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-07T10:00:01.000Z"
}
],
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-07T10:00:01.000Z"
}/api/posts/:postId/edit-publishedReplace the text of a published post on the platforms: every account where it is published, or only the given `accountIds` (from `platformPosts[].accountId`). Text only; media and links stay. Each account's result is in `platformPosts`: an edited account's `text` holds the new text, and an account the platform refused keeps its text with `error` (`EDIT_NOT_SUPPORTED` on Instagram, Threads, TikTok, Bluesky and X, and on Facebook stories; the platform's own refusal otherwise). Facebook edits only posts Ampost published; a Telegram caption under media stays limited to 1024 characters. YouTube and Pinterest replace the video or Pin description. Call platforms_list for `editPublished.supported` per platform. To change a draft or scheduled post, use posts_update.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| postId | string | Yes | Post id |
| text | string | Yes | The new text. It replaces the text only; media and links stay. Each platform's `text.maxLength` applies. |
| accountIds | string[] | No | Accounts to edit the post on (`platformPosts[].accountId`). Default: every account where it is published. |
{
"text": "Launch day. Here is what we built and why (now with the pricing link in the comments).",
"accountIds": [
"c41d8e2f-6a3b-4f9c-b1e5-7d2a9c4e6f80",
"5e9a1c3d-7b2f-4d8e-a6c1-3f9b2e7d4a60"
]
}{
"id": "7f1c2a9e-4b1d-4c55-9a0e-2f6d8b3c1a10",
"status": "published",
"targets": [
{
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"platforms": [
"instagram",
"linkedin"
]
}
],
"platforms": [
"instagram",
"linkedin"
],
"text": "Launch day: Ampost publishes everywhere from one API.",
"firstComment": null,
"linkUrl": null,
"media": [
{
"url": "https://cdn.example.com/launch.mp4",
"type": "video",
"mimeType": "video/mp4",
"sizeBytes": 18200000,
"width": 1080,
"height": 1920,
"durationSeconds": 21,
"altText": null,
"thumbnailUrl": null
}
],
"options": {
"instagram": {
"placement": "reel"
},
"linkedin": {
"text": "Launch day. Here is what we built and why.",
"visibility": "PUBLIC"
}
},
"scheduledFor": null,
"queuePlatformSetId": null,
"source": "api",
"metadata": null,
"platformPosts": [
{
"platform": "instagram",
"accountId": "c41d8e2f-6a3b-4f9c-b1e5-7d2a9c4e6f80",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "published",
"platformPostId": "17895695668004550",
"publishedUrl": "https://www.instagram.com/reel/C9xYz12AbCd/",
"text": null,
"error": {
"code": "EDIT_NOT_SUPPORTED",
"message": "This platform has no API to edit a published post. Edit it in the platform's own app.",
"retryable": false,
"platformCode": null,
"platformSubcode": null
},
"warnings": [],
"attempts": 1,
"nextAttemptAt": null,
"firstComment": null,
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-07T10:00:01.000Z"
},
{
"platform": "linkedin",
"accountId": "5e9a1c3d-7b2f-4d8e-a6c1-3f9b2e7d4a60",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "published",
"platformPostId": "urn:li:share:7248123456789012345",
"publishedUrl": "https://www.linkedin.com/feed/update/urn:li:share:7248123456789012345/",
"text": "Launch day. Here is what we built and why (now with the pricing link in the comments).",
"error": null,
"warnings": [],
"attempts": 1,
"nextAttemptAt": null,
"firstComment": null,
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-07T10:00:01.000Z"
}
],
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-07T10:00:01.000Z"
}/api/posts/:postIdDiscard a post that has not been published, including a scheduled one. The post stays readable with status `discarded`, and its media in Ampost storage is deleted. To take a published post down from the platforms, use posts_unpublish.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| postId | string | Yes | Post id |
{
"id": "7f1c2a9e-4b1d-4c55-9a0e-2f6d8b3c1a10",
"status": "published",
"targets": [
{
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"platforms": [
"instagram",
"linkedin"
]
}
],
"platforms": [
"instagram",
"linkedin"
],
"text": "Launch day: Ampost publishes everywhere from one API.",
"firstComment": "Pricing and docs: https://ampost.io",
"linkUrl": null,
"media": [
{
"url": "https://cdn.example.com/launch.mp4",
"type": "video",
"mimeType": "video/mp4",
"sizeBytes": 18200000,
"width": 1080,
"height": 1920,
"durationSeconds": 21,
"altText": null,
"thumbnailUrl": null
}
],
"options": {
"instagram": {
"placement": "reel"
},
"linkedin": {
"text": "Launch day. Here is what we built and why.",
"visibility": "PUBLIC"
}
},
"scheduledFor": null,
"queuePlatformSetId": null,
"source": "api",
"metadata": {
"campaignId": "launch-2026"
},
"platformPosts": [
{
"platform": "instagram",
"accountId": "c41d8e2f-6a3b-4f9c-b1e5-7d2a9c4e6f80",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "published",
"platformPostId": "17895695668004550",
"publishedUrl": "https://www.instagram.com/reel/C9xYz12AbCd/",
"text": null,
"error": null,
"warnings": [],
"attempts": 1,
"nextAttemptAt": null,
"firstComment": {
"status": "published",
"commentId": "17858893269123456",
"error": null,
"updatedAt": "2026-10-06T09:00:04.000Z"
},
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
},
{
"platform": "linkedin",
"accountId": "5e9a1c3d-7b2f-4d8e-a6c1-3f9b2e7d4a60",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "published",
"platformPostId": "urn:li:share:7248123456789012345",
"publishedUrl": "https://www.linkedin.com/feed/update/urn:li:share:7248123456789012345/",
"text": null,
"error": null,
"warnings": [],
"attempts": 1,
"nextAttemptAt": null,
"firstComment": {
"status": "failed",
"commentId": null,
"error": {
"code": "PLATFORM_ERROR",
"message": "LinkedIn comment failed: the service is temporarily unavailable",
"retryable": true,
"platformCode": null,
"platformSubcode": null
},
"updatedAt": "2026-10-06T09:00:04.000Z"
},
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
}
],
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:04.000Z"
}MCP Server
Ampost provides a Model Context Protocol (MCP) server for AI agents and LLMs. The MCP documentation lives on its own page with endpoint details, available tools, and configuration examples.
Platforms
Ampost supports 11 platforms. Their differences are data: GET /api/platforms (MCP: platforms_list) returns each platform's text limit, link and scheduling support, media rules (sizes, types, pixel limits) and required media metadata, placements by media shape, and the JSON Schema of its options. Read it instead of hardcoding platform rules. The table below is built from the same data.
| Platform | Text | Media | Max items | Link | Options |
|---|---|---|---|---|---|
Instagram instagram | 2200 | required: image, video; video ≤ 100 MB | 10 | ignored | text, firstComment, placement |
Facebook facebook | 63206 | optional: image, video; video (reel) 9:16 | 10 | yes | text, firstComment, placement |
LinkedIn linkedin | 3000 | optional: image, video; video ≤ 500 MB | 20 | yes | text, firstComment, visibility, isReshareDisabledByAuthor, articleTitle, articleDescription, videoTitle |
TikTok tiktok | 2200 | required: image, video; video ≤ 4 GB; image ≤ 1080x1080 px; video ≥ 360x360, ≤ 4096x4096 px | 35 | ignored | text, privacyLevel, disableComment, disableDuet, disableStitch, videoCoverTimestampMs, photoCoverIndex, autoAddMusic, brandContentToggle, brandOrganicToggle, isAigc |
Threads threads | 500 | optional: image, video | 20 | yes | text, firstComment |
X twitter | 280 | optional: image, video; video ≤ 512 MB | 4 | yes | text, firstComment |
Discord discord | 2000 | optional: image, video; video ≤ 10 MB | 10 | yes | text |
Telegram telegram | 4096 | optional: image, video; video ≤ 50 MB | 10 | yes | text |
Bluesky bluesky | 300 | optional: image | 4 | yes | text, firstComment |
YouTube youtube | 5000 | required: video | 1 | yes | text, firstComment, title, privacyStatus, madeForKids, tags, categoryId, playlistId, notifySubscribers, containsSyntheticMedia |
Pinterest pinterest | 800 | required: image, video | 5 | yes | text, boardId, title |
/api/platformsList every supported platform with its rules: text length, link support, scheduling, unpublish (delete a published post), editPublished (edit a published post's text), firstComment (comment posted after publish, and its length), media types, counts, sizes and required metadata, placements by media shape, and the JSON Schema of its `options`. Read this instead of hardcoding platform differences.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| limit | integer | No | Page size, 1-100. Default 20. |
| offset | integer | No | Number of items to skip. Default 0. |
{
"platform": "instagram",
"name": "Instagram",
"text": {
"maxLength": 2200
},
"link": {
"supported": false
},
"scheduling": {
"supported": true
},
"unpublish": {
"supported": false
},
"editPublished": {
"supported": false
},
"firstComment": {
"supported": true,
"maxLength": 2200
},
"media": {
"required": true,
"types": [
"image",
"video"
],
"maxItems": 10,
"maxImages": 10,
"maxVideos": 10,
"mixedTypes": true,
"maxImageSizeBytes": 8388608,
"maxVideoSizeBytes": 104857600,
"maxVideoDurationSeconds": 900,
"mimeTypes": null,
"requiredFields": {
"image": [],
"video": []
},
"dimensions": []
},
"placements": {
"values": [
"feed",
"story",
"reel",
"carousel"
],
"byMediaShape": {
"none": [],
"single-image": [
"feed",
"story"
],
"multi-image": [
"carousel"
],
"single-video": [
"story",
"reel"
]
},
"defaultByMediaShape": {
"none": null,
"single-image": "feed",
"multi-image": "carousel",
"single-video": "reel"
}
},
"options": "…JSON Schema…"
}Placement
Instagram and Facebook can publish to the feed, a story or a reel (Instagram also has carousels). Set it with options.instagram.placement or options.facebook.placement. The allowed values depend on the media, and the default is used when you leave it out (one Instagram video is a reel; a story disappears after 24 hours, so ask for it explicitly). placements in GET /api/platforms has these tables as data; a placement that does not fit the media is a 400 INVALID_PLACEMENT.
| No media | not supported |
| One image | feed, story (default feed) |
| 2+ images | carousel (default carousel) |
| One video | story, reel (default reel) |
| No media | feed (default feed) |
| One image | feed, story (default feed) |
| 2+ images | feed, story (default feed) |
| One video | feed, story, reel (default feed) |
Facebook reels also need a 9:16 video between 4 and 60 seconds. If Facebook returns an identity-confirmation error, complete the Page authorization in the Facebook mobile app before retrying.
{
"targets": [
{
"platforms": [
"instagram",
"facebook"
]
}
],
"text": "New drop today",
"media": [
{
"url": "https://cdn.example.com/drop.jpg",
"type": "image"
}
],
"options": {
"instagram": {
"placement": "story"
},
"facebook": {
"placement": "feed"
}
}
}Platform Sets
A platform set groups connected accounts, at most one per platform, for example one set per brand or client. A post's targets name the sets it publishes through, each with its platforms; a target without platformSetId uses your default set. Several sets in one post reach several accounts of the same platform. You can have up to 20 sets.
Deleting a set revokes its active accounts. Its disconnected accounts and its posts are kept without a set; reconnecting an account puts it in the set you connect it to. Your last set cannot be deleted (409 LAST_PLATFORM_SET), and creating more than 20 sets is a 409 PLATFORM_SET_LIMIT_REACHED. A set you do not own answers 404, like a missing one. Deleting a set disconnects its accounts.
/api/platform-setsList your platform sets with their active account counts, default set first. `accountCount` counts only active accounts, the same ones accounts_list returns by default; revoked accounts are not counted. A platform set groups at most one account per platform; every post targets one set.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| limit | integer | No | Page size, 1-100. Default 20. |
| offset | integer | No | Number of items to skip. Default 0. |
{
"data": [
{
"id": "9a2c4e6b-1d3f-4a5c-8e7b-2f4d6a8c0e10",
"name": "default",
"isDefault": true,
"accountCount": 3,
"createdAt": "2026-10-01T08:00:00.000Z",
"updatedAt": "2026-10-01T08:00:00.000Z"
},
{
"id": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"name": "Client A",
"isDefault": false,
"accountCount": 2,
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:00.000Z"
}
],
"pagination": {
"limit": 20,
"offset": 0,
"total": 2,
"hasMore": false
}
}/api/platform-setsCreate an empty platform set. You can have at most 20 sets.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Set name, 1-50 characters |
{
"name": "Client A"
}{
"id": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"name": "Client A",
"isDefault": false,
"accountCount": 2,
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:00.000Z"
}/api/platform-sets/:platformSetIdGet one platform set with its active account count (revoked accounts are not counted).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| platformSetId | string | Yes | Platform set id |
{
"id": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"name": "Client A",
"isDefault": false,
"accountCount": 2,
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:00.000Z"
}/api/platform-sets/:platformSetIdRename a platform set, including the default set.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| platformSetId | string | Yes | Platform set id |
| name | string | Yes | Set name, 1-50 characters |
{
"name": "Client A"
}{
"id": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"name": "Client A",
"isDefault": false,
"accountCount": 2,
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:00.000Z"
}/api/platform-sets/:platformSetIdDelete a platform set and revoke its active accounts. Its disconnected accounts and its posts are kept without a set; reconnect an account with accounts_connect and its accountId to put it in another set. Your last remaining set cannot be deleted (409 LAST_PLATFORM_SET). 409 PLATFORM_SET_IN_USE means an account was connected to the set during the delete: retry.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| platformSetId | string | Yes | Platform set id |
{
"id": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"deleted": true
}Posting Queues
Each platform set has one queue: weekly slots, such as Monday 09:00 and Friday 17:00, in an IANA time zone. Create a post with useQueue: true instead of scheduledFor and exactly one target, and Ampost schedules it in that set's next free slot. The post's queuePlatformSetId names the queue.
Slots follow the time zone's daylight saving changes. A slot is taken while a queued post is scheduled at its time; rescheduling, publishing or deleting the post frees it. Changing the slots does not move posts already in the queue. Queues need the Pro plan or above. An empty queue answers 409 QUEUE_EMPTY.
/api/platform-sets/:platformSetId/queueGet the posting queue of a platform set. A set without a saved queue answers with no slots in UTC. Each platform set has one queue: weekly slots (weekday and HH:MM) in an IANA time zone, which follow its daylight saving changes. posts_create with `useQueue: true` schedules the post in the set's next free slot. A slot is taken while a queued post is scheduled at its time; rescheduling, publishing or deleting the post frees it.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| platformSetId | string | Yes | Platform set id |
{
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"timezone": "Europe/Warsaw",
"slots": [
{
"day": "monday",
"time": "09:00"
},
{
"day": "wednesday",
"time": "12:30"
},
{
"day": "friday",
"time": "17:00"
}
],
"updatedAt": "2026-10-06T09:00:00.000Z"
}/api/platform-sets/:platformSetId/queueSet the time zone or the weekly slots of a platform set's queue; `slots` replaces the whole list. Queues need the Pro plan or above (403 UPGRADE_REQUIRED). Posts already in the queue keep their time. Each platform set has one queue: weekly slots (weekday and HH:MM) in an IANA time zone, which follow its daylight saving changes. posts_create with `useQueue: true` schedules the post in the set's next free slot. A slot is taken while a queued post is scheduled at its time; rescheduling, publishing or deleting the post frees it.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| platformSetId | string | Yes | Platform set id |
| timezone | string | No | |
| slots | object[] | No | Replaces every slot (max 100); duplicates are merged. An empty list turns the queue off. |
{
"timezone": "Europe/Warsaw",
"slots": [
{
"day": "monday",
"time": "09:00"
},
{
"day": "wednesday",
"time": "12:30"
},
{
"day": "friday",
"time": "17:00"
}
]
}{
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"timezone": "Europe/Warsaw",
"slots": [
{
"day": "monday",
"time": "09:00"
},
{
"day": "wednesday",
"time": "12:30"
},
{
"day": "friday",
"time": "17:00"
}
],
"updatedAt": "2026-10-06T09:00:00.000Z"
}/api/platform-sets/:platformSetId/queue/slotsThe queue's next slot times from now on, each with the queued post holding it (`postId`, null when free). With `filter: free` the first slot is where posts_create with `useQueue` puts the next post. Each platform set has one queue: weekly slots (weekday and HH:MM) in an IANA time zone, which follow its daylight saving changes. posts_create with `useQueue: true` schedules the post in the set's next free slot. A slot is taken while a queued post is scheduled at its time; rescheduling, publishing or deleting the post frees it.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| platformSetId | string | Yes | Platform set id |
| count | integer | No | How many slots, 1-50. Default 10. |
| filter | all | free | No | `free`: only free slots, the ones posts_create with `useQueue` fills next, in order. Default `all`: taken slots too, with their post. |
{
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"timezone": "Europe/Warsaw",
"slots": [
{
"scheduledFor": "2026-10-07T10:30:00.000Z",
"postId": "7f1c2a9e-4b1d-4c55-9a0e-2f6d8b3c1a10"
},
{
"scheduledFor": "2026-10-09T15:00:00.000Z",
"postId": null
},
{
"scheduledFor": "2026-10-12T07:00:00.000Z",
"postId": null
}
]
}Accounts
Use the account endpoints to start a browser-assisted connection or discover connected publishing accounts before creating and debugging posts.
The list returns active accounts by default. Pass status=revoked for disconnected accounts or status=all for both; a platform set's accountCount is the default list's total for that set. Getting an account by id also returns revoked accounts. Reconnect an account with the hosted flow below and its accountId; it keeps its id.
DELETE /api/accounts/{accountId} disconnects an account: it deletes the stored tokens and asks the platform to revoke Ampost's access where it can: Facebook, LinkedIn, TikTok, X, YouTube and Discord (the channel webhook is deleted). providerRevoked says whether the platform confirmed it; when it is false, providerAccessSettings says where the user removes Ampost in the platform itself. Instagram and Threads have no revoke endpoint, so the user removes Ampost in Instagram: Settings, Website permissions, Apps and websites, or in Threads: Settings, Account, Website permissions. Pinterest revokes no user tokens: the user removes Ampost in Pinterest: Settings, Security and logins, Apps. Telegram and Bluesky run on the user's own bot and app password: the user removes the bot or deletes the app password. A second call returns the account unchanged. Deleting a platform set does the same for its accounts.
health says whether an account can publish and why not: canPost, needsReconnect and issues[] with a code such as ACCOUNT_REVOKED, TOKEN_EXPIRED or MISSING_SCOPE:w_member_social, a severity and a message. Ampost renews logins by itself (Instagram and Threads 30 days before they expire, refresh tokens on use), so an issue means only a reconnect helps. It shows up 7 days before the login lapses, together with the account.needs_reconnect webhook.
analytics says whether Ampost collects analytics for the account. X bills every read, so X analytics stay off until the user turns them on with PATCH /api/accounts/{accountId}/analytics (Max and Scale; reads count against the plan's X units). Other platforms need no opt-in; Discord has no analytics.
platformMetadata holds public details captured at connect time, a fixed list per platform: the Facebook Page id and name, the LinkedIn display name, locale and Company Page posting flag, the Instagram and Threads display name and the Instagram account type, the X and Bluesky display name, the Telegram chat title, type and bot, the Discord server and channel, the YouTube channel title and handle, the Pinterest default board, and for TikTok the creator's allowed privacy levels, disabled comment/duet/stitch flags and maximum video duration; use those to pick options.tiktok.
A Pinterest Pin needs a board. GET /api/accounts/{accountId}/boards lists the account's boards and its defaultBoardId; PATCH /api/accounts/{accountId}/board sets the default with boardId, or creates a board with newBoardName. A connect picks the board by itself only when the account has exactly one; until a default is set the account has the BOARD_NOT_SELECTED health warning and each post must set options.pinterest.boardId.
Browser-assisted hosted flow
Create a short-lived request, show its one-time connectUrl to the user, and ask them to open it in a browser. The hosted Ampost page handles confirmation, provider consent, and Facebook Page selection. Poll the status resource every 2–5 seconds until it becomes connected (its accountId is then the new account), failed, or expired. A request expires after ten minutes. A new request for the same platform and set replaces a pending one, also one the user opened but did not finish, which then fails with SUPERSEDED; a user who cancels or declines consent gives USER_CANCELLED.
Pass accountId to reconnect an existing account: an active one gets fresh tokens and permissions, a revoked one is reactivated. For a new connection, expectedUsername or expectedPlatformAccountId pins the account. If the user signs in to another account, the request fails with ACCOUNT_MISMATCH and nothing changes. On Free, an account connected in another Ampost workspace (or disconnected from one less than 30 days ago) fails with SOCIAL_ACCOUNT_IN_USE.
Show the complete URL once, but never log, analyze, transform, or repeat its fragment after that handoff. Do not ask users to paste the fragment back, or ask for access tokens, OAuth codes, passwords, state values, or Facebook Page tokens. The create response is the only API response containing the URL; status responses contain only safe state and error information.
Not available: separate profiles (platform sets group accounts), customer callbacks, custom redirect URLs and headless authorization.
/api/accounts/connectCreate a short-lived Ampost-hosted link that connects one social account to a platform set. The user opens `connectUrl` in a browser and authorizes there; consent cannot happen in chat. In MCP, a client that supports URL elicitation asks the user to open the page itself, and the result then has no `connectUrl`. Then poll the connection request until it is connected, failed or expired. To reconnect an existing account (a revoked one, or an active one whose `health.needsReconnect` is true), pass its id as `accountId`; `platform` may then be omitted. For a new connection, `expectedUsername` or `expectedPlatformAccountId` makes sure the user signs in to the right account. If the user signs in to another account, the request fails with ACCOUNT_MISMATCH and nothing changes. A new request replaces the pending one for the same platform and set, also one the user opened but did not finish (it fails with SUPERSEDED), so call this again when the user lost or closed the page.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| platform | string | No | The platform to connect. Required unless `accountId` is given. |
| platformSetId | string | No | |
| accountId | string | No | |
| expectedUsername | string | No | |
| expectedPlatformAccountId | string | No |
{
"platform": "instagram",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40"
}{
"id": "11111111-1111-4111-8111-111111111111",
"platform": "instagram",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "pending",
"reconnectAccountId": null,
"expectedPlatformAccountId": null,
"expectedUsername": null,
"connectUrl": "https://ampost.io/connect/11111111-1111-4111-8111-111111111111#token=one-time-token",
"createdAt": "2026-10-06T12:00:00.000Z",
"expiresAt": "2026-10-06T12:10:00.000Z"
}/api/accounts/connect/:connectRequestIdRead the status of an account connection request: pending, awaiting_selection, connected, failed or expired. Poll every 2-5 seconds. When connected, `accountId` is the account's id (a reconnected account keeps its id). When failed or expired, `error.message` says why and what the user should do next; relay it as is. `ACCOUNT_MISMATCH` means the user signed in to another account than `accountId` or the expected one; nothing was changed. `SOCIAL_ACCOUNT_IN_USE` means a Free workspace tried to connect a social account another Ampost workspace has (or had in the last 30 days); a paid plan can connect it. `SUPERSEDED` means a newer request for this platform replaced this one: poll the newer request instead. `USER_CANCELLED` means the user cancelled or declined consent.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| connectRequestId | string | Yes | Connection request id |
{
"id": "11111111-1111-4111-8111-111111111111",
"platform": "instagram",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"status": "connected",
"accountId": "c41d8e2f-6a3b-4f9c-b1e5-7d2a9c4e6f80",
"reconnectAccountId": null,
"expectedPlatformAccountId": null,
"expectedUsername": null,
"error": null,
"createdAt": "2026-10-06T12:00:00.000Z",
"expiresAt": "2026-10-06T12:10:00.000Z",
"completedAt": "2026-10-06T12:02:14.000Z",
"updatedAt": "2026-10-06T12:02:14.000Z"
}/api/accountsList your social accounts, optionally only one platform or one platform set. `status` defaults to `active`, the accounts that can publish; pass `revoked` for disconnected accounts or `all` for both. The `active` total for a set equals that set's `accountCount` in platform_sets_list. `health` says whether each account can publish (`canPost`), whether only a reconnect helps (`needsReconnect`), and why (`issues[]` with codes such as `TOKEN_EXPIRED` or `MISSING_SCOPE:w_member_social`). Fix with accounts_connect and the account's id as `accountId`. `platformMetadata` has public per-platform details, such as the TikTok creator's allowed privacy levels.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| platform | string | No | |
| platformSetId | string | No | |
| status | active | revoked | all | No | `active` (default): accounts that can publish. `revoked`: disconnected accounts. `all`: both. |
| limit | integer | No | Page size, 1-100. Default 20. |
| offset | integer | No | Number of items to skip. Default 0. |
{
"data": [],
"pagination": {
"limit": 20,
"offset": 0,
"total": 0,
"hasMore": false
}
}/api/accounts/:accountIdGet one social account by id, including revoked ones (`status: revoked`). `health`: `canPost`, `needsReconnect` and `issues[]` (codes such as `ACCOUNT_REVOKED`, `TOKEN_EXPIRED`, `MISSING_SCOPE:<scope>`, each with a message to relay). When `needsReconnect` is true, call accounts_connect with `accountId` set to this id; the account keeps its id. `platformMetadata` has public per-platform details.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| accountId | string | Yes | Account id |
{
"id": "c41d8e2f-6a3b-4f9c-b1e5-7d2a9c4e6f80",
"platform": "instagram",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"platformAccountId": "17841400000000000",
"username": "ampost.io",
"avatarUrl": null,
"status": "active",
"scopes": [
"instagram_business_basic",
"instagram_business_content_publish"
],
"tokenExpiresAt": "2026-12-05T09:00:00.000Z",
"tokenHealth": "healthy",
"health": {
"canPost": true,
"needsReconnect": false,
"issues": []
},
"platformMetadata": {
"displayName": "Ampost",
"accountType": "BUSINESS"
},
"analytics": {
"available": true,
"optInRequired": false,
"enabled": true
},
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T09:00:00.000Z",
"lastUsedAt": "2026-10-06T09:00:04.000Z"
}/api/accounts/:accountIdDisconnect a social account: Ampost revokes its access at the platform where the platform allows it, deletes the stored tokens and sets `status: revoked`. Scheduled posts for the account will fail. The account keeps its id and can be reconnected later with accounts_connect and `accountId`. `providerRevoked` says whether the platform confirmed the revoke; when false, `providerAccessSettings` says where the user removes Ampost in the platform's settings. Instagram and Threads have no revoke endpoint, so they always return `providerRevoked: false`. Calling it again on a disconnected account returns the account unchanged with `providerRevoked: false`.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| accountId | string | Yes | Account id |
{
"id": "c41d8e2f-6a3b-4f9c-b1e5-7d2a9c4e6f80",
"platform": "instagram",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"platformAccountId": "17841400000000000",
"username": "ampost.io",
"avatarUrl": null,
"status": "revoked",
"scopes": [],
"tokenExpiresAt": null,
"tokenHealth": "inactive",
"health": {
"canPost": false,
"needsReconnect": true,
"issues": [
{
"code": "ACCOUNT_REVOKED",
"severity": "error",
"message": "The account is disconnected. Reconnect it with accounts_connect and its accountId."
}
]
},
"platformMetadata": {
"displayName": "Ampost",
"accountType": "BUSINESS"
},
"analytics": {
"available": true,
"optInRequired": false,
"enabled": true
},
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T12:20:00.000Z",
"lastUsedAt": "2026-10-06T09:00:04.000Z",
"providerRevoked": false,
"providerAccessSettings": "Instagram: Settings > Website permissions > Apps and websites"
}/api/accounts/:accountId/analyticsOpt an X account in to analytics, or out again. Every X read is billed, so X analytics are off until the account opts in, and the reads count against the plan's monthly X units like publishing (Max and Scale). When on, Ampost reads the metrics of the posts it published to the account during their first 29 days (daily for 3 days, then weekly) and the follower count once a day; three post reads or one follower read cost one X unit. The sync pauses when the units run out and resumes when they reset. Other platforms need no opt-in (`analytics.optInRequired` is false) and return 400. A plan without X returns 403 UPGRADE_REQUIRED. Returns the account with its `analytics` state.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| accountId | string | Yes | Account id |
| enabled | boolean | Yes | `true` starts the metered X analytics sync for the account; `false` stops it |
{
"enabled": true
}{
"id": "9a4e2c7b-3d1f-4b8a-9c6e-5f2a8d1b7c40",
"platform": "twitter",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"platformAccountId": "1580000000000000000",
"username": "ampost_io",
"avatarUrl": null,
"status": "active",
"scopes": [
"tweet.read",
"tweet.write",
"users.read",
"media.write",
"offline.access"
],
"tokenExpiresAt": "2026-10-07T11:00:00.000Z",
"tokenHealth": "healthy",
"health": {
"canPost": true,
"needsReconnect": false,
"issues": []
},
"platformMetadata": {},
"analytics": {
"available": true,
"optInRequired": true,
"enabled": true
},
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-07T10:00:00.000Z",
"lastUsedAt": "2026-10-06T09:00:04.000Z"
}Analytics
Metrics of Facebook Pages, Instagram, Threads, TikTok, Bluesky, YouTube and Pinterest accounts (LinkedIn once its API access is approved), X accounts that opted in, and Telegram followers, the same numbers the dashboard shows. Every platform fills the same fields (views, reach, likes, comments, shares, saves, quotes, followers); a metric a platform does not report is null. These operations need the analytics:read scope.
Ampost reads analytics in the background and serves the stored numbers: each account about hourly (daily on the Free plan), a post's metrics hourly in its first 48 hours, every 6 hours up to 14 days and daily up to 90 days. Meta itself can lag 24-48 hours. LinkedIn has no list of a member's posts, so only posts published through Ampost get LinkedIn metrics, and YouTube and Pinterest metrics cover only posts published through Ampost. Bluesky has no views. X bills every read, so X analytics run only for accounts turned on with PATCH /api/accounts/{accountId}/analytics (Max and Scale), read only posts published through Ampost, daily for 3 days and weekly up to 29 days, and count against the plan's X units: three post reads or one follower read cost a unit. When the units run out the X sync pauses until they reset. Telegram reports only the member count, and Discord has no analytics: asking for it answers 422 ANALYTICS_NOT_AVAILABLE_ON_PLATFORM. syncedAt says when each number was read. When none of the matching accounts has analytics enabled the call answers 403 ANALYTICS_NOT_ENABLED, with the reason per account in details.accounts.
/api/analytics/postsMetrics of your published posts, newest first: one `platformPosts` entry per account the post went to, with views, reach, likes, comments, shares and saves (null when the platform does not report one). Pass `postId` for one post. `analyticsStatus` says why an entry has no metrics. Analytics cover Facebook Pages, Instagram, Threads, TikTok, Bluesky, YouTube and Pinterest accounts (YouTube and Pinterest: only posts published through Ampost) (LinkedIn once its API access is approved), X accounts that opted in with accounts_set_analytics (Max and Scale; only posts published through Ampost), and Telegram followers (no post metrics). Discord has no analytics: asking for it fails with 422 `ANALYTICS_NOT_AVAILABLE_ON_PLATFORM`. When none of the matching accounts has analytics enabled the call fails with 403 `ANALYTICS_NOT_ENABLED` (its `details.accounts` say why per account). Ampost reads analytics in the background and serves the stored numbers: each account about hourly (daily on the Free plan), a post's metrics hourly in its first 48 hours, every 6 hours up to 14 days and daily up to 90 days. X posts are read daily in their first 3 days and weekly up to 29 days, which costs the plan's X units. Meta itself can lag 24-48 hours. `syncedAt` says when each number was read.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| postId | string | No | Only this post |
| platform | string | No | |
| platformSetId | string | No | |
| accountId | string | No | |
| limit | integer | No | Page size, 1-100. Default 20. |
| offset | integer | No | Number of items to skip. Default 0. |
{
"data": [
{
"postId": "7f1c2a9e-4b1d-4c55-9a0e-2f6d8b3c1a10",
"status": "published",
"text": "Launch day: Ampost publishes everywhere from one API.",
"createdAt": "2026-10-06T09:00:00.000Z",
"platformPosts": [
{
"platform": "instagram",
"accountId": "c41d8e2f-6a3b-4f9c-b1e5-7d2a9c4e6f80",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"platformPostId": "17912345678901234",
"publishedUrl": "https://www.instagram.com/p/C9xYz12AbCd/",
"publishedAt": "2026-10-06T09:00:04.000Z",
"contentType": "image",
"analyticsStatus": "available",
"metrics": {
"views": 1840,
"reach": 1210,
"likes": 96,
"comments": 12,
"shares": 7,
"saves": 15,
"quotes": null
},
"syncedAt": "2026-10-08T10:12:00.000Z"
},
{
"platform": "facebook",
"accountId": "8d3b6f1a-2c9e-4a7d-b5f3-1e6a9c2d7b30",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"platformPostId": "104857600000000_122100000000000",
"publishedUrl": "https://www.facebook.com/104857600000000/posts/122100000000000",
"publishedAt": "2026-10-06T09:00:04.000Z",
"contentType": "image",
"analyticsStatus": "available",
"metrics": {
"views": 920,
"reach": 640,
"likes": 41,
"comments": 3,
"shares": 5,
"saves": null,
"quotes": null
},
"syncedAt": "2026-10-08T10:14:00.000Z"
}
]
}
],
"pagination": {
"limit": 20,
"offset": 0,
"total": 1,
"hasMore": false
}
}/api/analytics/overviewPer account and in total, for posts published between `from` and `to` (UTC days, default the last 30): post count, summed views, reach, likes, comments, shares and saves, followers and their change, and sync state. These are the dashboard's numbers. `unavailableAccounts` lists matching accounts without analytics and why. Analytics cover Facebook Pages, Instagram, Threads, TikTok, Bluesky, YouTube and Pinterest accounts (YouTube and Pinterest: only posts published through Ampost) (LinkedIn once its API access is approved), X accounts that opted in with accounts_set_analytics (Max and Scale; only posts published through Ampost), and Telegram followers (no post metrics). Discord has no analytics: asking for it fails with 422 `ANALYTICS_NOT_AVAILABLE_ON_PLATFORM`. When none of the matching accounts has analytics enabled the call fails with 403 `ANALYTICS_NOT_ENABLED` (its `details.accounts` say why per account). Ampost reads analytics in the background and serves the stored numbers: each account about hourly (daily on the Free plan), a post's metrics hourly in its first 48 hours, every 6 hours up to 14 days and daily up to 90 days. X posts are read daily in their first 3 days and weekly up to 29 days, which costs the plan's X units. Meta itself can lag 24-48 hours. `syncedAt` says when each number was read.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| from | string | No | First day (UTC), YYYY-MM-DD. Default: 29 days before `to`. At most 90 days with `to`. |
| to | string | No | Last day (UTC), YYYY-MM-DD, included. Default: today. |
| platform | string | No | |
| platformSetId | string | No | |
| accountId | string | No |
{
"from": "2026-09-09",
"to": "2026-10-08",
"totals": {
"posts": 14,
"followers": 5320,
"followersChange": 142,
"metrics": {
"views": 21800,
"reach": 15400,
"likes": 1130,
"comments": 96,
"shares": 61,
"saves": 140,
"quotes": null
}
},
"accounts": [
{
"accountId": "c41d8e2f-6a3b-4f9c-b1e5-7d2a9c4e6f80",
"platform": "instagram",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"username": "ampost",
"posts": 9,
"followers": 4100,
"followersChange": 120,
"metrics": {
"views": 16200,
"reach": 11300,
"likes": 870,
"comments": 71,
"shares": 40,
"saves": 140,
"quotes": null
},
"syncState": "up_to_date",
"syncedAt": "2026-10-08T10:12:00.000Z",
"nextSyncAt": "2026-10-08T11:12:00.000Z"
},
{
"accountId": "8d3b6f1a-2c9e-4a7d-b5f3-1e6a9c2d7b30",
"platform": "facebook",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"username": "Ampost",
"posts": 5,
"followers": 1220,
"followersChange": 22,
"metrics": {
"views": 5600,
"reach": 4100,
"likes": 260,
"comments": 25,
"shares": 21,
"saves": null,
"quotes": null
},
"syncState": "up_to_date",
"syncedAt": "2026-10-08T10:14:00.000Z",
"nextSyncAt": "2026-10-08T11:14:00.000Z"
}
],
"unavailableAccounts": [
{
"accountId": "5e9a1c3d-7b2f-4d8e-a6c1-3f9b2e7d4a60",
"platform": "linkedin",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"reason": "platform_disabled"
}
]
}/api/analytics/followersFollower count per account with one daily snapshot per day between `from` and `to` (UTC days, default the last 30). History starts when Ampost first read the account; a day without a snapshot is missing. Analytics cover Facebook Pages, Instagram, Threads, TikTok, Bluesky, YouTube and Pinterest accounts (YouTube and Pinterest: only posts published through Ampost) (LinkedIn once its API access is approved), X accounts that opted in with accounts_set_analytics (Max and Scale; only posts published through Ampost), and Telegram followers (no post metrics). Discord has no analytics: asking for it fails with 422 `ANALYTICS_NOT_AVAILABLE_ON_PLATFORM`. When none of the matching accounts has analytics enabled the call fails with 403 `ANALYTICS_NOT_ENABLED` (its `details.accounts` say why per account). Ampost reads analytics in the background and serves the stored numbers: each account about hourly (daily on the Free plan), a post's metrics hourly in its first 48 hours, every 6 hours up to 14 days and daily up to 90 days. X posts are read daily in their first 3 days and weekly up to 29 days, which costs the plan's X units. Meta itself can lag 24-48 hours. `syncedAt` says when each number was read.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| from | string | No | First day (UTC), YYYY-MM-DD. Default: 29 days before `to`. At most 90 days with `to`. |
| to | string | No | Last day (UTC), YYYY-MM-DD, included. Default: today. |
| platform | string | No | |
| platformSetId | string | No | |
| accountId | string | No | |
| limit | integer | No | Page size, 1-100. Default 20. |
| offset | integer | No | Number of items to skip. Default 0. |
{
"data": [
{
"accountId": "c41d8e2f-6a3b-4f9c-b1e5-7d2a9c4e6f80",
"platform": "instagram",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"username": "ampost",
"followers": 4100,
"syncedAt": "2026-10-08T10:12:00.000Z",
"totalLikes": null,
"totalPosts": null,
"history": [
{
"date": "2026-10-06",
"followers": 4062,
"syncedAt": "2026-10-06T10:05:00.000Z"
},
{
"date": "2026-10-07",
"followers": 4081,
"syncedAt": "2026-10-07T10:09:00.000Z"
},
{
"date": "2026-10-08",
"followers": 4100,
"syncedAt": "2026-10-08T10:12:00.000Z"
}
]
}
],
"pagination": {
"limit": 20,
"offset": 0,
"total": 1,
"hasMore": false
}
}Webhooks
Ampost POSTs events to your https URLs as they happen, so you do not have to poll. Each webhook has its own URL, events and signing secret; you can have up to 10. These operations need the webhooks:write scope and a key that is not restricted to some platform sets. The dashboard manages webhooks under Settings, Webhooks.
The full secret is only in the response that creates the webhook and in an update with rotateSecret: true; store it then. Every other response has only secretPreview. Revoking the API key or disconnecting the app that created a webhook (or last changed its URL or rotated its secret), or the app losing webhooks:write, turns the webhook off with disabledReason: "key_revoked". Only the account owner can turn it back on, in the dashboard.
Events
post.scheduled: A post was scheduled, or its publish time changed.data.post.post.unscheduled: A scheduled post went back to a draft (scheduledForset to null).data.post.post.discarded: A post that was not published was discarded (posts_delete), scheduled or not.data.post.post.published: Every account of a post published.data.post.post.partial: A post finished with some accounts published and some failed.data.post.post.failed: Every account of a post failed.data.post.post.platform.published: One account's post went live.data.postanddata.platformPost.post.platform.failed: One account's post failed.data.postanddata.platformPost(witherror).post.platform.deleted: One account's post was deleted from its platform (posts_unpublish).data.postanddata.platformPost.post.platform.edited: One account's published text was edited on its platform (posts_edit_published).data.postanddata.platformPost(with the newtext).account.connected: An account was connected or reconnected.data.account.account.disconnected: An account was disconnected: by you, by deleting its set, or by the platform.data.account.account.needs_reconnect: An account's access was revoked, expired, or expires within 7 days and Ampost cannot renew it; reconnect it to keep publishing.data.account.analytics.synced: Analytics for an account were refreshed.dataholds the account, status and counts.webhook.test: sent only bywebhooks_test, whatever the webhook subscribes to.
data.post and data.platformPost have the shapes GET /api/posts/{postId} returns, and data.account the shape of an account. A post's metadata (your own JSON object of at most 4096 bytes, set when you create the post) is in every post event, so you can match events to your records.
{
"id": "4a9e2c7b-3d1f-4b8a-9c6e-1f5d3b7a9e28",
"type": "account.disconnected",
"createdAt": "2026-10-06T12:20:00.000Z",
"data": {
"account": {
"id": "c41d8e2f-6a3b-4f9c-b1e5-7d2a9c4e6f80",
"platform": "instagram",
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"platformAccountId": "17841400000000000",
"username": "ampost.io",
"avatarUrl": null,
"status": "revoked",
"scopes": [],
"tokenExpiresAt": null,
"tokenHealth": "inactive",
"health": {
"canPost": false,
"needsReconnect": true,
"issues": [
{
"code": "ACCOUNT_REVOKED",
"severity": "error",
"message": "The account is disconnected. Reconnect it with accounts_connect and its accountId."
}
]
},
"platformMetadata": {
"displayName": "Ampost",
"accountType": "BUSINESS"
},
"createdAt": "2026-10-06T09:00:00.000Z",
"updatedAt": "2026-10-06T12:20:00.000Z",
"lastUsedAt": "2026-10-06T09:00:04.000Z"
}
}
}Delivery and retries
Ampost-Signature:sha256=<hex>, the HMAC-SHA256 of<Ampost-Timestamp>.<raw body>keyed with the webhook'ssecret.Ampost-Timestamp: Unix seconds when the attempt was signed.Ampost-Event-Id: the event'sid. Retries and redeliveries keep it, so store it and skip an id you have already handled.Ampost-Event-TypeandAmpost-Delivery-Id.
Answer with any 2xx within 10 seconds and do slow work afterwards. Anything else (another status, a redirect, a timeout) fails the attempt, and Ampost retries after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (8 attempts in all). Events can arrive out of order. After 5 deliveries in a row fail all their retries, Ampost turns the webhook off (disabledReason: "failing") and shows it on the dashboard; fix the receiver and turn it back on. The delivery log keeps each delivery's attempts and last response for 30 days, and any delivery can be sent again.
Verify signatures
Compute the HMAC over the raw body exactly as received, before you parse the JSON, compare in constant time, and reject timestamps more than 300 seconds old.
import crypto from "node:crypto";
// rawBody: the request body as received (a string or Buffer), before JSON.parse.
export function verifyAmpostWebhook(rawBody, headers, secret) {
const timestamp = headers["ampost-timestamp"];
const signature = headers["ampost-signature"];
if (!timestamp || !signature || !/^\d+$/.test(timestamp)) return false;
// Reject old requests so a captured one cannot be replayed.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected =
"sha256=" + crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
return (
expected.length === signature.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
);
}import hashlib
import hmac
import time
# raw_body: the request body bytes as received, before json.loads.
def verify_ampost_webhook(raw_body: bytes, headers, secret: str) -> bool:
timestamp = headers.get("Ampost-Timestamp", "")
signature = headers.get("Ampost-Signature", "")
if not timestamp.isdigit() or not signature:
return False
# Reject old requests so a captured one cannot be replayed.
if abs(time.time() - int(timestamp)) > 300:
return False
message = timestamp.encode() + b"." + raw_body
expected = "sha256=" + hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)/api/webhooks/endpointsList your webhooks, oldest first. Secrets show only as `secretPreview`. Webhooks see every platform set, so keys restricted to some sets get 403 PLATFORM_SET_NOT_ALLOWED.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| limit | integer | No | Page size, 1-100. Default 20. |
| offset | integer | No | Number of items to skip. Default 0. |
{
"data": [
{
"id": "d3a7c1e9-5b2f-4a8d-9e6c-7f1b3d5a9c21",
"url": "https://example.com/ampost/webhooks",
"description": "Production receiver",
"events": [
"post.published",
"post.partial",
"post.failed",
"account.needs_reconnect"
],
"secretPreview": "whsec_…nM8q",
"enabled": true,
"disabledReason": null,
"disabledAt": null,
"consecutiveFailures": 0,
"lastDeliveryAt": "2026-10-08T15:00:01.000Z",
"lastSuccessAt": "2026-10-08T15:00:01.000Z",
"createdAt": "2026-10-01T09:00:00.000Z",
"updatedAt": "2026-10-01T09:00:00.000Z"
}
],
"pagination": {
"limit": 20,
"offset": 0,
"total": 1,
"hasMore": false
}
}/api/webhooks/endpointsSend events to an https URL. Ampost POSTs each event as JSON, signed with the webhook's secret (`Ampost-Signature`), and retries a failed delivery 7 times (1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h). After 5 deliveries in a row fail all retries the webhook is turned off. At most 10 webhooks. The response is the only one with the full `secret`: store it now. Revoking the key or app that created the webhook turns it off. Events: post.scheduled, post.unscheduled, post.discarded, post.published, post.partial, post.failed, post.platform.published, post.platform.failed, post.platform.deleted, post.platform.edited, account.connected, account.disconnected, account.needs_reconnect, analytics.synced. Webhooks see every platform set, so keys restricted to some sets get 403 PLATFORM_SET_NOT_ALLOWED.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| url | string | Yes | Public https URL that receives the events. Private, loopback, internal and Ampost's own hosts are rejected. |
| events | string[] | Yes | Events to send to this URL |
| description | string | null | No |
{
"url": "https://example.com/ampost/webhooks",
"events": [
"post.published",
"post.partial",
"post.failed",
"account.needs_reconnect"
],
"description": "Production receiver"
}{
"id": "d3a7c1e9-5b2f-4a8d-9e6c-7f1b3d5a9c21",
"url": "https://example.com/ampost/webhooks",
"description": "Production receiver",
"events": [
"post.published",
"post.partial",
"post.failed",
"account.needs_reconnect"
],
"secretPreview": "whsec_…nM8q",
"enabled": true,
"disabledReason": null,
"disabledAt": null,
"consecutiveFailures": 0,
"lastDeliveryAt": "2026-10-08T15:00:01.000Z",
"lastSuccessAt": "2026-10-08T15:00:01.000Z",
"createdAt": "2026-10-01T09:00:00.000Z",
"updatedAt": "2026-10-01T09:00:00.000Z",
"secret": "whsec_Jx2nQ8vR4tY6uI0oP3aS5dF7gH9jK1lZ2xC4vB6nM8q"
}/api/webhooks/endpoints/:webhookIdGet one webhook with its status and failure count. The secret shows only as `secretPreview`. Webhooks see every platform set, so keys restricted to some sets get 403 PLATFORM_SET_NOT_ALLOWED.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| webhookId | string | Yes | Webhook id |
{
"id": "d3a7c1e9-5b2f-4a8d-9e6c-7f1b3d5a9c21",
"url": "https://example.com/ampost/webhooks",
"description": "Production receiver",
"events": [
"post.published",
"post.partial",
"post.failed",
"account.needs_reconnect"
],
"secretPreview": "whsec_…nM8q",
"enabled": true,
"disabledReason": null,
"disabledAt": null,
"consecutiveFailures": 0,
"lastDeliveryAt": "2026-10-08T15:00:01.000Z",
"lastSuccessAt": "2026-10-08T15:00:01.000Z",
"createdAt": "2026-10-01T09:00:00.000Z",
"updatedAt": "2026-10-01T09:00:00.000Z"
}/api/webhooks/endpoints/:webhookIdChange a webhook's URL, events or description, turn it on or off, or rotate its secret (the response then has the new `secret`, the only time it is shown). While a webhook is off no events are queued for it. Turning it back on resets its failure count; events it missed are not sent, so resend the ones you need with webhooks_redeliver. A webhook turned off because its key or app was revoked (`disabledReason: "key_revoked"`) can only be turned on in the dashboard. Changing the URL or rotating the secret ties the webhook to the calling key or app.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| webhookId | string | Yes | Webhook id |
| url | string | No | |
| events | string[] | No | |
| description | string | null | No | |
| enabled | boolean | No | Turn the webhook on or off. Turning it on clears `disabledReason` and the failure count. |
| rotateSecret | boolean | No | True: replace the signing secret. The old one stops working at once. |
{
"enabled": true
}{
"id": "d3a7c1e9-5b2f-4a8d-9e6c-7f1b3d5a9c21",
"url": "https://example.com/ampost/webhooks",
"description": "Production receiver",
"events": [
"post.published",
"post.partial",
"post.failed",
"account.needs_reconnect"
],
"secretPreview": "whsec_…nM8q",
"enabled": true,
"disabledReason": null,
"disabledAt": null,
"consecutiveFailures": 0,
"lastDeliveryAt": "2026-10-08T15:00:01.000Z",
"lastSuccessAt": "2026-10-08T15:00:01.000Z",
"createdAt": "2026-10-01T09:00:00.000Z",
"updatedAt": "2026-10-01T09:00:00.000Z"
}/api/webhooks/endpoints/:webhookIdDelete a webhook and its delivery log. Pending deliveries are not sent.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| webhookId | string | Yes | Webhook id |
{
"id": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"deleted": true
}/api/webhooks/endpoints/:webhookId/testSend a signed `webhook.test` event to the webhook now, also when it is turned off, and return the delivery with your URL's response. One attempt, no retries.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| webhookId | string | Yes | Webhook id |
{
"id": "9f4b2e6d-8a1c-4d3e-b5f7-3c9a1e7d5b02",
"webhookId": "d3a7c1e9-5b2f-4a8d-9e6c-7f1b3d5a9c21",
"eventId": "2c8e4a6f-1b3d-4f5a-9e7c-8d2b4f6a1c35",
"eventType": "post.published",
"status": "succeeded",
"attempts": 1,
"maxAttempts": 1,
"nextAttemptAt": null,
"lastAttemptAt": "2026-10-08T15:00:01.000Z",
"responseStatus": 200,
"responseBody": "ok",
"error": null,
"durationMs": 84,
"redeliveryOf": null,
"payload": {
"id": "2c8e4a6f-1b3d-4f5a-9e7c-8d2b4f6a1c35",
"type": "post.published",
"createdAt": "2026-10-08T15:00:00.000Z",
"data": {
"post": {
"id": "7f1c2a9e-4b1d-4c55-9a0e-2f6d8b3c1a10",
"status": "published",
"metadata": {
"campaignId": "launch-2026"
}
}
}
},
"createdAt": "2026-10-08T15:00:00.000Z"
}/api/webhooks/endpoints/:webhookId/deliveriesThe webhook's delivery log, newest first: each event sent, its status, attempts, and the last response code and body. Kept for 30 days.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| webhookId | string | Yes | Webhook id |
| limit | integer | No | Page size, 1-100. Default 20. |
| offset | integer | No | Number of items to skip. Default 0. |
{
"data": [
{
"id": "6b2d8f4a-1c3e-4a5b-9d7f-2e4c6a8b0d13",
"webhookId": "d3a7c1e9-5b2f-4a8d-9e6c-7f1b3d5a9c21",
"eventId": "2c8e4a6f-1b3d-4f5a-9e7c-8d2b4f6a1c35",
"eventType": "post.published",
"status": "pending",
"attempts": 2,
"maxAttempts": 8,
"nextAttemptAt": "2026-10-08T15:06:30.000Z",
"lastAttemptAt": "2026-10-08T15:01:30.000Z",
"responseStatus": 500,
"responseBody": "Internal Server Error",
"error": "HTTP 500; only 2xx counts as delivered",
"durationMs": 132,
"redeliveryOf": null,
"payload": {
"id": "2c8e4a6f-1b3d-4f5a-9e7c-8d2b4f6a1c35",
"type": "post.published",
"createdAt": "2026-10-08T15:00:00.000Z",
"data": {
"post": {
"id": "7f1c2a9e-4b1d-4c55-9a0e-2f6d8b3c1a10",
"status": "published",
"metadata": {
"campaignId": "launch-2026"
}
}
}
},
"createdAt": "2026-10-08T15:00:00.000Z"
},
{
"id": "9f4b2e6d-8a1c-4d3e-b5f7-3c9a1e7d5b02",
"webhookId": "d3a7c1e9-5b2f-4a8d-9e6c-7f1b3d5a9c21",
"eventId": "2c8e4a6f-1b3d-4f5a-9e7c-8d2b4f6a1c35",
"eventType": "post.published",
"status": "succeeded",
"attempts": 1,
"maxAttempts": 8,
"nextAttemptAt": null,
"lastAttemptAt": "2026-10-08T15:00:01.000Z",
"responseStatus": 200,
"responseBody": "ok",
"error": null,
"durationMs": 84,
"redeliveryOf": null,
"payload": {
"id": "2c8e4a6f-1b3d-4f5a-9e7c-8d2b4f6a1c35",
"type": "post.published",
"createdAt": "2026-10-08T15:00:00.000Z",
"data": {
"post": {
"id": "7f1c2a9e-4b1d-4c55-9a0e-2f6d8b3c1a10",
"status": "published",
"metadata": {
"campaignId": "launch-2026"
}
}
}
},
"createdAt": "2026-10-08T15:00:00.000Z"
}
],
"pagination": {
"limit": 20,
"offset": 0,
"total": 2,
"hasMore": false
}
}/api/webhooks/endpoints/:webhookId/deliveries/:deliveryId/redeliverSend a past delivery's event again now, with the same event id and body, as a new delivery (one attempt, also when the webhook is turned off). Returns the new delivery with your URL's response.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| webhookId | string | Yes | Webhook id |
| deliveryId | string | Yes | Delivery id |
{
"id": "9f4b2e6d-8a1c-4d3e-b5f7-3c9a1e7d5b02",
"webhookId": "d3a7c1e9-5b2f-4a8d-9e6c-7f1b3d5a9c21",
"eventId": "2c8e4a6f-1b3d-4f5a-9e7c-8d2b4f6a1c35",
"eventType": "post.published",
"status": "succeeded",
"attempts": 1,
"maxAttempts": 1,
"nextAttemptAt": null,
"lastAttemptAt": "2026-10-08T15:00:01.000Z",
"responseStatus": 200,
"responseBody": "ok",
"error": null,
"durationMs": 84,
"redeliveryOf": null,
"payload": {
"id": "2c8e4a6f-1b3d-4f5a-9e7c-8d2b4f6a1c35",
"type": "post.published",
"createdAt": "2026-10-08T15:00:00.000Z",
"data": {
"post": {
"id": "7f1c2a9e-4b1d-4c55-9a0e-2f6d8b3c1a10",
"status": "published",
"metadata": {
"campaignId": "launch-2026"
}
}
}
},
"createdAt": "2026-10-08T15:00:00.000Z"
}Media Upload
Media hosted at a public URL goes straight into media. To upload files instead, send POST /api/posts as multipart/form-data: a data part with the usual JSON body, plus one part per file. A media item points at its file with { "file": "<part name>" }; Ampost reads the type, size, width, height and duration from the file's bytes. A file whose content is not its declared type gets VALIDATION_FAILED on media[i].file. Images are re-saved without metadata (EXIF, GPS). Every file part must be referenced exactly once.
Accepted types: image/jpeg, image/png, image/webp, video/mp4, video/quicktime, video/webm. The whole multipart request must be at most 4 MB, files and data together; a larger one gets 413 PAYLOAD_TOO_LARGE (over 4.5 MB, a plain-text 413 from the hosting edge). That fits images, not most videos: send larger files, up to 200 MB each, with an upload link (below) or as a public URL. Platforms that need media metadata list it in media.requiredFields of GET /api/platforms; TikTok videos, for example, need width, height and duration.
curl -X POST https://api.ampost.io/api/posts \
-H "Authorization: Bearer amp_live_YOUR_KEY" \
-F 'data={
"targets": [
{
"platforms": [
"instagram"
]
}
],
"text": "Uploaded straight from my laptop",
"media": [
{
"file": "photo",
"altText": "The new dashboard on a laptop"
}
]
}' \
-F "photo=@launch.jpg"Upload links for agents and apps
When the file is on the user's device, create a media upload. Show its uploadPageUrl to the user: an Ampost page where they pick up to 35 files in a browser. A client that has the file can instead PUT it to uploadUrl with the returned headers plus x-content-type. Both stop accepting files after 60 minutes.
Poll the upload every 3–5 seconds until it is completed, failed or expired. Each item in files is a media item; pass it to POST /api/posts unchanged. Ampost checks each file's bytes and reads width, height and duration from them; images are re-saved without metadata, so their URL differs from the one uploaded to. A direct upload whose content is not the type it was sent as ends failed, with the reason in error.
curl -X PUT "$UPLOAD_URL" \
-H "authorization: Bearer vercel_blob_client_..." \
-H "x-api-version: 12" -H "x-vercel-blob-access: public" \
-H "x-add-random-suffix: 0" -H "x-allow-overwrite: 1" \
-H "x-content-type: video/mp4" \
--data-binary @launch.mp4Files that no post uses are deleted 24 hours after the upload was created, by the daily cleanup. Files used in a post stay with the post.
/api/media/uploadsGet a way to upload a local file for a post. Media in posts_create must be a public URL; use this when the user has the file on their device or you have the file itself. Returns `uploadPageUrl`, an Ampost page where the user uploads files in a browser (in MCP, a client that supports URL elicitation asks the user to open it itself and leaves `uploadPageUrl` out), and `uploadUrl` + `method` + `headers` to PUT one file directly. Both stop accepting files after 60 minutes. Then poll media_upload_status; when it is completed, its `files` are `media[]` items for posts_create. Files that no post uses are deleted 24 hours after the upload was created (by the daily cleanup).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| fileName | string | No | File name for the direct upload, e.g. `launch.mp4`; it becomes the end of the file URL. Default `file`. Files uploaded on the page keep their own names. |
{
"fileName": "launch.mp4"
}{
"id": "3c7e9a1b-5d2f-4e8a-9b6c-1f3d5a7e9c20",
"status": "pending",
"uploadPageUrl": "https://ampost.io/upload/3c7e9a1b-5d2f-4e8a-9b6c-1f3d5a7e9c20#token=one-time-token",
"uploadUrl": "https://vercel.com/api/blob/?pathname=uploads%2F3c7e9a1b-5d2f-4e8a-9b6c-1f3d5a7e9c20%2Flaunch.mp4",
"method": "PUT",
"headers": {
"authorization": "Bearer vercel_blob_client_...",
"x-api-version": "12",
"x-vercel-blob-access": "public",
"x-add-random-suffix": "0",
"x-allow-overwrite": "1"
},
"limits": {
"maxFiles": 35,
"maxSizeBytes": 209715200,
"mimeTypes": [
"image/jpeg",
"image/png",
"image/webp",
"video/mp4",
"video/quicktime",
"video/webm"
]
},
"createdAt": "2026-10-06T12:00:00.000Z",
"expiresAt": "2026-10-06T13:00:00.000Z"
}/api/media/uploads/:uploadIdRead a media upload: `pending` until files arrive, `completed` with `files`, `failed` with `error` (the direct upload's content was not the type it was sent as), or `expired`. Poll every 3-5 seconds after media_upload_create. Each item in `files` is a `media[]` item (`url`, `type`, `mimeType`, `sizeBytes`, `width`, `height`, `durationSeconds`): pass them to posts_create unchanged.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| uploadId | string | Yes | Media upload id |
{
"id": "3c7e9a1b-5d2f-4e8a-9b6c-1f3d5a7e9c20",
"status": "completed",
"files": [
{
"url": "https://abc123.public.blob.vercel-storage.com/uploads/3c7e9a1b-5d2f-4e8a-9b6c-1f3d5a7e9c20/launch.mp4",
"type": "video",
"mimeType": "video/mp4",
"sizeBytes": 18350112,
"width": 1080,
"height": 1920,
"durationSeconds": 28
}
],
"createdAt": "2026-10-06T12:00:00.000Z",
"expiresAt": "2026-10-06T13:00:00.000Z",
"completedAt": "2026-10-06T12:03:41.000Z",
"error": null
}Scheduled posts copy URL media to Ampost storage when they are created, so the source URL only has to work at that moment. Media in Ampost storage is deleted when its post is discarded, and 7 days after the post is published or has failed; a retry after that needs new media. The published copy on the platform stays. TikTok publishes with the privacy levels the creator allows; check platformMetadata on the TikTok account before choosing options.tiktok.privacyLevel.
LinkedIn posts publish as the connected member: text-only, up to 20 images, one video, or an article (a link with an optional thumbnail image), with an optional first comment. Organization pages are not available yet, and analytics wait for Ampost's LinkedIn Community Management API access.
options.linkedinacceptstext,visibility(PUBLICorCONNECTIONS),isReshareDisabledByAuthor,articleTitle,articleDescriptionandvideoTitle.- Alt text goes on each image:
media[].altText. - Article fields need
linkUrl; an article cannot carry a video or more than one image. videoTitleneeds a video, and images and videos cannot be mixed.
{
"targets": [
{
"platforms": [
"linkedin"
]
}
],
"text": "Three screens from the new dashboard.",
"media": [
{
"url": "https://cdn.example.com/compose.png",
"type": "image",
"altText": "Compose screen"
},
{
"url": "https://cdn.example.com/posts.png",
"type": "image",
"altText": "Posts list"
},
{
"url": "https://cdn.example.com/sets.png",
"type": "image",
"altText": "Platform sets"
}
],
"options": {
"linkedin": {
"isReshareDisabledByAuthor": false
}
}
}{
"targets": [
{
"platforms": [
"linkedin"
]
}
],
"text": "Our API reference is live.",
"linkUrl": "https://ampost.io/docs",
"options": {
"linkedin": {
"articleTitle": "Ampost API reference",
"articleDescription": "Endpoints, options and limits for every platform.",
"visibility": "PUBLIC"
}
}
}{
"targets": [
{
"platforms": [
"linkedin"
]
}
],
"text": "A two-minute tour of Ampost.",
"media": [
{
"url": "https://cdn.example.com/demo.mp4",
"type": "video",
"durationSeconds": 38,
"thumbnailUrl": "https://cdn.example.com/demo-thumb.jpg"
}
],
"options": {
"linkedin": {
"videoTitle": "Ampost tour",
"visibility": "CONNECTIONS"
}
}
}Docs Search
Search these docs from code or an agent. Any valid key may call it, whatever its scopes. Results are sections of this page, the MCP docs and llms-full.txt, each with a short excerpt and its URL.
Without a key, use the public docs MCP server at https://api.ampost.io/mcp/docs: tools search, list_pages and get_page over the same sections.
/api/docs/searchSearch the Ampost docs (`/docs`, `/docs/mcp` and `/llms-full.txt`) for a question such as "schedule a post to instagram". Returns the best matching sections, each with its title, a short excerpt and its URL. Any valid key may call it.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| query | string | Yes | What you want to know, in plain words, e.g. "schedule a post to instagram". |
| limit | integer | No | Number of sections, 1-20. Default 5. |
{
"data": [
{
"id": "posts/posts_create",
"title": "Create, schedule or publish a post (posts_create)",
"url": "https://ampost.io/docs#posts_create",
"excerpt": "| `useQueue` | body | boolean | no | Schedule the post in the next free slot of its platform set's queue (see queue_get) instead of at `scheduledFor`."
},
{
"id": "platforms/instagram",
"title": "Instagram",
"url": "https://ampost.io/docs#platforms",
"excerpt": "- To post to Instagram, name `instagram` in `targets[].platforms`; its settings go in `options.instagram`."
}
]
}Feedback
Report a bug or suggest a change. Any valid key may call it, whatever its scopes, up to 10 reports an hour. When a call fails in a way you can't explain, send the request you made and the error you got, with its X-Request-Id, so the Ampost team can find it in the logs.
Ampost adds the user's plan and connected accounts with their health. Fields named like secrets (tokens, passwords, keys) and Ampost keys inside strings are removed before the report is stored. Reports are kept for 12 months. In the dashboard, use Report a bug or Suggest a change in the sidebar instead.
/api/feedbackSend the Ampost team a bug report or a suggestion. When an Ampost call fails in a way you can't explain or fix, report it with the `request` you sent (method, path or tool, body) and the `error` you got (status, code, message, `X-Request-Id`). Ampost adds the user's plan and connected accounts with their health. Fields named like secrets and Ampost keys in strings are removed; don't send credentials. Any valid key may call it; at most 10 reports an hour.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| kind | bug | suggestion | Yes | `bug` for something that went wrong, `suggestion` for a change you want |
| message | string | Yes | What happened and what you expected, or what you want changed. At most 5000 characters. |
| route | string | No | The dashboard page, or the API path or MCP tool the report is about |
| request | object | No | |
| error | object | No | |
| recentRequests | object[] | No | The dashboard's last API calls; the dashboard fills it in |
| browser | string | No | The browser's user agent; the dashboard fills it in |
| screenshot | string | No | Optional PNG or JPEG, base64 (a `data:` URL is fine). Ampost re-encodes it as JPEG. |
{
"kind": "bug",
"message": "Scheduling a LinkedIn post for next Monday fails, though the account shows as healthy.",
"route": "/api/posts",
"request": {
"method": "POST",
"path": "/api/posts",
"requestId": "req_8c1f2a9d4b7e4c0a",
"body": {
"targets": [
{
"platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
"platforms": [
"linkedin"
]
}
],
"text": "Launch day",
"scheduledFor": "2026-10-26T09:00:00.000Z"
}
},
"error": {
"status": 500,
"code": "INTERNAL_ERROR",
"message": "Something went wrong",
"requestId": "req_8c1f2a9d4b7e4c0a"
}
}{
"id": "6a0d3f52-1c4b-4e8e-9a51-3f7b2c9d8e10",
"kind": "bug",
"createdAt": "2026-10-23T09:14:00.000Z",
"attached": [
"route",
"request",
"error",
"plan",
"accounts"
]
}Idempotency and Request Ids
A request that times out may still have run. Send an Idempotency-Key header (a new UUID per action) on POST /api/posts, POST /api/posts/bulk, POST /api/posts/{postId}/publish, POST /api/posts/{postId}/retry, POST /api/accounts/connect, POST /api/platform-sets, POST /api/webhooks/endpoints and POST /api/feedback, and send the same key when you retry. A retry with the same key and body within 24 hours returns the first response without creating or publishing again, with Idempotent-Replayed: true. MCP tools take the key as the idempotencyKey argument.
curl -X POST https://api.ampost.io/api/posts \
-H "Authorization: Bearer amp_live_YOUR_KEY" \
-H "Idempotency-Key: 9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f" \
-H "Content-Type: application/json" \
-d '{"text":"Launch day","targets":[{"platforms":["linkedin"]}],"publishNow":true}'- The same key with a different request:
409 IDEMPOTENCY_KEY_REUSED. - The same key while the first request still runs:
409 IDEMPOTENCY_REQUEST_IN_PROGRESS; retry with the same key a few seconds later. - Only successful responses are kept. After an error the key is free again, so you can fix the request and retry with it.
- Keys belong to your Ampost user and are 1-255 visible ASCII characters.
Request ids
Every API and MCP response carries X-Request-Id. Send your own id (8-128 letters, digits, ., _, : or -) to have it echoed; otherwise Ampost generates one. Quote it when you contact support.
Rate Limits
Each API key owner has per-operation rate limits that depend on the plan. Each limit is a token bucket, not a fixed window: up to N requests at once, refilled continuously at N per window (60 per minute is one more request about every second), so short bursts are fine. API-key responses carry X-RateLimit-Limit (N), X-RateLimit-Remaining (requests allowed right now) and X-RateLimit-Reset (when the bucket is full again, Unix seconds). MCP tool calls count against the same limits. Requests from the Ampost dashboard are limited in separate buckets.
A request over the limit gets 429 with error code RATE_LIMITED. Every 429 carries Retry-After, the seconds until a request can pass again, and the same number as details.retryAfterSeconds (MCP tools return only the body). Wait that long, then retry.
| Limit | Endpoints |
|---|---|
| apiRead | GET /platforms, GET /posts, GET /posts/{postId}, GET /media/uploads/{uploadId}, GET /accounts, GET /accounts/{accountId}, GET /accounts/{accountId}/boards, GET /accounts/connect/{connectRequestId}, GET /platform-sets, GET /platform-sets/{platformSetId}, GET /platform-sets/{platformSetId}/queue, GET /platform-sets/{platformSetId}/queue/slots, GET /analytics/posts, GET /analytics/overview, GET /analytics/followers, GET /webhooks/endpoints, GET /webhooks/endpoints/{webhookId}, GET /webhooks/endpoints/{webhookId}/deliveries, GET /docs/search |
| createPost | POST /posts, POST /posts/bulk, PATCH /posts/{postId}, DELETE /posts/{postId}, POST /platform-sets, PATCH /platform-sets/{platformSetId}, DELETE /platform-sets/{platformSetId}, PATCH /platform-sets/{platformSetId}/queue, POST /webhooks/endpoints, PATCH /webhooks/endpoints/{webhookId}, DELETE /webhooks/endpoints/{webhookId}, POST /webhooks/endpoints/{webhookId}/test, POST /webhooks/endpoints/{webhookId}/deliveries/{deliveryId}/redeliver |
| publishPost | POST /posts/{postId}/publish, POST /posts/{postId}/unpublish, POST /posts/{postId}/edit-published, POST /posts/{postId}/retry |
| mediaUpload | POST /media/uploads |
| connectPlatform | DELETE /accounts/{accountId}, PATCH /accounts/{accountId}/analytics, PATCH /accounts/{accountId}/board, POST /accounts/connect |
| feedback | POST /feedback |
| importPosts | One token per CSV row of POST /posts/bulk (a dry run too) |
Monthly post quota
On the Free plan, creating a post counts once against the monthly postsPerMonth, whether it is published now, scheduled or kept as a draft; it is counted when the post is created, so parallel creates never go over it. Over the quota, create returns 429 QUOTA_EXCEEDED with limit, used and resetsAt in details. Paid plans have no monthly post quota; the rate limits above still apply. Scheduling needs a paid plan (403 UPGRADE_REQUIRED).
Monthly API requests
Every API-key call, REST or MCP, that passes the rate limit counts toward your plan's monthly apiRequestsPerMonth. The dashboard shows the count on Usage & plan; the limit is not enforced yet.
Error Codes
Every error has the same body on every endpoint, and MCP tools return the same body as an error result. Branch on error.code; the message is for people and may change.
{
"error": {
"code": "VALIDATION_FAILED",
"message": "media[0].type: Invalid option: expected one of \"image\"|\"video\"",
"details": {
"issues": [
{
"path": "media[0].type",
"message": "Invalid option: expected one of \"image\"|\"video\""
}
]
}
}
}Errors from a platform while publishing do not fail the request: they are stored on the post's platformPosts[].error with the platform's code, for example PAGE_IDENTITY_CONFIRMATION_REQUIRED when a Facebook Page manager must confirm their identity in the Facebook app.
| Code | HTTP | Meaning |
|---|---|---|
INVALID_REQUEST | 400 | The body is not JSON, or the multipart form cannot be read. |
VALIDATION_FAILED | 400 | A field is missing, has the wrong type or breaks a platform rule, or an id is not a UUID. details.issues lists each problem with its path. |
CONTENT_TOO_LONG | 400 | The effective text is longer than the platform allows. |
UNSUPPORTED_MEDIA_TYPE | 400 | The platform does not accept this media type. |
UNSUPPORTED_MEDIA_CONFIG | 400 | The platform does not accept this combination of media. |
MIXED_MEDIA_NOT_SUPPORTED | 400 | The platform cannot mix images and videos in one post. |
INVALID_PLACEMENT | 400 | The placement does not fit the media. details.available lists the allowed values. |
MEDIA_FETCH_FAILED | 400 | Ampost could not download a media URL: it failed, timed out, redirected too often or led to a private address. details.field names the item. |
PLATFORM_NOT_CONNECTED | 400 | The platform set has no active account for a requested platform. Connect one with accounts_connect, or target another set. |
TOO_MANY_ROWS | 400 | posts_import_csv: the file has more rows than the plan imports at once. details has rows, maxRows and tier; split the file. |
UNAUTHORIZED | 401 | The API key is missing, invalid or revoked (on MCP also an expired or revoked OAuth access token). |
INSUFFICIENT_SCOPE | 403 | The API key or connected app lacks the scope this operation needs. details.requiredScope names it. |
PLATFORM_SET_NOT_ALLOWED | 403 | The API key is restricted to other platform sets. details.platformSetId names the refused set (null when creating a set). |
UPGRADE_REQUIRED | 403 | The plan does not include this feature (scheduling and queues, X publishing and X analytics). details has feature or platform, and tier. |
ACCOUNT_LIMIT_REACHED | 403 | The plan's social account limit is reached (accounts_connect). details has limit, used and tier. Also a platform post's error.code when its account is beyond the limit after a downgrade: only the accounts connected first, up to the limit, publish until the user disconnects some or upgrades. |
ACCOUNT_SUSPENDED | 403 | Ampost suspended this account, usually after an abuse report: API keys, connected apps and the dashboard stop working, and scheduled posts fail with this code instead of publishing. Write to support@ampost.io. |
WEBHOOK_KEY_REVOKED | 403 | webhooks_update cannot turn on a webhook with disabledReason: "key_revoked"; the user turns it on in the dashboard. |
ANALYTICS_NOT_ENABLED | 403 | No matching account has analytics enabled (X needs accounts_set_analytics; LinkedIn needs Ampost's Community Management API access). details.accounts[] gives each account's reason. |
NOT_FOUND | 404 | No endpoint at this path. |
POST_NOT_FOUND | 404 | No post with this id belongs to you. |
ACCOUNT_NOT_FOUND | 404 | No account with this id belongs to you. |
PLATFORM_SET_NOT_FOUND | 404 | No platform set with this id belongs to you. |
CONNECT_REQUEST_NOT_FOUND | 404 | No connection request with this id belongs to you. |
MEDIA_UPLOAD_NOT_FOUND | 404 | No media upload with this id belongs to you. |
WEBHOOK_NOT_FOUND | 404 | No webhook with this id belongs to you. |
DELIVERY_NOT_FOUND | 404 | No delivery with this id belongs to the webhook. |
METHOD_NOT_ALLOWED | 405 | The path exists but not with this method. details.allowed and the Allow header list the methods. |
INVALID_STATUS_TRANSITION | 409 | The post's status does not allow this action, for example posts_update on a published post. |
IDEMPOTENCY_KEY_REUSED | 409 | The idempotency key was already used for a different request. Send a new key for a new action. |
IDEMPOTENCY_REQUEST_IN_PROGRESS | 409 | A request with this idempotency key is still running. Retry with the same key a few seconds later. |
QUEUE_EMPTY | 409 | useQueue: the platform set's queue has no slots. Add slots with queue_update. |
QUEUE_FULL | 409 | useQueue: every slot of the queue in the next 366 days is taken. |
QUEUE_SLOT_TAKEN | 409 | useQueue: concurrent requests kept taking the free slots. Retry. |
ACCOUNT_NEEDS_RECONNECT | 409 | The account is disconnected, or the platform no longer accepts its access (accounts_list_boards, accounts_set_board). Reconnect it with accounts_connect and its id as accountId. |
PLATFORM_SET_LIMIT_REACHED | 409 | You already have the maximum number of platform sets. |
LAST_PLATFORM_SET | 409 | Your last platform set cannot be deleted. |
PLATFORM_SET_IN_USE | 409 | An account was connected to the platform set while it was being deleted. Retry the delete. |
WEBHOOK_LIMIT_REACHED | 409 | You already have the maximum number of webhooks. details.limit gives it. |
PLATFORM_ALREADY_CONNECTED | 409 | accounts_connect: the platform set already has an active account for this platform. A set holds one account per platform: connect the second account into another set, or disconnect the first. |
CONNECT_IN_PROGRESS | 409 | accounts_connect: the user is finishing a connection for this platform and set right now. Poll the open request, then try again. |
SOCIAL_ACCOUNT_IN_USE | 409 | Free plan: the social account is connected in another Ampost workspace, or was disconnected from one less than 30 days ago. A paid plan can connect it. |
PAYLOAD_TOO_LARGE | 413 | The request body is over 4 MB; send large files with a media upload. A body over 4.5 MB never reaches Ampost: the hosting edge answers a plain-text 413 FUNCTION_PAYLOAD_TOO_LARGE. |
ANALYTICS_NOT_AVAILABLE_ON_PLATFORM | 422 | The platform has no analytics (Discord). details.platforms names it. |
QUOTA_EXCEEDED | 429 | The monthly post quota is used up. details has limit, used and resetsAt; Retry-After says how long to wait. On Free a publish also counts per social account across workspaces; then details.scope is socialAccount and details.accountId names the account. |
DAILY_PUBLISH_LIMIT_REACHED | 429 | A platform post's error.code when the account's daily publish limit is used up: accounts younger than 7 days publish at most 20 times a day on Free and 100 on a paid plan. details has limit, used and resetsAt; Ampost retries the post by itself after the reset. |
RATE_LIMITED | 429 | Too many requests. Wait Retry-After seconds (details.retryAfterSeconds), then retry. |
INTERNAL_ERROR | 500 | Something failed on Ampost's side. Retry later, and quote the response's X-Request-Id if you contact support. |
MEDIA_UPLOAD_FAILED | 502 | Ampost could not store a media file for the post. Retry later. |
PLATFORM_REQUEST_FAILED | 502 | The platform refused or failed a request Ampost made for this operation (accounts_list_boards, accounts_set_board). The message says what the platform answered. Retry later. |
PLATFORM_NOT_CONFIGURED | 503 | accounts_connect: Ampost cannot connect this platform at the moment. Try again later or contact support. |
Continue browsing
Related implementation paths
Use these nearby pages when you want product context, MCP-specific setup, or plan details alongside the raw API reference.