Planify
Planify
Get Started
v1.0.0REST · JSON · bearer key

Planify API

Schedule and publish to 9 social platforms from anything that can make an HTTP request — an AI assistant, a workflow tool, your own script. No browser required.

Introduction

Planify schedules and publishes social posts. This API exists so software can do that — an AI assistant acting for a person, a workflow in n8n or Zapier, or a script you wrote yourself.

Every response is JSON. Every error uses one envelope. Field names are snake_case throughout, including the platform settings, so nothing changes case halfway through a request.

Base URL

https://planifyapps.com/api/v1

Auth

Authorization: Bearer <key>

Spec

/openapi.json

MCP server

npx -y @planifyapps/mcp

Publish a post in five calls

Step 2 is the one worth not skipping. It reports the character limit, media rules and required settings for that specific channel — which is the difference between a post that publishes and one that discovers each rule by being rejected.

bash
export PLANIFY_API_KEY="plfy_live_..."
API="https://planifyapps.com/api/v1"

# 1 · find a channel
curl -s "$API/channels" -H "Authorization: Bearer $PLANIFY_API_KEY"

# 2 · learn what it accepts — limits, rules, settings schema, lookups
curl -s "$API/channels/$CHANNEL_ID/settings" -H "Authorization: Bearer $PLANIFY_API_KEY"

# 3 · fetch anything unguessable, e.g. a Pinterest board
curl -s -X POST "$API/channels/$CHANNEL_ID/trigger" \
  -H "Authorization: Bearer $PLANIFY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"method_name":"boards"}'

# 4 · get an image in (the server downloads it for you)
curl -s -X POST "$API/media/from-url" \
  -H "Authorization: Bearer $PLANIFY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/photo.jpg"}'

# 5 · publish
curl -s -X POST "$API/posts" \
  -H "Authorization: Bearer $PLANIFY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"type":"now","content":"Shipped something today.","channel_ids":["'"$CHANNEL_ID"'"]}'

Authentication

Send an API key as a bearer token. Create one in the dashboard under Settings → API.

Keys are shown exactly once and stored only as a SHA-256 hash, so a lost key is replaced rather than recovered — and a database leak cannot be replayed against the API. Keys cannot create other keys, so a leaked key cannot mint more.

http
Authorization: Bearer plfy_live_b5Ihj_NPiuEWh5BQrAz5wAHLmW8mi…

Scopes

A key carries only the scopes it was created with. Notably posts:publish is not granted by default — so a leaked read-write key can prepare drafts but cannot post to your audience.

If you are wiring up an AI assistant and want it to prepare work for your approval rather than publish on its own, issue a key without that scope and everything else still works.

Available scopes

channels:readdefaultList channels, read their posting rules, and run their lookups.
posts:readdefaultRead posts that already exist.
posts:writedefaultCreate drafts and scheduled posts.
posts:publishopt-inPublish immediately. Never granted by default — ask for it explicitly.
media:writedefaultUpload images and video.
analytics:readdefaultRead performance data.

Rate limits

Counted per key, per minute. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; a 429 also carries Retry-After.

Publishing is tightest because it is the one action that cannot be undone.

120
read / min
60
write / min
20
publish / min

Idempotency

Send an Idempotency-Key header on POST /v1/posts wherever a retry is possible. Repeating a request with the same key returns the posts created the first time rather than creating more, and sets idempotent_replay: true.

This matters more for software than for people, because software retries automatically. Generate one key per intended post — a UUID is ideal — and reuse it across retries of that same post.

Errors

Every failure returns the same envelope. Branch on code — it is stable and machine-readable. hint usually names the endpoint that answers the question.

json
{
  "error": {
    "code": "validation_failed",
    "message": "twitter (channel ch_1a2b3c): Content exceeds Twitter limit of 280 characters",
    "hint": "GET /api/v1/channels/:id/settings returns the limits and rules for each channel."
  }
}

Status codes you should handle

401invalid_api_keyMissing, unrecognised or revoked key. Do not retry.
402payment_requiredThe plan does not include this. GET /v1/me reports plan and status.
403insufficient_scopeThe key lacks a needed scope. Do not retry with this key.
409request_in_progressAn identical Idempotency-Key request is still running. Wait and retry.
422validation_failedThe post breaks a channel rule. Read the channel settings.
429rate_limitedHonour Retry-After.
502platform_errorThe social platform refused it. Retrying may help.

Account

get/v1/me

Who this credential is, and what it may do

The first call to make. Reports the plan, the scopes the key carries, and quota remaining.

Deliberately answers even when the plan grants no app access — it is how a caller discovers it is paywalled, rather than inferring that from a 402 somewhere less obvious.

Scopes: none beyond a valid credential.

Rate class: read (120/min)

curl
curl -s "https://planifyapps.com/api/v1/me" \
  -H "Authorization: Bearer $PLANIFY_API_KEY"

Response — 200

user_idstring (uuid)
authobject
planobject
platformsstring[]Platforms this plan may publish to. A channel outside this list is read-only.
featuresobject
usageobjectA null limit means unlimited, and so does a null remaining.
rate_limitsobjectPer-minute ceilings by operation weight, so a client can pace itself.

Channels

get/v1/channels

List connected channels

Channels the credential can reach. "Channel" rather than "account" because that word already means the Planify user, the social login, and the individual Facebook Page.

Channels the plan excludes are returned with available: false rather than hidden — omitting them would be indistinguishable from the account never having been connected.

Scopes: channels:read

Rate class: read (120/min)

curl
curl -s "https://planifyapps.com/api/v1/channels" \
  -H "Authorization: Bearer $PLANIFY_API_KEY"

Response — 200

channelsobject[]
summaryobject
get/v1/channels/{id}/settings

What this channel will accept — the endpoint to read before writing a post

Character and media limits, the hard rules that decide whether a publish succeeds, a JSON Schema for this platform's slice of platform_settings, and the lookups this channel supports.

Reading this first is the difference between a post that publishes and one that discovers each rule by being rejected. The settings schema is generated from the types the publishing code actually reads, so it cannot go stale.

Scopes: channels:read

Rate class: read (120/min)

Parameters

idrequiredstringChannel id from GET /v1/channels.
curl
curl -s "https://planifyapps.com/api/v1/channels/$CHANNEL_ID/settings" \
  -H "Authorization: Bearer $PLANIFY_API_KEY"

Response — 200

idstring (uuid)
platformstring
usernamestring | null
display_namestring | null
availableboolean
unavailable_reasonstring
limitsobject
threadingobject
rulesstring[]Hard requirements first, then style guidance. Prose, written to be read by a model.
settings_schemaobject | nullJSON Schema for this platform's slice of platform_settings.
toolsobject[]
post/v1/channels/{id}/trigger

Run a per-channel lookup

Some platforms need a value that cannot be guessed. Pinterest rejects a pin without board_id, and that id is opaque — so without this an agent cannot post to Pinterest at all.

Which lookups a channel supports is advertised as tools by GET /v1/channels/{id}/settings, so they are discovered rather than hardcoded. POST rather than GET because the arguments are structured and some lookups hit a rate-limited upstream API.

Scopes: channels:read

Rate class: read (120/min)

Parameters

idrequiredstringChannel id.

Body — application/json

method_namestringWhich lookup to run. Listed as tools[].method_name by GET /v1/channels/:id/settings.
methodNamestring
dataobjectArguments for the lookup, matching that tool's input schema.
curl
curl -s -X POST "https://planifyapps.com/api/v1/channels/$CHANNEL_ID/trigger" \
  -H "Authorization: Bearer $PLANIFY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "method_name": "boards" }'

Response — 200

method_namestring
get/v1/channels/{id}/slot

Next free posting slot for a channel

An agent told to "post this sometime good" has no way to pick a time. The user has usually already answered that in the dashboard as a weekly schedule; this is how that answer reaches the agent.

Slots already taken by a scheduled post are skipped, so filling a week does not stack everything on one time. Returns slot: null with a reason when the channel has no schedule — that is not an error.

Scopes: channels:read

Rate class: read (120/min)

Parameters

idrequiredstringChannel id.
?countintegerHow many slots to return.
?afterstring (date-time)Search from a later point than now.
curl
curl -s "https://planifyapps.com/api/v1/channels/$CHANNEL_ID/slot" \
  -H "Authorization: Bearer $PLANIFY_API_KEY"

Response — 200

channel_idstring (uuid)
platformstring
scheduleobject | nullThe weekly schedule these came from, or null when none is set.
slotsstring (date-time)[]Ascending, UTC.
slotstring | nullFirst of slots — what most callers want.
reasonstringPresent when there are no slots to give.
notestringPresent when fewer slots were free than requested.

Media

post/v1/media

Upload one or more files

multipart/form-data with one or more file parts. Multipart rather than base64 JSON because a 100MB video base64-encodes to ~133MB of request body, and every HTTP client already knows how to do this.

Returns public URLs to pass as media_urls when creating a post.

Scopes: media:write

Rate class: write (60/min)

Requires an active subscription (402 otherwise).

Body — multipart/form-data

filefile[]Up to 10 files, each at most 100MB.
curl
curl -s -X POST "https://planifyapps.com/api/v1/media" \
  -H "Authorization: Bearer $PLANIFY_API_KEY" \
  -F "file=@photo.jpg"

Response — 200

mediaobject[]
media_urlsstring (uri)[]Exactly what media_urls on POST /v1/posts expects, so one call feeds the next.
post/v1/media/from-url

Have the server download media from a URL

The common agent shape: an image was just generated somewhere and exists only as a URL, so making the agent download and re-upload it is pure round trip.

Every URL is resolved and checked before any request leaves the server, and again on each redirect hop — private, loopback, link-local and cloud-metadata addresses are refused.

Scopes: media:write

Rate class: write (60/min)

Requires an active subscription (402 otherwise).

Body — application/json

urlstringA single publicly reachable URL to ingest.
urlsstring[]Several URLs to ingest in one call.
curl
curl -s -X POST "https://planifyapps.com/api/v1/media/from-url" \
  -H "Authorization: Bearer $PLANIFY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/photo.jpg" }'

Response — 200

mediaobject[]
media_urlsstring (uri)[]Exactly what media_urls on POST /v1/posts expects, so one call feeds the next.

Posts

post/v1/posts

Create a post — draft, scheduled, or published now

content as a string fans the same copy to every channel in channel_ids. As an array of { channel_id, content, … } it gives per-channel copy, which is what you want when the same idea needs 280 characters on Twitter and 2,200 on Instagram.

type: "now" publishes immediately and requires the posts:publish scope, which keys do not carry unless asked for — so a leaked read-write key cannot post to someone's audience.

Send an Idempotency-Key header wherever a retry is possible. Repeating a request with the same key returns the original posts instead of creating more, and sets idempotent_replay: true.

Scopes: posts:write

Rate class: write (60/min)

Requires an active subscription (402 otherwise).

Parameters

Idempotency-KeystringMakes the call replay-safe. Send one wherever a retry is possible.

Body — application/json

type"draft" | "schedule" | "now"draft saves it, schedule queues it for scheduled_at, now publishes immediately (requires the posts:publish scope).
contentrequiredstring | object[]A string fans the same copy to every channel in channel_ids. An array gives per-channel copy.
channel_idsstring[]Required when content is a string; ignored when it is an array.
media_urlsstring (uri)[]Public URLs returned by POST /v1/media or /v1/media/from-url.
scheduled_atstring (date-time) | nullUTC ISO-8601. Required when type is "schedule".
platform_settingsobjectKeyed by platform slug: { pinterest: { board_id } }. See GET /v1/channels/:id/settings.
threadstring[]Post as a thread on platforms that support it. Part one becomes the body.
idempotency_keystringIn-body equivalent of the Idempotency-Key header; the header wins.
curl
curl -s -X POST "https://planifyapps.com/api/v1/posts" \
  -H "Authorization: Bearer $PLANIFY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
     "type": "schedule",
     "content": "Shipped the thing. Notes in the thread.",
     "channel_ids": ["ch_1a2b3c"],
     "media_urls": ["https://…/photo.jpg"],
     "scheduled_at": "2026-08-04T09:30:00Z"
  }'

Response — 200

type"draft" | "schedule" | "now"
postsobject[]
warningsstring[]Non-fatal. E.g. a thread sent to a platform that does not support threading.
idempotent_replaybooleanTrue when this Idempotency-Key had already been used and nothing new was created.
publishobjectPresent only for type "now".

Clients

Using Planify from an AI assistant

Planify publishes an MCP server, so an assistant that speaks the Model Context Protocol — Claude, or any other MCP client — can use this API directly without you writing glue code.

claude_desktop_config.json
{
  "mcpServers": {
    "planify": {
      "command": "npx",
      "args": ["-y", "@planifyapps/mcp"],
      "env": { "PLANIFY_API_KEY": "plfy_live_..." }
    }
  }
}

It exposes one tool per endpoint, and the per-channel rules from GET /v1/channels/{id}/settings become the tool inputs — so the assistant is told a channel’s character limit and required fields up front rather than discovering them by being rejected.

The tool that creates posts defaults to a draft. Publishing needs type: "now", a key holding posts:publish, and your confirmation, which the server’s own instructions tell the assistant to ask for.

A note on publishing

Publishing is irreversible and public. If you are building an agent, confirm with the person before publishing on their behalf — draft or schedule first and let them approve. The API supports that path precisely so it can be the default one.

Frequently asked questions

Can an AI assistant post to social media with Planify?

Yes. Planify publishes an MCP server, so any assistant that speaks the Model Context Protocol — Claude among them — can list your connected accounts, read each one's posting rules, upload media and publish. Install it with npx -y @planifyapps/mcp and a Planify API key. Publishing immediately requires a key holding the posts:publish scope, which is withheld by default, so an assistant can prepare drafts without being able to post to your audience.

Which social platforms can the Planify API publish to?

The API can publish to 9 platforms: twitter, facebook, instagram, threads, bluesky, pinterest, mastodon, linkedin, youtube. Which of those a given account may use depends on its plan — GET /api/v1/channels reports availability per channel rather than hiding the ones your plan excludes.

How do I know each platform's character limit and media rules?

Call GET /api/v1/channels/{id}/settings before writing the post. It returns that specific channel's character limit, how much media it accepts, whether media is required, the rules that decide whether publishing succeeds, and a JSON Schema for its platform settings. It is per channel rather than per platform, because a Facebook Page and a personal profile differ.

Does the Planify API prevent duplicate posts on retry?

Yes. Send an Idempotency-Key header on POST /api/v1/posts. Repeating a request with the same key returns the posts created the first time instead of creating more, and sets idempotent_replay: true in the response. This matters more for software than for people, because software retries automatically.

Is there an OpenAPI specification for the Planify API?

Yes, at https://planifyapps.com/openapi.json — OpenAPI 3.1, served without authentication. It is generated from the same schemas the API validates requests against, so it cannot describe a version of the API that no longer exists.

How do I get a Planify API key?

Create one in the Planify dashboard under Settings → API. The key is shown once and stored only as a SHA-256 hash, so a lost key is replaced rather than recovered. Keys cannot create other keys, which means a leaked key cannot mint more.

Ready to build?

Create a key in Settings → API, then start with GET /v1/channels.