# Create a post

Publish, schedule, queue or save a post to one or many connected accounts with a single POST /v1/posts request.

Every post in Pozy starts with one request to `POST /v1/posts`. The same body publishes right away, schedules for later, drops into a queue or saves a draft, and it can target any mix of connected accounts. This page walks through that body field by field.

You need an API key and at least one connected account. `GET /v1/accounts` lists the account ids you can post to ([Connecting accounts](https://pozy.ai/docs/connect)).

## Publish your first post

This request publishes a text post to one LinkedIn account straight away:

```bash cURL
curl -X POST https://api.pozy.ai/v1/posts \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "We just shipped scheduled exports. Details on the blog.",
    "platforms": [
      { "platform": "linkedin", "accountId": "6710c3e2a9f14b0d8e5c7a21" }
    ],
    "publishNow": true
  }'
```
```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: "We just shipped scheduled exports. Details on the blog.",
    platforms: [{ platform: "linkedin", accountId: "6710c3e2a9f14b0d8e5c7a21" }],
    publishNow: true,
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);

console.log(data.post.status, data.post.platforms[0].platformPostUrl);
```
```python Python
import os
import requests

resp = requests.post(
    "https://api.pozy.ai/v1/posts",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "content": "We just shipped scheduled exports. Details on the blog.",
        "platforms": [{"platform": "linkedin", "accountId": "6710c3e2a9f14b0d8e5c7a21"}],
        "publishNow": True,
    },
    timeout=60,
)
post = resp.json()["post"]
print(post["status"], post["platforms"][0].get("platformPostUrl"))
```

With `publishNow`, the request waits for the platform to answer, so the response already holds the result and the public URL:

```json
{
  "post": {
    "_id": "6721a8c04e9b2f1d7c3a5e88",
    "content": "We just shipped scheduled exports. Details on the blog.",
    "status": "published",
    "platforms": [
      {
        "platform": "linkedin",
        "accountId": "6710c3e2a9f14b0d8e5c7a21",
        "status": "published",
        "platformPostId": "urn:li:share:7254471109021356032",
        "platformPostUrl": "https://www.linkedin.com/feed/update/urn:li:share:7254471109021356032",
        "publishedAt": "2026-10-30T10:02:11.000Z"
      }
    ],
    "createdAt": "2026-10-30T10:02:09.000Z"
  }
}
```

## The request body

These fields sit at the top level of the body. Everything except `platforms` is optional, and `platforms` itself is optional for drafts.

| Field | Type | What it does |
| --- | --- | --- |
| `content` | string | The text of the post. Optional when you attach media or every platform entry has its own `customContent`. |
| `mediaItems` | array | Images, videos, GIFs or documents shared by every platform entry. See [Media](https://pozy.ai/docs/publishing/media). |
| `platforms` | array | One entry per account to publish to. See the next section. |
| `publishNow` | boolean | Publish inside this request. |
| `scheduledFor` | date-time | When to publish. Read with `timezone`. See [Scheduling and timezones](https://pozy.ai/docs/publishing/scheduling). |
| `timezone` | string | IANA zone name such as `Europe/Madrid`. Defaults to `UTC`. |
| `queuedFromProfile` | string | A profile id. The post takes the next free slot in that profile's queue. See [Queues](https://pozy.ai/docs/publishing/queues). |
| `queueId` | string | Pick a specific queue of that profile instead of its default one. |
| `isDraft` | boolean | Save without publishing. Wins over every scheduling field. |
| `requiresApproval` | boolean | Hold the post in `pending_approval` until someone approves it. See [Drafts and approvals](https://pozy.ai/docs/publishing/drafts-approvals). |
| `dryRun` | boolean | Check TikTok publishing eligibility without saving anything. See [Validation](https://pozy.ai/docs/publishing/validation). |
| `title` | string | A title stored with the post. Platform titles (YouTube, Pinterest, Reddit) go in that platform's `platformSpecificData`. |
| `tags` | array of strings | Your own labels, for filtering with `GET /v1/posts?tags=`. They are never published. |
| `hashtags`, `mentions` | array of strings | Stored on the post for reporting. To publish a hashtag or mention, write it in the text. |
| `metadata` | object | Free-form data you want back later, such as your own record id. Echoed in every post webhook. |
| `recycling` | object | Republish the post on a repeating schedule. See [Queues](https://pozy.ai/docs/publishing/queues#recycle-evergreen-posts). |
| `tiktokSettings`, `facebookSettings` | object | Settings applied to every TikTok or Facebook entry. See [Platform settings](https://pozy.ai/docs/publishing/platform-settings). |

## The platforms array

Each entry in `platforms` sends the post to one connected account. Two entries can point at the same platform when you want to publish to two accounts there.

| Field | Required | What it does |
| --- | --- | --- |
| `platform` | Yes | The platform id: `instagram`, `tiktok`, `youtube`, `facebook`, `linkedin`, `twitter` (X), `threads`, `pinterest`, `bluesky`, `googlebusiness`, `snapchat`, `telegram`, `discord`, `slack` and others. |
| `accountId` | Yes | The connected account to publish from. |
| `customContent` | No | Replaces `content` for this entry only. |
| `customMedia` | No | Replaces `mediaItems` for this entry only. |
| `scheduledFor` | No | Replaces the top-level `scheduledFor` for this entry only. |
| `platformSpecificData` | No | Options that only this platform understands, such as an Instagram first comment or a YouTube title. The shape depends on `platform`. |

> **Warning:**
> 
> Any account in your workspace is accepted, whichever profile it belongs to. If you publish on behalf of your own customers, only pass account ids you have stored against that customer, or use a [profile-scoped API key](https://pozy.ai/docs/build/tenant-keys) so a mistake is refused.

## Publish now, schedule, queue or draft

The fields you send decide what happens to the post and which status it starts in:

| You send | What happens | First status |
| --- | --- | --- |
| `publishNow: true` | Publishes during the request. The response carries the result. | `published`, `partial` or `failed` |
| `scheduledFor` in the future | Publishes at that time. | `scheduled` |
| `scheduledFor` in the past | Publishes during the request, like `publishNow`. | `published`, `partial` or `failed` |
| `queuedFromProfile` | Takes the next free slot of the profile's queue. | `scheduled` |
| `requiresApproval: true` and a time | Waits for approval, then follows the time. | `pending_approval` |
| `isDraft: true`, or no scheduling field at all | Saved, never published. | `draft` |

When more than one applies, `isDraft` wins over everything, and `publishNow` wins over `scheduledFor`.

> **Tip:**
> 
> A post created with `publishNow` can fail on one platform and succeed on another. The request still returns a 2xx response (207 when at least one platform failed), so always branch on `post.status` and on each `platforms[].status` instead of on whether the call threw.

## Post to several platforms at once

One request can carry different text, media and options per platform. Here LinkedIn gets the long version, X gets a short one, and Instagram gets a square image and a first comment:

```bash cURL
curl -X POST https://api.pozy.ai/v1/posts \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: launch-exports-2026-11-03" \
  -d '{
    "content": "Scheduled exports are live. Pick a time, pick a format, and the file lands in your inbox every week.",
    "mediaItems": [{ "type": "image", "url": "https://pozy.ai/media/temp/1730289600_k3f9_exports-wide.jpg" }],
    "scheduledFor": "2026-11-03T09:30:00",
    "timezone": "Europe/Madrid",
    "metadata": { "campaignId": "cmp_2041" },
    "platforms": [
      { "platform": "linkedin", "accountId": "6710c3e2a9f14b0d8e5c7a21" },
      {
        "platform": "twitter",
        "accountId": "6710c3e2a9f14b0d8e5c7a23",
        "customContent": "Scheduled exports are live. Weekly files, zero clicks."
      },
      {
        "platform": "instagram",
        "accountId": "6710c3e2a9f14b0d8e5c7a22",
        "customMedia": [{ "type": "image", "url": "https://pozy.ai/media/temp/1730289600_m2p7_exports-square.jpg" }],
        "platformSpecificData": { "firstComment": "Setup guide: link in bio" }
      }
    ]
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/posts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Idempotency-Key": "launch-exports-2026-11-03",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    content: "Scheduled exports are live. Pick a time, pick a format, and the file lands in your inbox every week.",
    mediaItems: [{ type: "image", url: "https://pozy.ai/media/temp/1730289600_k3f9_exports-wide.jpg" }],
    scheduledFor: "2026-11-03T09:30:00",
    timezone: "Europe/Madrid",
    metadata: { campaignId: "cmp_2041" },
    platforms: [
      { platform: "linkedin", accountId: "6710c3e2a9f14b0d8e5c7a21" },
      {
        platform: "twitter",
        accountId: "6710c3e2a9f14b0d8e5c7a23",
        customContent: "Scheduled exports are live. Weekly files, zero clicks.",
      },
      {
        platform: "instagram",
        accountId: "6710c3e2a9f14b0d8e5c7a22",
        customMedia: [{ type: "image", url: "https://pozy.ai/media/temp/1730289600_m2p7_exports-square.jpg" }],
        platformSpecificData: { firstComment: "Setup guide: link in bio" },
      },
    ],
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);

console.log(data.post._id, data.post.status); // "scheduled"
```
```python Python
resp = requests.post(
    "https://api.pozy.ai/v1/posts",
    headers={
        "Authorization": f"Bearer {os.environ['POZY_API_KEY']}",
        "Idempotency-Key": "launch-exports-2026-11-03",
    },
    json={
        "content": "Scheduled exports are live. Pick a time, pick a format, and the file lands in your inbox every week.",
        "mediaItems": [{"type": "image", "url": "https://pozy.ai/media/temp/1730289600_k3f9_exports-wide.jpg"}],
        "scheduledFor": "2026-11-03T09:30:00",
        "timezone": "Europe/Madrid",
        "metadata": {"campaignId": "cmp_2041"},
        "platforms": [
            {"platform": "linkedin", "accountId": "6710c3e2a9f14b0d8e5c7a21"},
            {
                "platform": "twitter",
                "accountId": "6710c3e2a9f14b0d8e5c7a23",
                "customContent": "Scheduled exports are live. Weekly files, zero clicks.",
            },
            {
                "platform": "instagram",
                "accountId": "6710c3e2a9f14b0d8e5c7a22",
                "customMedia": [{"type": "image", "url": "https://pozy.ai/media/temp/1730289600_m2p7_exports-square.jpg"}],
                "platformSpecificData": {"firstComment": "Setup guide: link in bio"},
            },
        ],
    },
    timeout=30,
)
print(resp.json()["post"]["status"])  # "scheduled"
```

Per-entry fields replace the top-level ones for that entry only. The LinkedIn entry above uses the shared text and image, the X entry swaps the text and keeps the image, and the Instagram entry keeps the text and swaps the image.

## Read the response

The response holds the saved post. The fields you will use most:

| Field | Meaning |
| --- | --- |
| `post._id` | The post id. Use it for every later call: get, update, retry, unpublish. |
| `post.status` | The overall status. See [Post lifecycle](https://pozy.ai/docs/publishing/lifecycle). |
| `post.scheduledFor` | The publish time as a UTC instant, ending in `Z`. |
| `post.platforms[].status` | Status of each entry: `pending`, `processing`, `uploading`, `published`, `failed` or `cancelled`. |
| `post.platforms[].platformPostUrl` | Public URL once that entry is published. |
| `post.platforms[].errorMessage`, `errorCategory` | Why an entry failed. |
| `warnings` | Things Pozy changed or ignored, such as a field placed on the wrong platform entry. The post was still saved. |

Read `warnings` in development. A field put in the wrong place is dropped rather than rejected, and the warning is the only sign.

## Avoid duplicate posts

Networks and job runners retry. To make a retry safe, send an `Idempotency-Key` header (up to 255 characters) that is unique per logical post. A second request with the same key within 24 hours returns the original post instead of creating a new one, even if the body changed.

Separately, Pozy refuses the same text and media sent to the same account twice within 24 hours with a `409`. The error names the existing post so you can link to it:

```json
{
  "error": "This exact content is already scheduled, publishing, or was posted to this account within the last 24 hours.",
  "details": {
    "accountId": "6710c3e2a9f14b0d8e5c7a21",
    "platform": "linkedin",
    "existingPostId": "6721a8c04e9b2f1d7c3a5e88"
  }
}
```

To repost on purpose, change the text, the media or the account. More on retries in [Idempotency](https://pozy.ai/docs/getting-started/idempotency).

## Find out what happened

A scheduled or queued post publishes later, so the create response cannot tell you the outcome. Subscribe to post webhooks instead of polling:

- `post.platform.published` and `post.platform.failed` fire per account as soon as each one finishes.
- `post.published`, `post.partial` or `post.failed` fires once, after every account has finished.

Put your own id in `metadata` when you create the post and it comes back in each of those events, so your handler never has to look the post up. See [Set up webhooks](https://pozy.ai/docs/webhooks).

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/posts`](https://pozy.ai/docs/api/listPosts) | List posts |
| [`POST /v1/posts`](https://pozy.ai/docs/api/createPost) | Create post |
| [`GET /v1/posts/{postId}`](https://pozy.ai/docs/api/getPost) | Get 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/bulk-upload`](https://pozy.ai/docs/api/bulkUploadPosts) | Bulk upload from CSV |
| [`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 |
| [`POST /v1/posts/{postId}/unpublish`](https://pozy.ai/docs/api/unpublishPost) | Unpublish post |
| [`POST /v1/posts/{postId}/pin`](https://pozy.ai/docs/api/pinPost) | Pin post |
| [`POST /v1/posts/{postId}/unpin`](https://pozy.ai/docs/api/unpinPost) | Unpin post |
| [`POST /v1/posts/{postId}/edit`](https://pozy.ai/docs/api/editPost) | Edit published post |
| [`POST /v1/posts/{postId}/update-metadata`](https://pozy.ai/docs/api/updatePostMetadata) | Update post metadata |

## 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. |
| [`post.tracking_completed`](https://pozy.ai/docs/webhooks/events#post-tracking-completed) | Post tracking completed. |
| [`post.authenticity_changed`](https://pozy.ai/docs/webhooks/events#post-authenticity-changed) | Post authenticity changed. |
| [`post.external.created`](https://pozy.ai/docs/webhooks/events#post-external-created) | External post created. |
| [`post.external.updated`](https://pozy.ai/docs/webhooks/events#post-external-updated) | External post updated. |
| [`post.external.deleted`](https://pozy.ai/docs/webhooks/events#post-external-deleted) | External post deleted. |
