# Post lifecycle

Every post status and per-platform status, how a post moves between them, what you can do in each, and which webhook fires on each change.

A post has one overall `status` and one `status` per entry in `platforms`. The overall status is a summary of the entries: a post is `published` only when every entry published, and `partial` when some did and some did not. This page lists every status, the moves between them and the webhook that tells you about each move.

## Overall statuses

| Status | What it means |
| --- | --- |
| `draft` | Saved, not going anywhere. The default when you send no scheduling field. |
| `pending_approval` | Saved with `requiresApproval: true`. It never publishes until someone approves it. |
| `scheduled` | Has a future publish time, set by you or by a queue. |
| `publishing` | Short-lived. Pozy is sending it to the platforms right now. |
| `published` | Every entry published. |
| `partial` | Some entries published, at least one failed. |
| `failed` | Every entry failed. |
| `cancelled` | Nothing will publish: either its account was disconnected before the publish time, or you removed every published copy with [unpublish](https://pozy.ai/docs/publishing/editing#take-a-post-down). |

## How a post moves

The main path is short:

```text
draft ──► scheduled ──► publishing ──┬──► published
                                     ├──► partial
                                     └──► failed
```

Everything else is a side door into or out of that path:

| From | To | What moves it |
| --- | --- | --- |
| `draft` | `scheduled` | An update with `isDraft: false` plus a time, `publishNow` or `queuedFromProfile`. |
| `draft` | `pending_approval` | An update with `requiresApproval: true` and a time. |
| `pending_approval` | `scheduled` | An approval. If the time has passed, it publishes within a couple of minutes. |
| `pending_approval` | `draft` | A rejection. |
| `scheduled` | `cancelled` | The account was disconnected before the publish time. |
| `failed`, `partial` | `scheduled` | A retry. Only the failed entries go out again. |
| `published`, `partial` | `cancelled` | Every published copy was removed with unpublish. Unpublishing some copies leaves the post `partial`. |
| `cancelled` | `scheduled` | An update with a new time. A cancelled post is editable like a draft. |

A `publishNow` post, or one whose `scheduledFor` is already in the past, goes straight from the request to `publishing` and comes back as `published`, `partial` or `failed`.

## Per-platform statuses

Each entry in `post.platforms` has its own status:

| Entry status | What it means |
| --- | --- |
| `pending` | Waiting for the publish time. |
| `processing` | Media is being prepared for this platform. |
| `uploading` | Pozy is uploading to the platform. Some platforms, like TikTok and YouTube, take minutes here. |
| `published` | Live. `platformPostId`, `platformPostUrl` and `publishedAt` are set. |
| `failed` | The platform refused it or it could not be sent. `errorMessage`, `errorCategory` and `errorSource` explain why. |
| `cancelled` | Removed from the platform with unpublish, or cancelled before publishing. |

An entry can be deleted on the platform after it published, for example when the owner removes it in the Instagram app. The entry keeps `status: "published"` and gains `removedFromPlatformAt`, and a `post.platform.deleted` webhook fires. Detection runs in the background roughly every hour, so expect some delay.

## Read the status

```bash cURL
curl https://api.pozy.ai/v1/posts/6721a8c04e9b2f1d7c3a5e88 \
  -H "Authorization: Bearer $POZY_API_KEY"
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/posts/6721a8c04e9b2f1d7c3a5e88", {
  headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);

for (const entry of data.post.platforms) {
  console.log(entry.platform, entry.status, entry.errorCategory ?? "");
}
```
```python Python
resp = requests.get(
    "https://api.pozy.ai/v1/posts/6721a8c04e9b2f1d7c3a5e88",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    timeout=30,
)
for entry in resp.json()["post"]["platforms"]:
    print(entry["platform"], entry["status"], entry.get("errorCategory", ""))
```

```json
{
  "post": {
    "_id": "6721a8c04e9b2f1d7c3a5e88",
    "status": "partial",
    "scheduledFor": "2026-11-03T08:30:00.000Z",
    "platforms": [
      {
        "platform": "linkedin",
        "accountId": "6710c3e2a9f14b0d8e5c7a21",
        "status": "published",
        "platformPostUrl": "https://www.linkedin.com/feed/update/urn:li:share:7254471109021356032"
      },
      {
        "platform": "instagram",
        "accountId": "6710c3e2a9f14b0d8e5c7a22",
        "status": "failed",
        "errorMessage": "The video is too short to publish as a Reel.",
        "errorCategory": "user_content",
        "errorSource": "user"
      }
    ]
  }
}
```

For anything that changes after creation, prefer webhooks to polling. Use `GET /v1/posts` with `updatedSince` to reconcile after downtime.

## Why an entry failed

Failed entries carry a readable `errorMessage` and two fields you can branch on. `errorSource` says who can fix it, and `errorCategory` says what kind of problem it is. Some entries also carry `platformError` with the platform's own `code`, `subcode` and `message`.

| `errorCategory` | What happened | What to do |
| --- | --- | --- |
| `auth_expired` | The account's access was revoked or expired. | Ask the owner to reconnect. [Account health](https://pozy.ai/docs/connect/account-health) spots this before a post fails. |
| `user_content` | The post breaks a platform rule: length, format, media size or duration. | Fix the content, then retry or update the post. |
| `user_abuse` | The platform flagged the activity as spam or too frequent. | Slow down and space posts out. |
| `account_issue` | Something on the account blocks it, such as the wrong account type. | Fix it on the platform. |
| `platform_rejected` | The platform refused the content on policy grounds. | Change the content. |
| `platform_error` | The platform had an outage or a server error. | Retry later. |
| `platform_rate_limit` | The platform is throttling requests. | Pozy retries these on its own. |
| `quota_exhausted` | A daily platform quota ran out. | It works again after the platform's quota resets. |
| `system_error` | Something failed on Pozy's side. | Retry. Contact support@pozy.ai if it keeps happening. |
| `unknown` | Not classified. | Read `errorMessage`. |

| `errorSource` | Meaning |
| --- | --- |
| `user` | You or your user must change something before a retry can work. |
| `platform` | A platform-side problem. Retrying later usually works. |
| `system` | A Pozy-side problem. Retrying usually works. |

## Retry a failed post

`POST /v1/posts/{postId}/retry` publishes the failed entries of a `failed` or `partial` post again. Entries that already published are left alone, so a retry never double-posts.

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

Fix `user` errors before you retry, otherwise the same error comes back. For a content fix, update the post with `PUT /v1/posts/{postId}` first, then retry.

## What you can do in each status

| Action | Allowed when the post is | Notes |
| --- | --- | --- |
| [Update](https://pozy.ai/docs/publishing/editing) (`PUT /v1/posts/{postId}`) | `draft`, `scheduled`, `failed`, `partial`, `cancelled` | On a `published` post only the `recycling` settings can change. |
| Approve or reject (`POST .../review`) | `pending_approval` | Any other status returns `409`. |
| Retry (`POST .../retry`) | `failed`, `partial` | Only failed entries are sent again. |
| Edit published text (`POST .../edit`) | `published`, `partial` | Text only, on platforms that allow edits. |
| Unpublish (`POST .../unpublish`) | `published`, `partial` | Removes the copy from one platform. |
| Delete (`DELETE /v1/posts/{postId}`) | Anything except `published` | Removes the record from Pozy. It does not touch the platform and sends no webhook. |

## Webhooks for each change

| Event | Fires when |
| --- | --- |
| `post.scheduled` | The post enters `scheduled`: created with a time, added to a queue, a draft promoted, an approval, or a retry. Editing the time of a post that is already scheduled does not fire it again. |
| `post.platform.published` | One entry published. Fires per entry, without waiting for the others. |
| `post.platform.failed` | One entry failed for good. Errors Pozy is still retrying stay quiet. |
| `post.published` | Every entry published. |
| `post.partial` | Some entries published, others failed. |
| `post.failed` | Every entry failed. |
| `post.cancelled` | A scheduled post was cancelled because its account was disconnected and nothing had published. |
| `post.recycled` | A recycling schedule created a new copy of a published post. |
| `post.tiktok.url_resolved` | A published TikTok entry got its public URL. TikTok assigns it a few minutes after upload, so `post.published` can arrive with an empty TikTok URL. |
| `post.platform.deleted` | A published entry was found deleted on the platform. |

Exactly one of `post.published`, `post.partial` and `post.failed` fires each time a post publishes. Per-entry events usually arrive first, but do not rely on the order: see [Webhook idempotency and ordering](https://pozy.ai/docs/webhooks/idempotency).

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/posts/{postId}`](https://pozy.ai/docs/api/getPost) | Get post |
| [`GET /v1/posts`](https://pozy.ai/docs/api/listPosts) | List posts |
| [`POST /v1/posts/{postId}/retry`](https://pozy.ai/docs/api/retryPost) | Retry failed post |
| [`POST /v1/posts/{postId}/review`](https://pozy.ai/docs/api/reviewPost) | Approve or reject a post |
| [`PUT /v1/posts/{postId}`](https://pozy.ai/docs/api/updatePost) | Update post |
| [`DELETE /v1/posts/{postId}`](https://pozy.ai/docs/api/deletePost) | Delete post |
| [`POST /v1/posts/{postId}/unpublish`](https://pozy.ai/docs/api/unpublishPost) | Unpublish post |
| [`POST /v1/posts/{postId}/edit`](https://pozy.ai/docs/api/editPost) | Edit published post |

## Webhook events

| Event | When it fires |
| --- | --- |
| [`post.scheduled`](https://pozy.ai/docs/webhooks/events#post-scheduled) | Post scheduled. |
| [`post.published`](https://pozy.ai/docs/webhooks/events#post-published) | Post published. |
| [`post.failed`](https://pozy.ai/docs/webhooks/events#post-failed) | Post failed. |
| [`post.partial`](https://pozy.ai/docs/webhooks/events#post-partial) | Post partial. |
| [`post.cancelled`](https://pozy.ai/docs/webhooks/events#post-cancelled) | Post cancelled. |
| [`post.recycled`](https://pozy.ai/docs/webhooks/events#post-recycled) | Post recycled. |
| [`post.platform.published`](https://pozy.ai/docs/webhooks/events#post-platform-published) | Post platform published. |
| [`post.platform.failed`](https://pozy.ai/docs/webhooks/events#post-platform-failed) | Post platform failed. |
| [`post.platform.deleted`](https://pozy.ai/docs/webhooks/events#post-platform-deleted) | Post platform deleted. |
| [`post.tiktok.url_resolved`](https://pozy.ai/docs/webhooks/events#post-tiktok-url-resolved) | TikTok post URL resolved. |
