API docs

One endpoint. Every platform.

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

Every request carries a bearer key

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.

Authorization header
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/posts

One 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.

request
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"
  }'

Request fields

  • 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.

Response fields

  • 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.
201 Created
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

Presign, upload, confirm

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.

POST /v1/media/presign
# 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 }
PUT the file, then POST /v1/media/{id}/confirm
# 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"
}

Legacy multipart upload

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

Read the header. Trust the code.

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.

low-credit warning
{
  "posts": [ ... ],
  "warning": { "code": "low_credits", "remaining": 12 }
}

Hard freeze at zero

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.

Error envelope

Every failure returns the same two-field shape. Branch on code, which is stable; error is a human-readable message and may change.

400 Bad Request
{
  "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

The same account, wired into your agent

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).

claude_desktop_config.json
{
  "mcpServers": {
    "publish-now": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "https://publishnow.app/mcp"
      ]
    }
  }
}
or, from the terminal
claude mcp add --transport http publish-now https://publishnow.app/mcp

Tools it exposes

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

Built for agents to read on their own

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.

Get a key. Ship the first post.

No card. Cancel anytime. First posts in minutes.

Start free for 5 days