Publishing
One request publishes or schedules a post to up to 50 accounts on any mix of networks. postpeg checks it against each network first, then publishes each account separately and reports back per account.
Create a post
POST/v1/posts
| Field | Description |
|---|---|
account_idsstring[], 1–50 · required | Account ids from GET /v1/accounts. Each must be active. |
contentstring | The text. Counted in characters as people see them (an emoji is one). Each network’s limit applies; see Platforms. |
mediaobject[], up to 20 | { url, type, alt_text? }. url must be a public https URL the network can fetch; type is image or video; alt_text is up to 2,000 characters. |
scheduled_atISO 8601 with offset | When to publish, up to a year ahead. Leave out to publish now. |
platform_optionsobject | Extra settings some networks need. See Platform options. |
A post needs text, media, or both.
curl https://api.postpeg.com/v1/posts \
-H "Authorization: Bearer $POSTPEG_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: changelog-2026-10-01" \
-d '{
"account_ids": [
"acc_mfz1x0k27d4q8vbn3c5s0a1p9e",
"acc_mfz1y4r9h2c6t0wqk8e3m7ld5f",
"acc_mfz20b5n1s8f4j7xv2p9r6hu1c"
],
"content": "Scheduled posts now retry on their own when a network has a bad minute. Changelog: https://example.com/changelog #buildinpublic",
"media": [
{
"url": "https://example.com/changelog.png",
"type": "image"
}
],
"scheduled_at": "2026-10-01T09:00:00Z"
}'{
"id": "post_mfz2k3v8q1w5e9r4t7y0u2i6o3",
"content": "Scheduled posts now retry on their own when a network has a bad minute. Changelog: https://example.com/changelog #buildinpublic",
"media": [
{
"url": "https://example.com/changelog.png",
"type": "image"
}
],
"platform_options": {},
"status": "scheduled",
"scheduled_at": "2026-10-01T09:00:00.000Z",
"targets": [
{
"account_id": "acc_mfz1x0k27d4q8vbn3c5s0a1p9e",
"platform": "x",
"status": "pending",
"platform_post_id": null,
"url": null,
"error": null,
"attempts": 0,
"published_at": null
},
{
"account_id": "acc_mfz1y4r9h2c6t0wqk8e3m7ld5f",
"platform": "linkedin",
"status": "pending",
"platform_post_id": null,
"url": null,
"error": null,
"attempts": 0,
"published_at": null
},
{
"account_id": "acc_mfz20b5n1s8f4j7xv2p9r6hu1c",
"platform": "instagram",
"status": "pending",
"platform_post_id": null,
"url": null,
"error": null,
"attempts": 0,
"published_at": null
}
],
"created_at": "2026-09-24T14:02:11.418Z"
}Now or later
Without scheduled_at, or with a time in the past, the post is queued straight away: the response has status: "publishing" and targets queued. With a future time the post is scheduled and its targets wait as pending until then. Scheduled posts go out within a minute of their time.
Validation
Before anything is stored or sent, the post is checked against every network it targets: text length, required media, video-only networks, image and video counts, and required options. Every problem comes back at once in issues, each with the path of the field to fix. Nothing is published, on any network, until the request is valid.
curl https://api.postpeg.com/v1/posts \
-H "Authorization: Bearer $POSTPEG_KEY" \
-H "Content-Type: application/json" \
-d '{
"account_ids": [
"acc_mfz1x0k27d4q8vbn3c5s0a1p9e",
"acc_mfz22d1q5w8e2r6t0y4u7i3o9s",
"acc_mfz23e4a7s1d5f9g3h6j0k2l8z"
],
"content": "Big week. Scheduled posts now retry on their own, the dashboard shows every target’s status, and cancelling a post is one call. We also rewrote the connect flow so reconnecting an expired login keeps the same account id. Full changelog and upgrade notes: https://example.com/changelog #buildinpublic #api",
"media": [
{
"url": "https://example.com/changelog.png",
"type": "image"
}
],
"platform_options": {}
}'{
"type": "https://postpeg.com/errors/invalid_request",
"title": "Invalid request",
"status": 400,
"detail": "X: text is 304 characters, the limit is 280; TikTok: accepts video only; YouTube: accepts video only; YouTube: `platform_options.youtube.title` is required",
"code": "invalid_request",
"request_id": "8f14e45f-ceea-467a-9575-1f2a5c3e6b0d",
"issues": [
{
"path": "content",
"message": "X: text is 304 characters, the limit is 280"
},
{
"path": "media",
"message": "TikTok: accepts video only"
},
{
"path": "media",
"message": "YouTube: accepts video only"
},
{
"path": "platform_options.youtube.title",
"message": "YouTube: `platform_options.youtube.title` is required"
}
]
}Accounts are checked too: an unknown or disconnected id gives “No connected account …”, and an account whose login expired gives “Account … must be reconnected first”. Malformed fields (a non-https media URL, an unknown key) are reported the same way, with the field’s path.
Platform options
platform_options holds what a network needs beyond text and media, keyed by network. Unknown keys are rejected, so a typo fails loudly instead of doing nothing.
| Field | Description |
|---|---|
youtube.titlestring, 1–100 · required | Required for YouTube: the video’s title. |
youtube.privacypublic | unlisted | private | Default public. |
pinterest.board_idstring · required | Required for Pinterest. The board to pin to. |
pinterest.titlestring, up to 100 | Pin title. |
pinterest.linkURL | Where the pin links to. |
tiktok.privacypublic | friends | private | Default public. |
google_business.call_to_actionlearn_more | book | order | shop | sign_up | call | Button shown on the post. |
google_business.linkURL | Where the button goes. |
curl https://api.postpeg.com/v1/posts \
-H "Authorization: Bearer $POSTPEG_KEY" \
-H "Content-Type: application/json" \
-d '{
"account_ids": [
"acc_mfz23e4a7s1d5f9g3h6j0k2l8z"
],
"content": "What changed in the scheduler this week, in four minutes.",
"media": [
{
"url": "https://example.com/scheduler-update.mp4",
"type": "video"
}
],
"platform_options": {
"youtube": {
"title": "Scheduler update",
"privacy": "unlisted"
}
}
}'Statuses
Each account a post goes to is a target with its own status. The post’s status is worked out from its targets.
| Field | Description |
|---|---|
pendingtarget | Waiting for its time, or for a retry. |
queuedtarget | Due, waiting for a publisher. |
publishingtarget | Being sent. Videos can stay here while the network processes them. |
publishedtarget | Live. url and platform_post_id are set. |
failedtarget | Not published. error says why. |
canceledtarget | You cancelled it before it went out. |
| Field | Description |
|---|---|
scheduledpost | Targets are waiting for a future time. |
publishingpost | At least one target is on its way. |
publishedpost | Every target published. |
partially_publishedpost | Some targets published, others failed. |
failedpost | No target published. |
canceledpost | Every target was cancelled. |
{
"id": "post_mfz2k3v8q1w5e9r4t7y0u2i6o3",
"content": "Scheduled posts now retry on their own when a network has a bad minute. Changelog: https://example.com/changelog #buildinpublic",
"media": [
{
"url": "https://example.com/changelog.png",
"type": "image"
}
],
"platform_options": {},
"status": "partially_published",
"scheduled_at": "2026-10-01T09:00:00.000Z",
"targets": [
{
"account_id": "acc_mfz1x0k27d4q8vbn3c5s0a1p9e",
"platform": "x",
"status": "published",
"platform_post_id": "1971234567890123456",
"url": "https://x.com/acme/status/1971234567890123456",
"error": null,
"attempts": 1,
"published_at": "2026-10-01T09:00:04.112Z"
},
{
"account_id": "acc_mfz1y4r9h2c6t0wqk8e3m7ld5f",
"platform": "linkedin",
"status": "published",
"platform_post_id": "urn:li:share:7378123456789012345",
"url": "https://www.linkedin.com/feed/update/urn:li:share:7378123456789012345",
"error": null,
"attempts": 1,
"published_at": "2026-10-01T09:00:05.870Z"
},
{
"account_id": "acc_mfz20b5n1s8f4j7xv2p9r6hu1c",
"platform": "instagram",
"status": "failed",
"platform_post_id": null,
"url": null,
"error": "The account must be reconnected",
"attempts": 1,
"published_at": null
}
],
"created_at": "2026-09-24T14:02:11.418Z"
}Retries
When a network fails in a way that may pass (a timeout, an outage, a rate limit), the target goes back to pending and is retried, up to 5 attempts in all, waiting 2, 4, 8 and 16 minutes between them, or longer if the network asks. While it waits, error shows the last failure and attempts counts the tries. A refusal that won’t change on retry fails straight away.
If a publish is interrupted after the request may have reached the network, the target is marked failed rather than retried: a duplicate post is worse than a missing one. If a login expired, the account becomes reconnect_required.
Cancelling
DELETE/v1/posts/{id}
Cancels every target that hasn’t started publishing and returns the post. Targets already publishing or published are left as they are. If nothing is left to cancel you get a 409.
curl -X DELETE https://api.postpeg.com/v1/posts/post_mfz2k3v8q1w5e9r4t7y0u2i6o3 \
-H "Authorization: Bearer $POSTPEG_KEY"{
"id": "post_mfz2k3v8q1w5e9r4t7y0u2i6o3",
"content": "Scheduled posts now retry on their own when a network has a bad minute. Changelog: https://example.com/changelog #buildinpublic",
"media": [
{
"url": "https://example.com/changelog.png",
"type": "image"
}
],
"platform_options": {},
"status": "canceled",
"scheduled_at": "2026-10-01T09:00:00.000Z",
"targets": [
{
"account_id": "acc_mfz1x0k27d4q8vbn3c5s0a1p9e",
"platform": "x",
"status": "canceled",
"platform_post_id": null,
"url": null,
"error": null,
"attempts": 0,
"published_at": null
},
{
"account_id": "acc_mfz1y4r9h2c6t0wqk8e3m7ld5f",
"platform": "linkedin",
"status": "canceled",
"platform_post_id": null,
"url": null,
"error": null,
"attempts": 0,
"published_at": null
},
{
"account_id": "acc_mfz20b5n1s8f4j7xv2p9r6hu1c",
"platform": "instagram",
"status": "canceled",
"platform_post_id": null,
"url": null,
"error": null,
"attempts": 0,
"published_at": null
}
],
"created_at": "2026-09-24T14:02:11.418Z"
}Idempotency keys
Send an Idempotency-Key header (up to 255 characters) with POST /v1/posts. If a request with the same key already created a post, you get that post back with 200 instead of 201, and nothing new is published. Use one key per post you mean to create, such as your own record’s id, so a timeout or a retry on your side never double-posts.
Listing posts
GET/v1/posts
Newest first. limit is 1–100 (default 20); filter with status. When there are more, pass next_cursor back as cursor; it’s null on the last page. With a status filter a page can hold fewer than limit posts, so keep following the cursor until it’s null.
curl https://api.postpeg.com/v1/posts?limit=1 \
-H "Authorization: Bearer $POSTPEG_KEY"{
"data": [
{
"id": "post_mfz2k3v8q1w5e9r4t7y0u2i6o3",
"content": "Scheduled posts now retry on their own when a network has a bad minute. Changelog: https://example.com/changelog #buildinpublic",
"media": [
{
"url": "https://example.com/changelog.png",
"type": "image"
}
],
"platform_options": {},
"status": "published",
"scheduled_at": "2026-10-01T09:00:00.000Z",
"targets": [
{
"account_id": "acc_mfz1x0k27d4q8vbn3c5s0a1p9e",
"platform": "x",
"status": "published",
"platform_post_id": "1971234567890123456",
"url": "https://x.com/acme/status/1971234567890123456",
"error": null,
"attempts": 1,
"published_at": "2026-10-01T09:00:04.112Z"
},
{
"account_id": "acc_mfz1y4r9h2c6t0wqk8e3m7ld5f",
"platform": "linkedin",
"status": "published",
"platform_post_id": "urn:li:share:7378123456789012345",
"url": "https://www.linkedin.com/feed/update/urn:li:share:7378123456789012345",
"error": null,
"attempts": 1,
"published_at": "2026-10-01T09:00:05.870Z"
},
{
"account_id": "acc_mfz20b5n1s8f4j7xv2p9r6hu1c",
"platform": "instagram",
"status": "published",
"platform_post_id": "18061234567890123",
"url": "https://www.instagram.com/p/DPq7xYzAbCd/",
"error": null,
"attempts": 1,
"published_at": "2026-10-01T09:00:09.304Z"
}
],
"created_at": "2026-09-24T14:02:11.418Z"
}
],
"next_cursor": "MjAyNi0wOS0yNFQxNDowMjoxMS40MThafHBvc3RfbWZ6MmszdjhxMXc1ZTlyNHQ3eTB1Mmk2bzM"
}