PlanSend for developers

Approval-aware publishing, from scripts or AI assistants.

Use workspace-scoped API keys to create drafts, request client approval, schedule posts and inspect publishing jobs. The same keys work with the built-in MCP server, so an AI assistant follows the same approval rules as the app.

create-draft.sh
# Create a draft in the workspace this key belongs to
curl https://plansend.io/api/v1/posts \
  -H "Authorization: Bearer $PLANSEND_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Launch week","body":"Ready to review.","channel_ids":[12,15]}'

# Ask the client to approve it
curl -X POST https://plansend.io/api/v1/posts/481/request-approval \
  -H "Authorization: Bearer $PLANSEND_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reviewer_email":"[email protected]"}'
# → { "data": { "review_url": "https://plansend.io/review/…", "expires_at": "…", "emailed": true } }

Authentication

Keys belong to one workspace

Create and revoke keys in the app under Developers → API keys, which also shows each key's last use and request count. Send the key as a Bearer token on every request: Authorization: Bearer ps_…. The raw key is shown once when it is generated; PlanSend stores only a hash. Every call, successful or not, is written to the workspace audit log with the key id.

ScopeAllows
readListing and reading: workspace, channels, posts, jobs, publishing health and AI credits. The read-only MCP tools.
writeEverything in read, plus the post lifecycle (create, update, request approval, approve, schedule, cancel) and AI Assist suggestions.
adminEverything in write, plus creating brand workspaces and inviting members. The key acts as the owner who created it, so that person must still be an owner.

Scopes are checked on the server for each request; a key never inherits a signed-in session. Each key is limited to 120 requests in a rolling minute, and AI Assist is additionally bounded by the workspace's monthly credits.

Version 1

REST endpoints

Base URL https://plansend.io/api/v1. Send and receive JSON; responses arrive under a data key, errors as { "error": { "code", "message" } } with a matching HTTP status. Writes that change a post require the current revision, so two people can never overwrite each other unknowingly.

GET/api/v1/workspacesread

The workspace this key belongs to

POST/api/v1/workspacesadmin

Create another brand workspace on the same billing account

POST/api/v1/invitationsadmin

Invite an editor or reviewer by email

GET/api/v1/channelsread

Connected channels and their status

GET/api/v1/mediaread

Ready media items that can be attached by id

POST/api/v1/mediawrite

Upload an image or video by its public https URL

POST/api/v1/media/presignwrite

Create a 10-minute direct-upload URL for a file

POST/api/v1/media/:id/completewrite

Verify a direct upload and make the item ready

DELETE/api/v1/media/:idwrite

Delete an unused media object

GET/api/v1/postsread

List posts; filter by status, author_id, channel_id, network, from and to

GET/api/v1/posts/:idread

One post with variants, media, review history, comments and jobs

POST/api/v1/postswrite

Create a draft with one variant per channel

PATCH/api/v1/posts/:idwrite

Update a draft (requires the current revision); omitted fields keep their value

POST/api/v1/posts/:id/request-approvalwrite

Create a review link for the current revision; add reviewer_email to send it by email

POST/api/v1/posts/:id/approvewrite

Approve a specific revision

POST/api/v1/posts/:id/schedulewrite

Schedule an approved revision at a local time in a time zone

POST/api/v1/posts/:id/publish-nowwrite

Queue an approved revision for the next worker run

POST/api/v1/posts/:id/cancelwrite

Cancel a scheduled post before the worker claims it

GET/api/v1/jobsread

Inspect publishing jobs, retries and errors

POST/api/v1/jobs/:id/retrywrite

Retry a confirmed failed job; ambiguous results remain blocked

GET/api/v1/ai/creditsread

AI Assist credits used and remaining this month

POST/api/v1/ai/assistwrite

Generate caption, hook, hashtag, rewrite or shorten suggestions (one credit)

Reference

Requests and responses

The post lifecycle in four calls. Ids, tokens and dates are illustrative; scheduled_at and run_at are UTC, local_datetime and timezone are what you sent.

Create a draft

POST /api/v1/posts with a write key. channel_ids come from GET /api/v1/channels; every channel must belong to the workspace. Optional media_ids attach ready items from the media library. The draft starts at revision 1.

request
curl https://plansend.io/api/v1/posts \
  -H "Authorization: Bearer $PLANSEND_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Launch week",
    "body": "Ready to review.",
    "channel_ids": [12, 15]
  }'
201 Created
{
  "data": {
    "id": 481, "status": "draft", "revision": 1, "approved_revision": null,
    "title": "Launch week", "body": "Ready to review.",
    "scheduled_at": null, "local_datetime": null, "timezone": null,
    "variants": [
      { "id": 901, "channel_id": 12, "network": "linkedin", "channel_name": "Acme LinkedIn", "status": "draft" },
      { "id": 902, "channel_id": 15, "network": "instagram", "channel_name": "@acme", "status": "draft" }
    ],
    "media": []
  }
}

Request approval

POST /api/v1/posts/:id/request-approval moves the post to in_review and returns a review link that expires after 7 days. Reviewers need no account. Add reviewer_email to have PlanSend email the link (emailed tells you whether it did) and passcode to require a passcode on the review page; the passcode is never emailed. A scheduled post must be cancelled first.

request
curl -X POST https://plansend.io/api/v1/posts/481/request-approval \
  -H "Authorization: Bearer $PLANSEND_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "reviewer_email": "[email protected]",
    "passcode": "spring-24"
  }'
201 Created
{
  "data": {
    "review_url": "https://plansend.io/review/9f1c…Xw",
    "expires_at": "2026-09-16T09:00:00.000Z",
    "emailed": true
  }
}

Approve and schedule

Clients normally approve through the review link. When you hold the approval yourself, POST /api/v1/posts/:id/approve with the exact revision. Then POST /api/v1/posts/:id/schedule with the same revision, a local wall-clock time as YYYY-MM-DDTHH:mm and an IANA time zone. Workspaces that require approval reject an unapproved revision with 409 approval_required; a stale revision is 409 stale_revision. Scheduling creates one queued job per channel; the worker publishes at that time.

requests
curl -X POST https://plansend.io/api/v1/posts/481/approve \
  -H "Authorization: Bearer $PLANSEND_KEY" \
  -H "Content-Type: application/json" \
  -d '{"revision": 1, "note": "Approved on the call"}'

curl -X POST https://plansend.io/api/v1/posts/481/schedule \
  -H "Authorization: Bearer $PLANSEND_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "revision": 1,
    "local_datetime": "2026-09-15T09:30",
    "timezone": "Europe/Lisbon"
  }'
200 OK (schedule)
{
  "data": {
    "id": 481, "status": "scheduled",
    "revision": 1, "approved_revision": 1,
    "scheduled_at": "2026-09-15 08:30:00",
    "local_datetime": "2026-09-15T09:30",
    "timezone": "Europe/Lisbon",
    "variants": [ … ], "media": []
  }
}

Inspect publishing jobs

GET /api/v1/jobs returns the 200 most recent jobs of the workspace, one per channel variant and revision. status is queued, processing, retry, published, failed, cancelled or reconciliation_required; the last one means the network may already have accepted the post and a person must check before retrying. GET /api/v1/posts/:id includes the same jobs for one post.

request
curl https://plansend.io/api/v1/jobs \
  -H "Authorization: Bearer $PLANSEND_KEY"
200 OK
{
  "data": [
    {
      "id": 77, "post_id": 481, "variant_id": 901, "revision": 1,
      "status": "published", "attempts": 1,
      "run_at": "2026-09-15 08:30:00",
      "external_url": "https://www.linkedin.com/feed/update/…",
      "last_error": null,
      "idempotency_key": "post:481:variant:901:revision:1"
    }
  ]
}

Outbound events

Signed webhooks with automatic retries

Create an endpoint under Developers → Webhooks and subscribe it to post.published, post.failed, post.approved or channel.disconnected. Each endpoint receives its own whsec_… signing secret once. Deliveries retry after about 1, 5 and 30 minutes, then remain visible as failed in the delivery history.

Verify PlanSend-Signature before parsing the JSON. Read PlanSend-Timestamp, concatenate it with a period and the exact raw request body, compute HMAC-SHA256 with the endpoint secret, and compare the hexadecimal digest to the value after v1=. Reject old timestamps to prevent replay.

verify.js
const expected = crypto
  .createHmac('sha256', process.env.PLANSEND_WEBHOOK_SECRET)
  .update(`${req.get('PlanSend-Timestamp')}.${req.rawBody}`)
  .digest('hex');

const supplied = req.get('PlanSend-Signature').replace('v1=', '');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(supplied))) throw new Error('Invalid signature');

Errors

One shape, meaningful codes

Every error is { "error": { "code": "…", "message": "…" } } with an HTTP status you can branch on. code is stable; message is for people. MCP tools return the same codes inside a tool error (see below).

StatusCodeWhen
401invalid_api_keyThe Authorization header is missing, or the key is unknown or revoked.
403insufficient_scopeThe key's scope is below what the endpoint or tool needs.
403forbidden, billing_owner_requiredAdmin endpoints when the key's creator is no longer an owner of the workspace or billing account.
404not_foundThe post, workspace or invitation does not exist in this workspace. Other workspaces' ids are never revealed.
409stale_revisionThe revision you sent is not the current one: someone edited the post. Read it again and retry with the new revision.
409approval_requiredThe workspace requires approval and the current revision is not approved.
409invalid_stateThe action does not fit the post's status: editing a published post, cancelling a draft, reviewing a scheduled post.
409reconciliation_required, publish_startedPublishing already started for this revision; a person must check the network before retrying or cancelling.
409brand_limit, editor_limit, member_existsPlan allowances and duplicates on the admin endpoints.
422validation_error, invalid_channel, invalid_schedule, validation_failedBad input: a missing title, a channel from another workspace, a past or malformed schedule time, or text and media that break a network's limits.
429rate_limitedThis API key exceeded 120 requests in a rolling minute.
400 / 402 / 503invalid_input, ai_credits_exhausted, ai_unavailable, ai_busyAI Assist: bad request, no credits left this month, or the provider is not configured or busy (the credit is refunded).

Model Context Protocol

Connect an AI assistant

The MCP server at https://plansend.io/mcp exposes PlanSend as tools over Streamable HTTP: list channels, create drafts, request approval, approve a revision, schedule, inspect jobs and draft copy with AI Assist. It authenticates with the same API keys, and scopes, revision checks and the audit log apply exactly as they do for the REST API. An assistant can prepare a week of posts, but every post still goes through the review and scheduling rules; nothing is published on the spot.

  1. Create an API key under Developers → API keys. Use read scope for an assistant that only reports, write scope for one that drafts and schedules.
  2. Add the server to your client with one of the snippets below, replacing YOUR_KEY.
  3. Ask: “List my channels”, “Draft three LinkedIn posts about the launch and send them for approval”, “What is scheduled next week?”

Claude Code

One command; the header carries your key. Check with claude mcp list or /mcp inside a session.

terminal
claude mcp add --transport http plansend https://plansend.io/mcp --header "Authorization: Bearer YOUR_KEY"

Cursor and Windsurf

Cursor reads .cursor/mcp.json in the project or ~/.cursor/mcp.json. Windsurf uses the same shape in ~/.codeium/windsurf/mcp_config.json, with serverUrl in place of url.

mcp.json
{
  "mcpServers": {
    "plansend": {
      "url": "https://plansend.io/mcp",
      "headers": { "Authorization": "Bearer YOUR_KEY" }
    }
  }
}

Claude Desktop and clients without header support

Bridge through mcp-remote (needs Node.js). In Claude Desktop open Settings → Developer → Edit Config and add the server to claude_desktop_config.json. On Windows, pass the header through an environment variable: "--header", "Authorization:${PLANSEND_AUTH}" with "env": { "PLANSEND_AUTH": "Bearer YOUR_KEY" }.

claude_desktop_config.json
{
  "mcpServers": {
    "plansend": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://plansend.io/mcp", "--header", "Authorization: Bearer YOUR_KEY"]
    }
  }
}
# the same bridge from a terminal:
# npx mcp-remote https://plansend.io/mcp --header "Authorization: Bearer YOUR_KEY"

Any Streamable HTTP client

Send JSON-RPC 2.0 messages with POST. The server is stateless: no session id, no server-to-client stream (GET and DELETE answer 405), plain JSON responses. A missing or revoked key answers 401.

terminal
curl https://plansend.io/mcp \
  -H "Authorization: Bearer $PLANSEND_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"list_channels","arguments":{}}}'

MCP tools

What the assistant can do

Each tool describes its arguments to the client, so the assistant fills them in itself. Results carry the same JSON as the REST data payload in the first text block and a one-sentence summary in the second. Failures come back as a tool error with { "error": code, "message": text } using the codes above, so a stale revision or a missing approval is explained rather than crashing the conversation.

ToolWhat it doesScope
list_workspacesThe workspace this key belongs to: time zone, plan and whether approval is required before schedulingread
list_channelsConnected channels with the ids used as channel_idsread
list_postsPosts by status, channel, network or scheduled date rangeread
get_postOne post with variants, media, review history, comments and jobsread
list_jobsPublishing jobs, newest first, with status, attempts and the last errorread
list_mediaMedia library items that can be attached by idread
get_publish_healthQueue counts by status and the most recent failuresread
ai_creditsAI Assist credits used and remaining this monthread
upload_mediaAdd an image or video to the media library by its public https URLwrite
create_draftCreate a draft with one variant per channel; nothing is scheduledwrite
update_draftChange title, body, channels or media; creates a new revision and clears approvalwrite
request_approvalPut a post in review and return the review link, optionally emailed to the reviewerwrite
approve_postApprove one exact revisionwrite
schedule_postSchedule an approved revision at a local time; the worker publishes itwrite
cancel_scheduled_postTake a scheduled post off the queue before publishing startswrite
ai_assistCaption, headline, hashtag, rewrite or shorten suggestions for a person to review (one credit)write

Two resources are readable as well: plansend://posts/{postId} (the same object as get_post) and plansend://calendar/{from}/{to} for scheduled posts in an inclusive YYYY-MM-DD range. There is deliberately no “publish now” tool: publishing happens through scheduling, in the worker, with the idempotency guarantees below.

Guarantees

What the API promises

Revision safety

Approvals, schedules and edits reference a revision number. A stale revision is rejected with 409 stale_revision instead of silently winning, and every content edit clears earlier approvals.

Idempotent publishing

Each publish job carries an idempotency key per variant and revision, so retries after a network failure never double-post.

Tenant isolation

Every query is scoped to the key's workspace. There is no way to address another workspace's posts, channels or jobs.

Audit trail

Every API and MCP action is recorded with the key id, action and outcome, and is visible to workspace owners.

Sign in to PlanSend

Use the Google account you work with. Your first brand workspace is created automatically.

Continue with Google By continuing you agree to the Terms and Privacy Policy. Clients reviewing posts never need an account.