Guides
Publishing a post
A single POST /v1/posts fans out to every account you choose and returns one normalised Post object. This page covers the whole request and response in depth.
Where to post: profile or accounts
You choose which accounts a post goes to in one of two ways, pick whichever fits:
- By profile (simplest): give a
profilename and it posts to every account under that profile. It's the same name you see on the Channels page. Add an optionalplatformsfilter to narrow it to certain networks. - By account id (precise): give an
accountsarray of exact account ids (acc_…) fromGET /v1/social-accounts. Ideal when an agent needs to hit specific accounts and nothing else.
Provide profile, accounts, or both (they're combined and de-duplicated). At least one is required.
// Simple: whole profile
{ "text": "Hello", "profile": "my-brand" }
// Profile, but only some networks
{ "text": "Hello", "profile": "my-brand", "platforms": ["bluesky", "threads"] }
// Precise: exact accounts
{ "text": "Hello", "accounts": ["acc_7f3k9"] }The request body
| Field | Type | Description |
|---|---|---|
text | string | The post text. Used for every account unless overridden. Required unless every account is overridden. |
profile | string | A profile name: posts to every account it owns. The simple way to say where. Provide this, accounts, or both. |
platforms | array<string> | Optional filter: keep only these networks from the resolved set (e.g. ["bluesky"]). |
accounts | array<string> | Exact social-account ids to publish to (e.g. acc_123), from GET /v1/social-accounts. An alternative (or addition) to profile. |
textOverrides | object | Per-platform text, keyed by platform (e.g. { "x": "shorter" }). Falls back to text. |
media | array<string> | Media ids from POST /v1/media to attach to every account. |
mediaOverrides | object | Per-platform media sets, keyed by platform. |
scheduledAt | string<date-time> | ISO-8601 UTC. Omit to publish now. See Scheduling. |
platformOptions | object | Platform-specific options (Pinterest boardId, TikTok mode, …). See below. |
Per-platform text & options
The same post rarely reads the same on every network. Two hooks let you tailor per platform without sending separate requests:
textOverrides: a shorter caption for X, a longer one for LinkedIn, keyed by platform. Anything not overridden usestext.platformOptions: platform-specific settings, e.g. a PinterestboardId+ destinationlink, or a TikTokmode(directto publish now,inboxto send to drafts). CallGET /v1/platforms/{platform}for the full, live option set.
{
"text": "Big update on the blog 👇",
"textOverrides": { "x": "Big update, link in thread" },
"accounts": ["acc_x", "acc_pin"],
"platformOptions": { "pinterest": { "boardId": "98765", "link": "https://example.com/post" } }
}The response & its lifecycle
You get one Post with an overall state and a targets array. One entry per account, each with its own state, url and (if it failed) an error.
{
"id": "post_a1b2c3",
"state": "partial",
"targets": [
{ "account": "acc_x", "platform": "x", "state": "published", "url": "https://x.com/…" },
{ "account": "acc_pin", "platform": "pinterest", "state": "failed",
"error": { "type": "invalid_request", "message": "boardId not found", "retryable": false } }
]
}A post's state summarises its targets:
| State | Meaning |
|---|---|
queued | Accepted, not yet sent. |
processing | Being published (some platforms publish asynchronously). |
published | Every target succeeded. |
partial | Some targets published, some failed: check each target. |
failed | No target published. |
scheduled | Queued for a future scheduledAt. |
Partial success is normal. And free where it fails
One target failing (over a limit, a bad option) never blocks the others. You're only charged for targets that actually publish: failed targets cost nothing. Always read each targets[].state rather than assuming the whole post succeeded. Full detail on the Errors page.
Idempotency
Send an Idempotency-Key header (any unique string per logical post). If the request is retried with the same key, a network blip, an agent re-run, PostLake returns the original result instead of posting again. This is what makes automated and agent-driven posting safe.
curl -X POST https://api.postlake.dev/v1/posts \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: post-2026-08-01-blog" \
-H "Content-Type: application/json" \
-d '{ "text": "Hello", "accounts": ["acc_123"] }'