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.

1

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.

2

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.

3

Create a post

This creates a draft. Add "publishNow": true to publish it immediately.

terminal
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!"
}'
4

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.

ScopeAllows
posts:readList and read posts and posting queues, and read platform rules
posts:writeCreate, change, publish, retry, delete and import posts; edit posting queues
accounts:readList and read accounts, platform sets and connection requests
accounts:writeConnect and disconnect accounts; create, rename and delete platform sets
media:writeUpload media for posts
analytics:readRead post metrics, account overviews and follower history
webhooks:writeCreate, 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 media is a public https URL with a required type (image or video) and optional metadata such as width, height, durationSeconds and altText. URLs that use http, carry credentials, or point at localhost, private, link-local or internal hosts fail with VALIDATION_FAILED. If Ampost cannot download a URL (also after up to 3 redirects, each checked the same way), the error is MEDIA_FETCH_FAILED. To upload files instead, see Media upload.
  • Options: every platform accepts text; Instagram and Facebook add placement, LinkedIn and TikTok add their own settings. Unknown keys are rejected. The full schema per platform is in GET /api/platforms.
  • Targets: targets lists platform sets, each with its platforms; each platform goes to the set's account for it. Omit platformSetId to 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: platformPosts has one entry per account with its platform, set, status, published URL and error. A post whose accounts partly failed is partial; retry the failed ones, or pass accountIds to retry only some.
  • Automatic retries: when a platform rate-limits a publish, times out or has a server error (error.retryable true), 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 stays pending with the last error, attempts and nextAttemptAt, and the post is posting. Only after the last attempt does the account turn failed and the post.failed or post.partial webhook 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 nextAttemptAt and is never marked failed for it; its account stays pending.
  • Status: ampost.io/status shows whether publishing works on each platform right now, from real publish outcomes, and explains these guarantees.
  • First comment: firstComment is 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 with options.<platform>.firstComment ("" turns it off there). Each account's result is in platformPosts[].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 stays published with an error. 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 an error. Instagram, Threads, TikTok, Bluesky and X answer EDIT_NOT_SUPPORTED. Drafts and scheduled posts change with the update endpoint instead.
  • CSV import: POST /api/posts/bulk creates one post per CSV row, with the same checks as POST /api/posts. Send dryRun first: 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 an Idempotency-Key with 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: source is where the post was created: api, mcp or dashboard. Ampost sets it from the request; posts created before it was recorded have null.

The same LinkedIn post on two accounts, in two platform sets:

POST /api/posts
{
  "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
}
Response: one result per account
{
  "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:

Post with a failed platform
{
  "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:

Post waiting for a retry
{
  "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:

POST /api/posts/{postId}/unpublish
{
  "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:

POST /api/posts/{postId}/edit-published
{
  "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:

POST /api/posts/bulk
{
  "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
}
Response: one result per row
{
  "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"
        }
      ]
    }
  ]
}
POST/api/posts

Create 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.

Auth:Bearer token · scope posts:write · MCP: posts_create

Parameters

NameTypeRequiredDescription
targetsobject[]YesWhere 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.
textstringNo
firstCommentstring | nullNo
linkUrlstring | nullNo
mediaobject[]No
optionsobjectNo
scheduledForISO 8601 | nullNo
publishNowbooleanNoPublish right after creating. Cannot be combined with `scheduledFor`.
useQueuebooleanNoSchedule 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`.
metadataobjectNo
RequestExample body
{
  "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"
  }
}
201Success
{
  "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"
}
POST/api/posts/bulk

Create 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`.

Auth:Bearer token · scope posts:write · MCP: posts_import_csv

Parameters

NameTypeRequiredDescription
csvstringYesThe 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.
dryRunbooleanNoCheck every row like posts_create does without creating anything. `valid` rows would be created.
RequestExample body
{
  "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
}
200Success
{
  "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"
        }
      ]
    }
  ]
}
GET/api/posts

List your posts, newest first, optionally filtered by status, platform, platform set (posts with an account in it) or text.

Auth:Bearer token · scope posts:read · MCP: posts_list

Parameters

NameTypeRequiredDescription
statusstringNo
platformstringNo
platformSetIdstringNo
searchstringNoCase-insensitive match on the post text
limitintegerNoPage size, 1-100. Default 20.
offsetintegerNoNumber of items to skip. Default 0.
200Success
{
  "data": [],
  "pagination": {
    "limit": 20,
    "offset": 0,
    "total": 0,
    "hasMore": false
  }
}
GET/api/posts/:postId

Get one post with its result per account (`platformPosts`).

Auth:Bearer token · scope posts:read · MCP: posts_get

Parameters

NameTypeRequiredDescription
postIdstringYesPost id
200Success
{
  "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"
}
PATCH/api/posts/:postId

Change 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.

Auth:Bearer token · scope posts:write · MCP: posts_update

Parameters

NameTypeRequiredDescription
postIdstringYesPost id
targetsobject[]NoReplaces all targets. Accounts the post keeps also keep their results.
textstringNo
firstCommentstring | nullNoNew first comment; null removes it
linkUrlstring | nullNo
mediaobject[]NoReplaces all media
optionsobjectNo
scheduledForISO 8601 | nullNoNew publish time; null turns the post back into a draft
RequestExample body
{
  "text": "Launch day (updated): Ampost publishes everywhere from one API.",
  "scheduledFor": "2026-10-09T08:00:00Z"
}
200Success
{
  "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"
}
POST/api/posts/:postId/publish

Publish a draft or scheduled post to all its accounts now. Check `platformPosts` for each account's result; videos may stay `processing` for a while.

Auth:Bearer token · scope posts:write · MCP: posts_publish

Parameters

NameTypeRequiredDescription
postIdstringYesPost id
200Success
{
  "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"
}
POST/api/posts/:postId/retry

Publish 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.

Auth:Bearer token · scope posts:write · MCP: posts_retry

Parameters

NameTypeRequiredDescription
postIdstringYesPost id
accountIdsstring[]NoAccounts 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.
RequestExample body
{
  "accountIds": [
    "c41d8e2f-6a3b-4f9c-b1e5-7d2a9c4e6f80"
  ]
}
200Success
{
  "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"
}
POST/api/posts/:postId/unpublish

Take 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.

Auth:Bearer token · scope posts:write · MCP: posts_unpublish

Parameters

NameTypeRequiredDescription
postIdstringYesPost id
accountIdsstring[]NoAccounts to delete the post from (`platformPosts[].accountId`). Default: every account where it is published.
RequestExample body
{
  "accountIds": [
    "c41d8e2f-6a3b-4f9c-b1e5-7d2a9c4e6f80",
    "5e9a1c3d-7b2f-4d8e-a6c1-3f9b2e7d4a60"
  ]
}
200Success
{
  "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"
}
POST/api/posts/:postId/edit-published

Replace 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.

Auth:Bearer token · scope posts:write · MCP: posts_edit_published

Parameters

NameTypeRequiredDescription
postIdstringYesPost id
textstringYesThe new text. It replaces the text only; media and links stay. Each platform's `text.maxLength` applies.
accountIdsstring[]NoAccounts to edit the post on (`platformPosts[].accountId`). Default: every account where it is published.
RequestExample body
{
  "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"
  ]
}
200Success
{
  "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"
}
DELETE/api/posts/:postId

Discard 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.

Auth:Bearer token · scope posts:write · MCP: posts_delete

Parameters

NameTypeRequiredDescription
postIdstringYesPost id
200Success
{
  "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.

PlatformTextMediaMax itemsLinkOptions
Instagram instagram2200required: image, video; video ≤ 100 MB10ignoredtext, firstComment, placement
Facebook facebook63206optional: image, video; video (reel) 9:1610yestext, firstComment, placement
LinkedIn linkedin3000optional: image, video; video ≤ 500 MB20yestext, firstComment, visibility, isReshareDisabledByAuthor, articleTitle, articleDescription, videoTitle
TikTok tiktok2200required: image, video; video ≤ 4 GB; image ≤ 1080x1080 px; video ≥ 360x360, ≤ 4096x4096 px35ignoredtext, privacyLevel, disableComment, disableDuet, disableStitch, videoCoverTimestampMs, photoCoverIndex, autoAddMusic, brandContentToggle, brandOrganicToggle, isAigc
Threads threads500optional: image, video20yestext, firstComment
X twitter280optional: image, video; video ≤ 512 MB4yestext, firstComment
Discord discord2000optional: image, video; video ≤ 10 MB10yestext
Telegram telegram4096optional: image, video; video ≤ 50 MB10yestext
Bluesky bluesky300optional: image4yestext, firstComment
YouTube youtube5000required: video1yestext, firstComment, title, privacyStatus, madeForKids, tags, categoryId, playlistId, notifySubscribers, containsSyntheticMedia
Pinterest pinterest800required: image, video5yestext, boardId, title
GET/api/platforms

List 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.

Auth:Bearer token · scope posts:read · MCP: platforms_list

Parameters

NameTypeRequiredDescription
limitintegerNoPage size, 1-100. Default 20.
offsetintegerNoNumber of items to skip. Default 0.
One entry of GET /api/platforms
{
  "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.

Instagram

No medianot supported
One imagefeed, story (default feed)
2+ imagescarousel (default carousel)
One videostory, reel (default reel)

Facebook

No mediafeed (default feed)
One imagefeed, story (default feed)
2+ imagesfeed, story (default feed)
One videofeed, 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.

POST /api/posts
{
  "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.

GET/api/platform-sets

List 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.

Auth:Bearer token · scope accounts:read · MCP: platform_sets_list

Parameters

NameTypeRequiredDescription
limitintegerNoPage size, 1-100. Default 20.
offsetintegerNoNumber of items to skip. Default 0.
200Success
{
  "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
  }
}
POST/api/platform-sets

Create an empty platform set. You can have at most 20 sets.

Auth:Bearer token · scope accounts:write · MCP: platform_sets_create

Parameters

NameTypeRequiredDescription
namestringYesSet name, 1-50 characters
RequestExample body
{
  "name": "Client A"
}
201Success
{
  "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"
}
GET/api/platform-sets/:platformSetId

Get one platform set with its active account count (revoked accounts are not counted).

Auth:Bearer token · scope accounts:read · MCP: platform_sets_get

Parameters

NameTypeRequiredDescription
platformSetIdstringYesPlatform set id
200Success
{
  "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"
}
PATCH/api/platform-sets/:platformSetId

Rename a platform set, including the default set.

Auth:Bearer token · scope accounts:write · MCP: platform_sets_update

Parameters

NameTypeRequiredDescription
platformSetIdstringYesPlatform set id
namestringYesSet name, 1-50 characters
RequestExample body
{
  "name": "Client A"
}
200Success
{
  "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"
}
DELETE/api/platform-sets/:platformSetId

Delete 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.

Auth:Bearer token · scope accounts:write · MCP: platform_sets_delete

Parameters

NameTypeRequiredDescription
platformSetIdstringYesPlatform set id
200Success
{
  "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.

GET/api/platform-sets/:platformSetId/queue

Get 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.

Auth:Bearer token · scope posts:read · MCP: queue_get

Parameters

NameTypeRequiredDescription
platformSetIdstringYesPlatform set id
200Success
{
  "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"
}
PATCH/api/platform-sets/:platformSetId/queue

Set 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.

Auth:Bearer token · scope posts:write · MCP: queue_update

Parameters

NameTypeRequiredDescription
platformSetIdstringYesPlatform set id
timezonestringNo
slotsobject[]NoReplaces every slot (max 100); duplicates are merged. An empty list turns the queue off.
RequestExample body
{
  "timezone": "Europe/Warsaw",
  "slots": [
    {
      "day": "monday",
      "time": "09:00"
    },
    {
      "day": "wednesday",
      "time": "12:30"
    },
    {
      "day": "friday",
      "time": "17:00"
    }
  ]
}
200Success
{
  "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"
}
GET/api/platform-sets/:platformSetId/queue/slots

The 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.

Auth:Bearer token · scope posts:read · MCP: queue_slots

Parameters

NameTypeRequiredDescription
platformSetIdstringYesPlatform set id
countintegerNoHow many slots, 1-50. Default 10.
filterall | freeNo`free`: only free slots, the ones posts_create with `useQueue` fills next, in order. Default `all`: taken slots too, with their post.
200Success
{
  "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.

POST/api/accounts/connect

Create 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.

Auth:Bearer token · scope accounts:write · MCP: accounts_connect

Parameters

NameTypeRequiredDescription
platformstringNoThe platform to connect. Required unless `accountId` is given.
platformSetIdstringNo
accountIdstringNo
expectedUsernamestringNo
expectedPlatformAccountIdstringNo
RequestExample body
{
  "platform": "instagram",
  "platformSetId": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40"
}
201Success
{
  "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"
}
GET/api/accounts/connect/:connectRequestId

Read 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.

Auth:Bearer token · scope accounts:read · MCP: accounts_connect_status

Parameters

NameTypeRequiredDescription
connectRequestIdstringYesConnection request id
200Success
{
  "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"
}
GET/api/accounts

List 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.

Auth:Bearer token · scope accounts:read · MCP: accounts_list

Parameters

NameTypeRequiredDescription
platformstringNo
platformSetIdstringNo
statusactive | revoked | allNo`active` (default): accounts that can publish. `revoked`: disconnected accounts. `all`: both.
limitintegerNoPage size, 1-100. Default 20.
offsetintegerNoNumber of items to skip. Default 0.
200Success
{
  "data": [],
  "pagination": {
    "limit": 20,
    "offset": 0,
    "total": 0,
    "hasMore": false
  }
}
GET/api/accounts/:accountId

Get 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.

Auth:Bearer token · scope accounts:read · MCP: accounts_get

Parameters

NameTypeRequiredDescription
accountIdstringYesAccount id
200Success
{
  "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"
}
DELETE/api/accounts/:accountId

Disconnect 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`.

Auth:Bearer token · scope accounts:write · MCP: accounts_disconnect

Parameters

NameTypeRequiredDescription
accountIdstringYesAccount id
200Success
{
  "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"
}
PATCH/api/accounts/:accountId/analytics

Opt 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.

Auth:Bearer token · scope accounts:write · MCP: accounts_set_analytics

Parameters

NameTypeRequiredDescription
accountIdstringYesAccount id
enabledbooleanYes`true` starts the metered X analytics sync for the account; `false` stops it
RequestExample body
{
  "enabled": true
}
200Success
{
  "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.

GET/api/analytics/posts

Metrics 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.

Auth:Bearer token · scope analytics:read · MCP: analytics_posts

Parameters

NameTypeRequiredDescription
postIdstringNoOnly this post
platformstringNo
platformSetIdstringNo
accountIdstringNo
limitintegerNoPage size, 1-100. Default 20.
offsetintegerNoNumber of items to skip. Default 0.
200Success
{
  "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
  }
}
GET/api/analytics/overview

Per 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.

Auth:Bearer token · scope analytics:read · MCP: analytics_overview

Parameters

NameTypeRequiredDescription
fromstringNoFirst day (UTC), YYYY-MM-DD. Default: 29 days before `to`. At most 90 days with `to`.
tostringNoLast day (UTC), YYYY-MM-DD, included. Default: today.
platformstringNo
platformSetIdstringNo
accountIdstringNo
200Success
{
  "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"
    }
  ]
}
GET/api/analytics/followers

Follower 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.

Auth:Bearer token · scope analytics:read · MCP: analytics_followers

Parameters

NameTypeRequiredDescription
fromstringNoFirst day (UTC), YYYY-MM-DD. Default: 29 days before `to`. At most 90 days with `to`.
tostringNoLast day (UTC), YYYY-MM-DD, included. Default: today.
platformstringNo
platformSetIdstringNo
accountIdstringNo
limitintegerNoPage size, 1-100. Default 20.
offsetintegerNoNumber of items to skip. Default 0.
200Success
{
  "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 (scheduledFor set 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.post and data.platformPost.
  • post.platform.failed: One account's post failed. data.post and data.platformPost (with error).
  • post.platform.deleted: One account's post was deleted from its platform (posts_unpublish). data.post and data.platformPost.
  • post.platform.edited: One account's published text was edited on its platform (posts_edit_published). data.post and data.platformPost (with the new text).
  • 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. data holds the account, status and counts.
  • webhook.test: sent only by webhooks_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.

account.disconnected
{
  "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's secret.
  • Ampost-Timestamp: Unix seconds when the attempt was signed.
  • Ampost-Event-Id: the event's id. Retries and redeliveries keep it, so store it and skip an id you have already handled.
  • Ampost-Event-Type and Ampost-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.

verify.mjs
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))
  );
}
verify.py
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)
GET/api/webhooks/endpoints

List 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.

Auth:Bearer token · scope webhooks:write · MCP: webhooks_list

Parameters

NameTypeRequiredDescription
limitintegerNoPage size, 1-100. Default 20.
offsetintegerNoNumber of items to skip. Default 0.
200Success
{
  "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
  }
}
POST/api/webhooks/endpoints

Send 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.

Auth:Bearer token · scope webhooks:write · MCP: webhooks_create

Parameters

NameTypeRequiredDescription
urlstringYesPublic https URL that receives the events. Private, loopback, internal and Ampost's own hosts are rejected.
eventsstring[]YesEvents to send to this URL
descriptionstring | nullNo
RequestExample body
{
  "url": "https://example.com/ampost/webhooks",
  "events": [
    "post.published",
    "post.partial",
    "post.failed",
    "account.needs_reconnect"
  ],
  "description": "Production receiver"
}
201Success
{
  "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"
}
GET/api/webhooks/endpoints/:webhookId

Get 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.

Auth:Bearer token · scope webhooks:write · MCP: webhooks_get

Parameters

NameTypeRequiredDescription
webhookIdstringYesWebhook id
200Success
{
  "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"
}
PATCH/api/webhooks/endpoints/:webhookId

Change 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.

Auth:Bearer token · scope webhooks:write · MCP: webhooks_update

Parameters

NameTypeRequiredDescription
webhookIdstringYesWebhook id
urlstringNo
eventsstring[]No
descriptionstring | nullNo
enabledbooleanNoTurn the webhook on or off. Turning it on clears `disabledReason` and the failure count.
rotateSecretbooleanNoTrue: replace the signing secret. The old one stops working at once.
RequestExample body
{
  "enabled": true
}
200Success
{
  "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"
}
DELETE/api/webhooks/endpoints/:webhookId

Delete a webhook and its delivery log. Pending deliveries are not sent.

Auth:Bearer token · scope webhooks:write · MCP: webhooks_delete

Parameters

NameTypeRequiredDescription
webhookIdstringYesWebhook id
200Success
{
  "id": "0b6e4d2a-9c3f-4e1b-8a7d-5f2c1e9b3d40",
  "deleted": true
}
POST/api/webhooks/endpoints/:webhookId/test

Send 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.

Auth:Bearer token · scope webhooks:write · MCP: webhooks_test

Parameters

NameTypeRequiredDescription
webhookIdstringYesWebhook id
200Success
{
  "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"
}
GET/api/webhooks/endpoints/:webhookId/deliveries

The webhook's delivery log, newest first: each event sent, its status, attempts, and the last response code and body. Kept for 30 days.

Auth:Bearer token · scope webhooks:write · MCP: webhooks_deliveries_list

Parameters

NameTypeRequiredDescription
webhookIdstringYesWebhook id
limitintegerNoPage size, 1-100. Default 20.
offsetintegerNoNumber of items to skip. Default 0.
200Success
{
  "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
  }
}
POST/api/webhooks/endpoints/:webhookId/deliveries/:deliveryId/redeliver

Send 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.

Auth:Bearer token · scope webhooks:write · MCP: webhooks_redeliver

Parameters

NameTypeRequiredDescription
webhookIdstringYesWebhook id
deliveryIdstringYesDelivery id
200Success
{
  "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.

terminal
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.

terminal
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.mp4

Files 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.

POST/api/media/uploads

Get 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).

Auth:Bearer token · scope media:write · MCP: media_upload_create

Parameters

NameTypeRequiredDescription
fileNamestringNoFile 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.
RequestExample body
{
  "fileName": "launch.mp4"
}
201Success
{
  "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"
}
GET/api/media/uploads/:uploadId

Read 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.

Auth:Bearer token · scope media:write · MCP: media_upload_status

Parameters

NameTypeRequiredDescription
uploadIdstringYesMedia upload id
200Success
{
  "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

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.linkedin accepts text, visibility (PUBLIC or CONNECTIONS), isReshareDisabledByAuthor, articleTitle, articleDescription and videoTitle.
  • Alt text goes on each image: media[].altText.
  • Article fields need linkUrl; an article cannot carry a video or more than one image.
  • videoTitle needs a video, and images and videos cannot be mixed.
Images with alt text
{
  "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
    }
  }
}
Article
{
  "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"
    }
  }
}
Video
{
  "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"
    }
  }
}

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.

POST/api/feedback

Send 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.

Auth:Bearer token · scope none (any key) · MCP: feedback_submit

Parameters

NameTypeRequiredDescription
kindbug | suggestionYes`bug` for something that went wrong, `suggestion` for a change you want
messagestringYesWhat happened and what you expected, or what you want changed. At most 5000 characters.
routestringNoThe dashboard page, or the API path or MCP tool the report is about
requestobjectNo
errorobjectNo
recentRequestsobject[]NoThe dashboard's last API calls; the dashboard fills it in
browserstringNoThe browser's user agent; the dashboard fills it in
screenshotstringNoOptional PNG or JPEG, base64 (a `data:` URL is fine). Ampost re-encodes it as JPEG.
RequestExample body
{
  "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"
  }
}
201Success
{
  "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.

Create and publish once
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.

LimitEndpoints
apiReadGET /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
createPostPOST /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
publishPostPOST /posts/{postId}/publish, POST /posts/{postId}/unpublish, POST /posts/{postId}/edit-published, POST /posts/{postId}/retry
mediaUploadPOST /media/uploads
connectPlatformDELETE /accounts/{accountId}, PATCH /accounts/{accountId}/analytics, PATCH /accounts/{accountId}/board, POST /accounts/connect
feedbackPOST /feedback
importPostsOne 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.

400 VALIDATION_FAILED
{
  "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.

CodeHTTPMeaning
INVALID_REQUEST400The body is not JSON, or the multipart form cannot be read.
VALIDATION_FAILED400A 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_LONG400The effective text is longer than the platform allows.
UNSUPPORTED_MEDIA_TYPE400The platform does not accept this media type.
UNSUPPORTED_MEDIA_CONFIG400The platform does not accept this combination of media.
MIXED_MEDIA_NOT_SUPPORTED400The platform cannot mix images and videos in one post.
INVALID_PLACEMENT400The placement does not fit the media. details.available lists the allowed values.
MEDIA_FETCH_FAILED400Ampost 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_CONNECTED400The platform set has no active account for a requested platform. Connect one with accounts_connect, or target another set.
TOO_MANY_ROWS400posts_import_csv: the file has more rows than the plan imports at once. details has rows, maxRows and tier; split the file.
UNAUTHORIZED401The API key is missing, invalid or revoked (on MCP also an expired or revoked OAuth access token).
INSUFFICIENT_SCOPE403The API key or connected app lacks the scope this operation needs. details.requiredScope names it.
PLATFORM_SET_NOT_ALLOWED403The API key is restricted to other platform sets. details.platformSetId names the refused set (null when creating a set).
UPGRADE_REQUIRED403The plan does not include this feature (scheduling and queues, X publishing and X analytics). details has feature or platform, and tier.
ACCOUNT_LIMIT_REACHED403The 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_SUSPENDED403Ampost 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_REVOKED403webhooks_update cannot turn on a webhook with disabledReason: "key_revoked"; the user turns it on in the dashboard.
ANALYTICS_NOT_ENABLED403No 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_FOUND404No endpoint at this path.
POST_NOT_FOUND404No post with this id belongs to you.
ACCOUNT_NOT_FOUND404No account with this id belongs to you.
PLATFORM_SET_NOT_FOUND404No platform set with this id belongs to you.
CONNECT_REQUEST_NOT_FOUND404No connection request with this id belongs to you.
MEDIA_UPLOAD_NOT_FOUND404No media upload with this id belongs to you.
WEBHOOK_NOT_FOUND404No webhook with this id belongs to you.
DELIVERY_NOT_FOUND404No delivery with this id belongs to the webhook.
METHOD_NOT_ALLOWED405The path exists but not with this method. details.allowed and the Allow header list the methods.
INVALID_STATUS_TRANSITION409The post's status does not allow this action, for example posts_update on a published post.
IDEMPOTENCY_KEY_REUSED409The idempotency key was already used for a different request. Send a new key for a new action.
IDEMPOTENCY_REQUEST_IN_PROGRESS409A request with this idempotency key is still running. Retry with the same key a few seconds later.
QUEUE_EMPTY409useQueue: the platform set's queue has no slots. Add slots with queue_update.
QUEUE_FULL409useQueue: every slot of the queue in the next 366 days is taken.
QUEUE_SLOT_TAKEN409useQueue: concurrent requests kept taking the free slots. Retry.
ACCOUNT_NEEDS_RECONNECT409The 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_REACHED409You already have the maximum number of platform sets.
LAST_PLATFORM_SET409Your last platform set cannot be deleted.
PLATFORM_SET_IN_USE409An account was connected to the platform set while it was being deleted. Retry the delete.
WEBHOOK_LIMIT_REACHED409You already have the maximum number of webhooks. details.limit gives it.
PLATFORM_ALREADY_CONNECTED409accounts_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_PROGRESS409accounts_connect: the user is finishing a connection for this platform and set right now. Poll the open request, then try again.
SOCIAL_ACCOUNT_IN_USE409Free 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_LARGE413The 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_PLATFORM422The platform has no analytics (Discord). details.platforms names it.
QUOTA_EXCEEDED429The 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_REACHED429A 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_LIMITED429Too many requests. Wait Retry-After seconds (details.retryAfterSeconds), then retry.
INTERNAL_ERROR500Something failed on Ampost's side. Retry later, and quote the response's X-Request-Id if you contact support.
MEDIA_UPLOAD_FAILED502Ampost could not store a media file for the post. Retry later.
PLATFORM_REQUEST_FAILED502The 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_CONFIGURED503accounts_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.