API docs
A JSON REST API and an MCP server over the same account. Send one request; we fan it out to X, Instagram, TikTok, LinkedIn, and YouTube, on your schedule. Everything below is copy-pasteable.
01 — Authentication
Create a key on your dashboard — it starts with pn_ and is shown once, so store it somewhere safe. Each key is scoped to a single organization: it can only see and act on that organization's profiles, media, and posts. Rotate or revoke a key at any time and it stops working immediately.
curl -X POST https://publishnow.app/v1/posts \
-H "Authorization: Bearer pn_your_key_here" \
-H "Content-Type: application/json" \
-d '{"content": "Hello!", "profiles": ["my-x-handle"]}'
Send the key over HTTPS only, from your server. Never ship it in a browser bundle or a mobile app.
02 — Publish a post
POST /v1/postsOne call creates one post per profile you name. content and profiles are required; everything else is optional. Omit schedule_at to publish right away, or pass an ISO 8601 timestamp to queue it.
curl -X POST https://publishnow.app/v1/posts \
-H "Authorization: Bearer pn_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"content": "Hello from Publish Now!",
"profiles": ["my-x-handle", "my-ig-handle"],
"schedule_at": "2026-08-01T09:00:00Z",
"media_ids": ["a1b2c3d4-..."],
"post_types": { "instagram": "reel" },
"title": "Optional YouTube title"
}'
content — the post body. Required.profiles — array of profile slugs. Required.schedule_at — ISO 8601 datetime. Publishes now when omitted.media_ids — array of confirmed media IDs.post_types — per-platform overrides, e.g. reel or story on Instagram.title — YouTube title, 100 characters max.posts[].id — the post ID to track it by.posts[].profile — the profile slug it was created for.posts[].connector — the destination platform.posts[].status — where it is in the pipeline.posts[].schedule_at — when it goes out.posts[].created_at — when we accepted it.HTTP/1.1 201 Created
X-Credits-Remaining: 480
{
"posts": [
{
"id": "01J...",
"profile": "my-x-handle",
"connector": "x",
"content": "Hello from Publish Now!",
"status": "scheduled",
"schedule_at": "2026-08-01T09:00:00Z",
"created_at": "2026-07-26T11:04:22.104Z",
"media_ids": ["a1b2c3d4-..."],
"post_type": "tweet"
}
]
}
03 — Upload media
Big files never pass through the API. You ask for a short-lived upload URL, push the bytes straight into object storage, then confirm. Pass a content_hash (SHA-256, lowercase hex) and we deduplicate: if the same bytes are already in your library you get back duplicate: true with the existing media_id and no upload to do.
# 1. Ask for an upload URL
curl -X POST https://publishnow.app/v1/media/presign \
-H "Authorization: Bearer pn_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"filename": "launch.jpg",
"mime_type": "image/jpeg",
"size_bytes": 184320,
"content_hash": "9f86d081...64 hex chars"
}'
# 201 Created
{ "media_id": "a1b2c3d4-...", "upload_url": "https://...", "expires_in": 3600 }
# 2. PUT the bytes straight at the returned URL
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: image/jpeg" \
--data-binary @launch.jpg
# 3. Confirm — the file is only usable after this
curl -X POST https://publishnow.app/v1/media/a1b2c3d4-.../confirm \
-H "Authorization: Bearer pn_your_key_here"
# 200 OK
{
"media_id": "a1b2c3d4-...",
"type": "image",
"mime_type": "image/jpeg",
"size_bytes": 184320,
"name": "launch.jpg",
"description": "",
"tags": "",
"created_at": "2026-07-26T11:02:00.000Z"
}
Posting a multipart/form-data body to POST /v1/media still works and still returns a media_id. It is kept for backward compatibility only — it is slower and caps out on file size, so use the presign flow for anything new.
Limits on the presign flow: 50 MB per image, 50 MB per GIF, 1 GB per video. The legacy multipart endpoint above accepts the same images and GIFs but caps video at 100 MB. Upload URLs expire after one hour. A media file is only usable in media_ids once it has been confirmed.
04 — Credits and errors
Every successful POST /v1/posts comes back with an X-Credits-Remaining header, so your client always knows where it stands without a second call. Drop below the low-credit threshold and the response body also carries a warning.
{
"posts": [ ... ],
"warning": { "code": "low_credits", "remaining": 12 }
}
Credits are consumed when a post actually goes out. Hit zero and publishing stops — queued posts are marked failed with insufficient_credits instead of silently degrading or posting a truncated version. Top up and re-send them. If the organization behind the key has no active plan at all, every call to /v1 returns 402 before anything is created.
Every failure returns the same two-field shape. Branch on code, which is stable; error is a human-readable message and may change.
{
"error": "content is required",
"code": "VALIDATION_ERROR"
}
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | A required field is missing or malformed — content, profiles, schedule_at, title, filename, mime_type, size_bytes. |
| 400 | INVALID_MEDIA | One or more media_ids do not exist in your library. |
| 400 | INVALID_MEDIA_COMBINATION | The attachments break a platform rule (for example more than four images on X). |
| 400 | UNSUPPORTED_MEDIA_TYPE | The mime_type sent to /v1/media/presign is not on the allowed list. |
| 400 | FILE_TOO_LARGE | size_bytes exceeds the per-type limit: 50 MB images, 50 MB GIFs, 1 GB video (100 MB video on the legacy multipart endpoint). |
| 400 | FILE_NOT_UPLOADED | You confirmed before the bytes finished landing in storage. |
| 400 | FILE_SIZE_MISMATCH | The stored object size does not match the declared size_bytes. |
| 401 | UNAUTHORIZED | Missing, malformed, or revoked key — or a key that does not start with pn_. |
| 402 | SUBSCRIPTION_REQUIRED | The key's organization has no active or trialing plan. |
| 404 | NOT_FOUND | The media record does not exist, or it was already confirmed. |
05 — MCP server
We host a Model Context Protocol server at https://publishnow.app/mcp. Point Claude, Cursor, or any MCP client at it and authorize once in the browser — there is no API key to paste, and the client is granted only the scopes you approve (posts:read, posts:write, media:write).
{
"mcpServers": {
"publish-now": {
"command": "npx",
"args": [
"mcp-remote",
"https://publishnow.app/mcp"
]
}
}
}
claude mcp add --transport http publish-now https://publishnow.app/mcp
create_post
Create and schedule posts on X, Instagram, TikTok, or YouTube.
list_profiles
List the connected profiles on your account, optionally filtered by tag.
list_posts
List posts, optionally filtered by handle, platform, or status.
create_media
Request an upload URL for an image, GIF, or video.
confirm_media
Finalize an upload and get back a media_id for create_post.
06 — Machine-readable
If you are an agent, skip the prose. Each of these is served as a plain document, cached, and always current with the endpoints above.
Public pages also advertise these targets in an RFC 8288 Link response header, so a crawler can find them without parsing HTML. Usage is governed by the API terms.
No card. Cancel anytime. First posts in minutes.
Start free for 5 days