Publishing
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).
#Publish your first post
This request publishes a text post to one LinkedIn account straight away:
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
}'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);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:
{
"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. |
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. |
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. |
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. |
dryRun |
boolean | Check TikTok publishing eligibility without saving anything. See 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. |
tiktokSettings, facebookSettings |
object | Settings applied to every TikTok or Facebook entry. See 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. |
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 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.
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:
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" }
}
]
}'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"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. |
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:
{
"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.
#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.publishedandpost.platform.failedfire per account as soon as each one finishes.post.published,post.partialorpost.failedfires 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.
#Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/posts |
List posts |
POST /v1/posts |
Create post |
GET /v1/posts/{postId} |
Get post |
PUT /v1/posts/{postId} |
Update post |
DELETE /v1/posts/{postId} |
Delete post |
POST /v1/posts/bulk-upload |
Bulk upload from CSV |
POST /v1/posts/{postId}/retry |
Retry failed post |
POST /v1/posts/{postId}/review |
Approve or reject a post |
POST /v1/posts/{postId}/unpublish |
Unpublish post |
POST /v1/posts/{postId}/pin |
Pin post |
POST /v1/posts/{postId}/unpin |
Unpin post |
POST /v1/posts/{postId}/edit |
Edit published post |
POST /v1/posts/{postId}/update-metadata |
Update post metadata |
#Webhook events
| Event | When it fires |
|---|---|
post.scheduled |
Post scheduled. |
post.published |
Post published. |
post.failed |
Post failed. |
post.partial |
Post partial. |
post.cancelled |
Post cancelled. |
post.recycled |
Post recycled. |
post.platform.published |
Post platform published. |
post.platform.failed |
Post platform failed. |
post.platform.deleted |
Post platform deleted. |
post.tiktok.url_resolved |
TikTok post URL resolved. |
post.tracking_completed |
Post tracking completed. |
post.authenticity_changed |
Post authenticity changed. |
post.external.created |
External post created. |
post.external.updated |
External post updated. |
post.external.deleted |
External post deleted. |