Create Post
Publish content to one or more social platforms with a single API call.
Endpoint
POST https://api.postpeer.dev/v1/postsRequest Body
To save a post without publishing or scheduling it, send the normal create-post request with saveAsDraft: true; drafts require content or media and at least one platform target, consume no credits, and can later be updated or published with PUT /v1/posts/:postId.
| Field | Type | Required | Description |
|---|---|---|---|
content | string | Yes | The text body of your post |
publishNow | boolean | Yes* | Set to true to publish immediately |
saveAsDraft | boolean | No | Set to true to save without publishing or scheduling; cannot be combined with publishNow: true or scheduledFor. |
scheduledFor | string | No | ISO 8601 datetime for scheduled posts |
timezone | string | No | IANA timezone (default: "UTC") |
idempotencyKey | string | No | Unique key to safely retry the request. See Idempotency. |
*Required when neither scheduledFor nor saveAsDraft: true is provided.
platforms (required)
An array of platform targets (min 1).
| Field | Type | Required | Description |
|---|---|---|---|
platform | string | Yes | One of the supported platform values below |
accountId | string | Yes | Integration ID from /v1/connect/integrations |
content | string | No | Per-platform text override. When set, this replaces the top-level content for this platform only. Useful when different platforms call for different copy (e.g. hashtags on Instagram, shorter on Twitter). |
platformSpecificData | object | No | Platform-specific options (see Platforms) |
Supported values: "twitter", "instagram", "youtube", "tiktok", "pinterest", "linkedin", "bluesky", "facebook", "threads", "googlebusiness".
mediaItems (optional)
An array of media attachments.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | "image", "video", or "gif" |
url | string | Yes | Public URL to the media file |
thumbnail | string | No | Video thumbnail URL (Facebook and YouTube) |
Example: Publish Now
curl -X POST "https://api.postpeer.dev/v1/posts" \
-H "x-access-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Launching our new feature today!",
"platforms": [
{ "platform": "twitter", "accountId": "abc123" },
{ "platform": "instagram", "accountId": "def456" }
],
"mediaItems": [
{ "type": "image", "url": "https://example.com/image.png" }
],
"publishNow": true
}'import PostPeer from '@postpeer/node';
const client = new PostPeer();
const { data } = await client.posts.create({
body: {
content: 'Launching our new feature today!',
platforms: [
{ platform: 'twitter', accountId: 'abc123' },
{ platform: 'instagram', accountId: 'def456' },
],
mediaItems: [{ type: 'image', url: 'https://example.com/image.png' }],
publishNow: true,
},
});from postpeer import PostPeer
with PostPeer() as client:
post = client.posts.create(
content="Launching our new feature today!",
platforms=[
{"platform": "twitter", "accountId": "abc123"},
{"platform": "instagram", "accountId": "def456"},
],
media_items=[
{"type": "image", "url": "https://example.com/image.png"},
],
publish_now=True,
)Example: Cross-Post to Multiple Platforms
curl -X POST "https://api.postpeer.dev/v1/posts" \
-H "x-access-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Check out our latest video!",
"platforms": [
{ "platform": "twitter", "accountId": "tw_123" },
{ "platform": "youtube", "accountId": "yt_456", "platformSpecificData": { "title": "Our Latest Video", "visibility": "public" } }
],
"mediaItems": [
{ "type": "video", "url": "https://example.com/video.mp4" }
],
"publishNow": true
}'const { data } = await client.posts.create({
body: {
content: 'Check out our latest video!',
platforms: [
{ platform: 'twitter', accountId: 'tw_123' },
{
platform: 'youtube',
accountId: 'yt_456',
platformSpecificData: {
title: 'Our Latest Video',
visibility: 'public',
},
},
],
mediaItems: [{ type: 'video', url: 'https://example.com/video.mp4' }],
publishNow: true,
},
});post = client.posts.create(
content="Check out our latest video!",
platforms=[
{"platform": "twitter", "accountId": "tw_123"},
{
"platform": "youtube",
"accountId": "yt_456",
"platformSpecificData": {
"title": "Our Latest Video",
"visibility": "public",
},
},
],
media_items=[
{"type": "video", "url": "https://example.com/video.mp4"},
],
publish_now=True,
)Example: Per-Platform Content Override
Each platform entry accepts its own content field. When provided, it replaces the top-level content for that platform only — handy when you want a punchy tweet but a longer caption on Instagram.
curl -X POST "https://api.postpeer.dev/v1/posts" \
-H "x-access-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Shipped: faster scheduling, fewer clicks. 🚀",
"platforms": [
{
"platform": "twitter",
"accountId": "tw_123"
},
{
"platform": "linkedin",
"accountId": "li_789",
"content": "Excited to share that we just shipped a major update to our scheduling engine. The team focused on two things: cutting time-to-publish and reducing the number of clicks it takes to get a post live across multiple channels. If you manage social for a brand or team, I would love your feedback."
}
],
"publishNow": true
}'const { data } = await client.posts.create({
body: {
content: 'Shipped: faster scheduling, fewer clicks. 🚀',
platforms: [
{ platform: 'twitter', accountId: 'tw_123' },
{
platform: 'linkedin',
accountId: 'li_789',
content:
'Excited to share that we just shipped a major update to our scheduling engine. The team focused on two things: cutting time-to-publish and reducing the number of clicks it takes to get a post live across multiple channels. If you manage social for a brand or team, I would love your feedback.',
},
],
publishNow: true,
},
});post = client.posts.create(
content="Shipped: faster scheduling, fewer clicks. 🚀",
platforms=[
{"platform": "twitter", "accountId": "tw_123"},
{
"platform": "linkedin",
"accountId": "li_789",
"content": (
"Excited to share that we just shipped a major update to our "
"scheduling engine. The team focused on two things: cutting "
"time-to-publish and reducing the number of clicks it takes to "
"get a post live across multiple channels. If you manage social "
"for a brand or team, I would love your feedback."
),
},
],
publish_now=True,
)Twitter falls back to the top-level content, while LinkedIn gets its own longer, more professional version.
Response
Publish-now requests return final platform results when the worker completes within the request wait window:
{
"success": true,
"status": "published",
"postId": "post_abc123",
"platforms": [
{
"platform": "twitter",
"success": true,
"platformPostUrl": "https://twitter.com/you/status/123456"
},
{
"platform": "instagram",
"success": true,
"platformPostUrl": "https://instagram.com/p/abc123"
}
]
}If the publish worker is still processing when that wait window expires, the request still returns 202 with the saved postId. Poll GET /v1/posts/{postId} or list posts to read the final platform URLs and errors.
{
"success": true,
"status": "publishing",
"postId": "post_abc123",
"platforms": [
{ "platform": "twitter", "success": true },
{ "platform": "instagram", "success": true }
]
}Status Codes
| Code | Meaning |
|---|---|
200 | Idempotent replay — the original post for this idempotencyKey is returned |
202 | Post accepted — pending, publishing, published, scheduled, partial, or failed |
400 | Validation error (bad request body) |
402 | Not enough credits |
403 | Forbidden |
503 | Publishing or scheduling unavailable |
Idempotency
Publishing is synchronous, so a slow platform can push a request past your HTTP client's timeout. If your client then retries, you risk publishing the same post twice.
To make retries safe, send an idempotencyKey: any unique string you generate per post (a UUID works well). The first request with a given key creates and publishes the post as normal. Any later request that reuses the same key skips publishing entirely and returns the original post's result with a 200 status instead of 202. No extra credits are charged.
The key is scoped to your project and never expires. Use a fresh key for each distinct post; reusing a key always returns the first post it was attached to. The field is fully optional. Leave it out and posts behave exactly as before.
curl -X POST "https://api.postpeer.dev/v1/posts" \
-H "x-access-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Launching our new feature today!",
"platforms": [
{ "platform": "twitter", "accountId": "abc123" }
],
"publishNow": true,
"idempotencyKey": "launch-2026-08-29-tweet"
}'Retrying that exact request (same idempotencyKey) returns the post created by the first call rather than posting again.
Partial Failures
When posting to multiple platforms, some may succeed and others fail. A 202 response with "status": "partial" includes per-platform results:
{
"success": true,
"status": "partial",
"postId": "post_abc123",
"platforms": [
{ "platform": "twitter", "success": true, "platformPostUrl": "..." },
{
"platform": "instagram",
"success": false,
"error": "Image URL is not publicly accessible"
}
]
}Failed platform posts don't consume credits.