# Publishing a Post · PostLake Docs > Markdown version of https://docs.postlake.dev/publishing . The canonical page for humans. > PostLake is the social media API for AI agents: https://postlake.dev/llms.txt 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 `profile` name and it posts to **every account under that profile**. It's the same name you see on the Channels page. Add an optional `platforms` filter to narrow it to certain networks. - **By account id (precise)**: give an `accounts` array of exact account ids (`acc_…`) from `GET /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 | Optional filter: keep only these networks from the resolved set (e.g. `["bluesky"]`). | | `accounts` | array | 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 | Media ids from `POST /v1/media` to attach to every account. | | `mediaOverrides` | object | Per-platform media sets, keyed by platform. | | `scheduledAt` | string | ISO-8601 UTC. Omit to publish now. See [Scheduling](https://postlake.dev/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 uses `text`. - **`platformOptions`**: platform-specific settings, e.g. a Pinterest `boardId` + destination `link`, or a TikTok `mode` (`direct` to publish now, `inbox` to send to drafts). Call `GET /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](https://postlake.dev/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"] }' ```