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.
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.
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:read
default
List channels, read their posting rules, and run their lookups.
posts:read
default
Read posts that already exist.
posts:write
default
Create drafts and scheduled posts.
posts:publish
opt-in
Publish immediately. Never granted by default — ask for it explicitly.
media:write
default
Upload images and video.
analytics:read
default
Read 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
401
invalid_api_key
Missing, unrecognised or revoked key. Do not retry.
402
payment_required
The plan does not include this. GET /v1/me reports plan and status.
403
insufficient_scope
The key lacks a needed scope. Do not retry with this key.
409
request_in_progress
An identical Idempotency-Key request is still running. Wait and retry.
422
validation_failed
The post breaks a channel rule. Read the channel settings.
429
rate_limited
Honour Retry-After.
502
platform_error
The 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.
Platforms this plan may publish to. A channel outside this list is read-only.
features
object
—
usage
object
A null limit means unlimited, and so does a null remaining.
rate_limits
object
Per-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.
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.
Hard requirements first, then style guidance. Prose, written to be read by a model.
settings_schema
object | null
JSON Schema for this platform's slice of platform_settings.
tools
object[]
—
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
idrequired
string
Channel id.
Body — application/json
method_name
string
Which lookup to run. Listed as tools[].method_name by GET /v1/channels/:id/settings.
methodName
string
—
data
object
Arguments for the lookup, matching that tool's input schema.
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.
The weekly schedule these came from, or null when none is set.
slots
string (date-time)[]
Ascending, UTC.
slot
string | null
First of slots — what most callers want.
reason
string
Present when there are no slots to give.
note
string
Present 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.
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.
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-Key
string
Makes 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).
contentrequired
string | object[]
A string fans the same copy to every channel in channel_ids. An array gives per-channel copy.
channel_ids
string[]
Required when content is a string; ignored when it is an array.
media_urls
string (uri)[]
Public URLs returned by POST /v1/media or /v1/media/from-url.
scheduled_at
string (date-time) | null
UTC ISO-8601. Required when type is "schedule".
platform_settings
object
Keyed by platform slug: { pinterest: { board_id } }. See GET /v1/channels/:id/settings.
thread
string[]
Post as a thread on platforms that support it. Part one becomes the body.
idempotency_key
string
In-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"
—
posts
object[]
—
warnings
string[]
Non-fatal. E.g. a thread sent to a platform that does not support threading.
idempotent_replay
boolean
True when this Idempotency-Key had already been used and nothing new was created.
publish
object
Present 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.
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.