# Human-in-the-Loop Social Posting for AI Agents (2026) · PostLake > Markdown version of https://postlake.dev/guides/human-approval-drafts . The canonical page for humans. > PostLake is the social media API for AI agents: https://postlake.dev/llms.txt Agents draft. You *approve*. A prompt that says 'always create a draft' is guidance, not a security boundary. PostLake lets the owner enforce approval on each OAuth agent. Once enabled, publish requests become drafts at the API boundary and the same agent cannot approve them. Existing agents keep direct publishing until the owner turns the policy on. **In short:** Turn on Require my approval for an OAuth agent. PostLake forces its publish requests into drafts and prevents that agent from approving its own work. API-key workflows can use draft: true. ## When this guide is for you You want an agent to draft social posts, and a person to approve them, without a second approval product. ## Before you start Same setup the [docs quickstart](https://docs.postlake.dev/quickstart.md) uses. Do this once: - Sign up at [app.postlake.dev](https://app.postlake.dev) and verify your email (unlocks free credits). - On **Channels**, create a **profile** (e.g. `my-brand`) and connect at least one account. **Bluesky is the fastest first channel**: no app review. Instagram/TikTok/Facebook need each platform's review before API posting. - Account menu → **API Keys** → create a key (`sk_live_…`). It shows once; treat it like a password. ## Create a draft, then publish it The owner sets the rule once. The agent writes, PostLake saves a draft, and only the owner or an account-level workflow can send it. ``` # 1. Agent writes. Nothing goes live. Nothing is charged. curl -X POST https://api.postlake.dev/v1/posts \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: recap-draft-001" \ -d '{ "text": "Friday recap from the changelog", "profile": "my-brand", "scheduledAt": "2026-09-04T09:00:00", "timezone": "Europe/London", "draft": true }' # Response state should be "draft". # 2. After you read it: publish. Keeps the scheduled time unless you override. curl -X POST https://api.postlake.dev/v1/posts/post_a1b2c3/publish \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' # MCP OAuth: turn on Require my approval under Agents. create_post then saves # a draft automatically, and the owner publishes it from PostLake. ``` ## Step by step 1. Open **Agents**, edit the OAuth connection's limits, and turn on **Require my approval before publishing**. 1. The agent writes with the same fields as a live post (text, media, overrides, scheduledAt). 1. List drafts with `GET /v1/posts?state=draft` or the dashboard calendar. 1. Edit with `PATCH /v1/posts/{id}` (drafts can also change destinations, which scheduled posts cannot). 1. Approve from PostLake or with an account-level API key using `POST /v1/posts/{id}/publish`. The gated OAuth agent cannot approve its own draft. A failed publish leaves the draft intact. ## Read the response (don't skip this) You get one `Post` with an overall `state` and a `targets[]` array. One entry per account. **Always check each target**; partial success is normal. ``` { "id": "post_a1b2c3", "state": "partial", "targets": [ { "platform": "bluesky", "state": "published", "url": "https://bsky.app/…" }, { "platform": "linkedin", "state": "failed", "error": { "type": "invalid_request", "message": "…", "retryable": false } } ] } ``` - `published`: every target succeeded. `partial`: some published, some failed. `failed`: none published. `processing`: still going (async networks like TikTok). - **You're only charged for targets that actually publish.** Failed targets cost nothing. - Send an `Idempotency-Key` header on writes so a retry never double-posts. - Full detail: [Publishing](https://docs.postlake.dev/publishing) · [Errors & retries](https://docs.postlake.dev/errors) ## Where the post goes Same rules as the docs. Pick one addressing style: - **By profile**: `"profile": "my-brand"` posts to every account under that profile (the name on Channels). - **Filter networks**: `"platforms": ["bluesky", "linkedin"]` narrows that set. It is a **filter, not a selector**: if you have two Pinterest boards, both match `pinterest`. - **By account id**: `"accounts": ["acc_…"]` for exact channels (copy an id on Channels, or `GET /v1/social-accounts`). - One profile and you omit `profile`? PostLake uses that profile. Multiple profiles and you omit it? You'll get an error that names them. See [Publishing: where to post](https://docs.postlake.dev/publishing). ## Do more (same API) - **Schedule**: add `scheduledAt` as UTC (trailing `Z`), or a naive local time plus `timezone` (IANA, e.g. `Europe/London`). Credits charge when it fires. [Scheduling docs](https://docs.postlake.dev/scheduling) · [scheduling guide](/guides/schedule-posts-api). - **Media**: `POST /v1/media`, then pass the `med_…` id in `media`. [Media docs](https://docs.postlake.dev/media). - **Per-network caption**: `textOverrides` (e.g. shorter text for X). **Per-network options**: `platformOptions` (Pinterest `boardId`, TikTok privacy, …). Live option lists: `GET /v1/platforms/{platform}`. - **Validate first**: `POST /v1/posts/validate` runs the same checks without publishing (free). ## Pitfalls specific to this path - A failed publish does not consume the draft. If you treat a 4xx as 'it's gone', you will rewrite work that is still there. - Pasting a live key into chat is how keys leak. Prefer MCP OAuth so the agent can draft without ever seeing a secret. - Do not rely on a system prompt as the approval control. Enforce the policy on the OAuth connection so an omitted field cannot publish. Want zero wrapper code? Connect the hosted [MCP server](https://docs.postlake.dev/mcp) (`https://api.postlake.dev/mcp`) over OAuth. Same accounts and responses as this API path. [Agents overview](/agents/). ## Common questions ### How do I stop an AI agent from publishing until I approve? Turn on Require my approval for the agent under Agents. PostLake then saves every publish request from that OAuth connection as a draft, even if the agent omits draft: true, and prevents it from self-approving. Publish from PostLake after review. ### Is this like pendpost or Rolino? Same job (fail-closed approval), different shape. Those are local-first apps. PostLake is a hosted API: the draft lives next to scheduling, analytics and MCP, so the agent does not need a second vendor for the gate. ### Can a draft also be scheduled? Yes. Save it with scheduledAt. publish_draft keeps that time unless you send a new one, or clear it to publish immediately. ## Go deeper in the docs These guides stay short on purpose. Canonical behaviour lives here: [Docs ### Quickstart Account, first channel, first post, MCP or API.](https://docs.postlake.dev/quickstart) [Docs ### Publishing Request body, addressing. Partial success, lifecycle.](https://docs.postlake.dev/publishing) [Docs ### Scheduling UTC or local time plus timezone, edit/cancel, fire-time credits.](https://docs.postlake.dev/scheduling) [Docs ### Platforms Limits, media rules, live platformOptions.](https://docs.postlake.dev/platforms) Also: [Media](https://docs.postlake.dev/media) · [Errors](https://docs.postlake.dev/errors) · [MCP](https://docs.postlake.dev/mcp) · [Analytics](https://docs.postlake.dev/analytics) ## Related guides - [Agent content calendar](https://postlake.dev/guides/ai-content-calendar.md) - [Build an AI social agent](https://postlake.dev/guides/ai-social-media-agent.md) - [Manage social media agents](https://postlake.dev/guides/manage-social-media-agents.md) - [Schedule posts via API](https://postlake.dev/guides/schedule-posts-api.md) - [Social media API for AI agents](https://postlake.dev/guides/social-media-api-for-ai-agents.md) [All guides](https://postlake.dev/guides/index.md) · [Full docs](https://docs.postlake.dev/) · [llms.txt](https://postlake.dev/llms.txt) Plain markdown for agents: https://postlake.dev/guides/human-approval-drafts.md · https://postlake.dev/llms.txt