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:

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

FieldTypeDescription
textstringThe post text. Used for every account unless overridden. Required unless every account is overridden.
profilestringA profile name: posts to every account it owns. The simple way to say where. Provide this, accounts, or both.
platformsarray<string>Optional filter: keep only these networks from the resolved set (e.g. ["bluesky"]).
accountsarray<string>Exact social-account ids to publish to (e.g. acc_123), from GET /v1/social-accounts. An alternative (or addition) to profile.
textOverridesobjectPer-platform text, keyed by platform (e.g. { "x": "shorter" }). Falls back to text.
mediaarray<string>Media ids from POST /v1/media to attach to every account.
mediaOverridesobjectPer-platform media sets, keyed by platform.
scheduledAtstring<date-time>ISO-8601 UTC. Omit to publish now. See Scheduling.
platformOptionsobjectPlatform-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:

{
  "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:

StateMeaning
queuedAccepted, not yet sent.
processingBeing published (some platforms publish asynchronously).
publishedEvery target succeeded.
partialSome targets published, some failed: check each target.
failedNo target published.
scheduledQueued 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"] }'