# Drafts and approvals

Save posts as drafts, hold them for review with requiresApproval, and approve or reject them with one call before anything goes live.

Not every post should publish the moment it is written. A **draft** is saved and goes nowhere until you schedule it. A post that **requires approval** has a time but waits in `pending_approval` until someone with the right access approves it. Both are ordinary posts, so they show up in `GET /v1/posts` and can be edited like any other.

## Save a draft

Send `isDraft: true`, or simply leave out every scheduling field. `platforms` is optional for drafts, so you can save the text and media before anyone has chosen the accounts.

```bash cURL
curl -X POST https://api.pozy.ai/v1/posts \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Draft: holiday opening hours. Confirm with the store team.",
    "isDraft": true,
    "tags": ["holiday", "needs-review"]
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/posts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    content: "Draft: holiday opening hours. Confirm with the store team.",
    isDraft: true,
    tags: ["holiday", "needs-review"],
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.post._id, data.post.status); // "draft"
```
```python Python
resp = requests.post(
    "https://api.pozy.ai/v1/posts",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "content": "Draft: holiday opening hours. Confirm with the store team.",
        "isDraft": True,
        "tags": ["holiday", "needs-review"],
    },
    timeout=30,
)
print(resp.json()["post"]["status"])  # draft
```

`isDraft: true` wins over `publishNow`, `scheduledFor` and `queuedFromProfile`, so a draft can never publish by accident. Drafts skip most platform checks, which is why an incomplete post saves without errors. The checks run when you schedule it.

List drafts with `GET /v1/posts?status=draft`, optionally filtered by `profileId`, `tags` or `search`.

## Turn a draft into a scheduled post

Update the post with `isDraft: false` and one of `scheduledFor`, `publishNow` or `queuedFromProfile`. Add `platforms` if the draft had none.

```bash cURL
curl -X PUT https://api.pozy.ai/v1/posts/6721a8c04e9b2f1d7c3a5e88 \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "isDraft": false,
    "content": "Holiday hours: open 10:00 to 16:00 on 24 and 31 December.",
    "platforms": [
      { "platform": "facebook", "accountId": "6710c3e2a9f14b0d8e5c7a25" },
      { "platform": "googlebusiness", "accountId": "6710c3e2a9f14b0d8e5c7a26" }
    ],
    "scheduledFor": "2026-12-20T09:00:00",
    "timezone": "Europe/Madrid"
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/posts/6721a8c04e9b2f1d7c3a5e88", {
  method: "PUT",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    isDraft: false,
    content: "Holiday hours: open 10:00 to 16:00 on 24 and 31 December.",
    platforms: [
      { platform: "facebook", accountId: "6710c3e2a9f14b0d8e5c7a25" },
      { platform: "googlebusiness", accountId: "6710c3e2a9f14b0d8e5c7a26" },
    ],
    scheduledFor: "2026-12-20T09:00:00",
    timezone: "Europe/Madrid",
  }),
});
if (!res.ok) throw new Error((await res.json()).error);
```
```python Python
requests.put(
    "https://api.pozy.ai/v1/posts/6721a8c04e9b2f1d7c3a5e88",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "isDraft": False,
        "content": "Holiday hours: open 10:00 to 16:00 on 24 and 31 December.",
        "platforms": [
            {"platform": "facebook", "accountId": "6710c3e2a9f14b0d8e5c7a25"},
            {"platform": "googlebusiness", "accountId": "6710c3e2a9f14b0d8e5c7a26"},
        ],
        "scheduledFor": "2026-12-20T09:00:00",
        "timezone": "Europe/Madrid",
    },
    timeout=30,
).raise_for_status()
```

> **Note:**
> 
> If you send a time without `isDraft: false`, the update succeeds but the post stays a draft. This is deliberate: an editor changing the suggested time of a draft should not publish it.

Scheduling runs the full platform validation (media rules, required fields, text limits). A post that fails it returns `400` and stays a draft. Run [validation](https://pozy.ai/docs/publishing/validation) first if you want to show problems before the person presses Schedule.

## Hold a post for approval

Add `requiresApproval: true` and a time. The post is saved as `pending_approval` and will not publish, even when its time comes, until it is approved.

```json
{
  "content": "Flash sale: 30% off annual plans until Sunday.",
  "platforms": [
    { "platform": "instagram", "accountId": "6710c3e2a9f14b0d8e5c7a22" },
    { "platform": "linkedin", "accountId": "6710c3e2a9f14b0d8e5c7a21" }
  ],
  "mediaItems": [{ "type": "image", "url": "https://pozy.ai/media/temp/1730289600_z1x5_sale.jpg" }],
  "scheduledFor": "2026-11-20T10:00:00",
  "timezone": "Europe/Madrid",
  "requiresApproval": true
}
```

A common setup: writers and AI agents create posts with keys that always set `requiresApproval`, and a person reviews the list each morning with `GET /v1/posts?status=pending_approval`.

## Approve or reject

`POST /v1/posts/{postId}/review` takes one field, `decision`, which is `approve` or `reject`:

```bash cURL
curl -X POST https://api.pozy.ai/v1/posts/6721a8c04e9b2f1d7c3a5e88/review \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "decision": "approve" }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/posts/6721a8c04e9b2f1d7c3a5e88/review", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ decision: "approve" }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.post.status); // "scheduled"
```
```python Python
resp = requests.post(
    "https://api.pozy.ai/v1/posts/6721a8c04e9b2f1d7c3a5e88/review",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={"decision": "approve"},
    timeout=30,
)
print(resp.json()["post"]["status"])  # scheduled
```

| Decision | Result |
| --- | --- |
| `approve` | The post becomes `scheduled` for its `scheduledFor` and `post.scheduled` fires. If that time has already passed, it publishes within a couple of minutes. |
| `reject` | The post goes back to `draft`. Edit it and send it for approval again, or delete it. |

Reviewing a post that is not `pending_approval` returns `409`. Approval does not fire `post.scheduled` at creation time, only when the post is approved.

## Who can create, approve and publish

| Caller | Create drafts and posts | Approve or reject | Notes |
| --- | --- | --- | --- |
| Workspace owner, admin or member | Yes | Yes | From the dashboard or the MCP server. |
| Workspace viewer | No | No | Read-only. |
| Read-write API key | Yes | Yes | Restrict it to some profiles with a [profile-scoped key](https://pozy.ai/docs/build/tenant-keys). |
| Read-only API key | No | No | Writes return `403` with `code: "read_only_key"`. |

To keep approval with people only, give automated writers a key that is scoped to the right profiles and always send `requiresApproval: true` from your own code. Keys cannot be forced to send it, so treat it as a rule in your integration.

## Platform drafts are different

Two platforms have a draft of their own, separate from Pozy drafts:

- **TikTok:** `tiktokSettings.draft: true` uploads the video or photos to the creator's TikTok inbox, where they finish and post it in the app.
- **Facebook:** `facebookSettings.draft: true` creates an unpublished Page post.

For Pozy these are published posts: the upload succeeded. See [TikTok](https://pozy.ai/docs/platforms/tiktok) and [Facebook](https://pozy.ai/docs/platforms/facebook).

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`POST /v1/posts`](https://pozy.ai/docs/api/createPost) | Create post |
| [`PUT /v1/posts/{postId}`](https://pozy.ai/docs/api/updatePost) | Update post |
| [`POST /v1/posts/{postId}/review`](https://pozy.ai/docs/api/reviewPost) | Approve or reject a post |
| [`GET /v1/posts`](https://pozy.ai/docs/api/listPosts) | List posts |
| [`DELETE /v1/posts/{postId}`](https://pozy.ai/docs/api/deletePost) | Delete post |

## Webhook events

| Event | When it fires |
| --- | --- |
| [`post.scheduled`](https://pozy.ai/docs/webhooks/events#post-scheduled) | Post scheduled. |
