Skip to content

developers

PostEveryPlatform API

Let your AI agent or your script write, schedule and reply. Nothing it schedules goes out until you approve it, unless you say so.

OpenAPI 3.1Skill file

On this pageCONTENTS

Quick start

  1. In the app, open Settings > AI agents and press New API key. Copy the key: it is shown once.
  2. Check who the key acts for:
    shell
    export PEP_API_KEY=pep_live_...
    curl https://posteveryplatform.com/api/v1/me \
      -H "Authorization: Bearer $PEP_API_KEY"
  3. Schedule a post to every connected X and LinkedIn account, at 9:00 in your workspace timezone:
    shell
    curl -X POST https://posteveryplatform.com/api/v1/posts \
      -H "Authorization: Bearer $PEP_API_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: huila-launch" \
      -d '{"text": "New single origin from Huila just landed.",
           "platforms": ["x", "linkedin"],
           "scheduled_at": "2026-11-03T09:00"}'
  4. The answer says "status": "pending_approval": the post waits under Waiting for your approval in Settings > AI agents until you approve it.

Base URL: https://posteveryplatform.com/api/v1. Send JSON with Content-Type: application/json. Any origin may call the API from a browser (CORS is open; it takes bearer tokens only, never cookies), but keep keys out of public web pages.

Keys and authentication

Every request carries Authorization: Bearer <token>. The token is either:

  • an API key (pep_live_ and 32 more characters) made by the workspace owner in Settings > AI agents, or
  • an OAuth access token that an MCP client got when the owner signed in and allowed it (see the MCP guide).

A key acts in the one workspace it was made in, with the permissions ticked when it was made. We store only a hash of it, so it is shown once; lost keys are replaced, not recovered. The owner can revoke a key at any time and it stops working at once. A workspace has up to 20 active keys, on every plan, Free included.

A missing, mistyped or revoked token answers 401 with code unauthorized or invalid_token.

Permissions

Each key has some of five permissions. New keys get read, draft, schedule and reply; publish is off until the owner ticks it.

PermissionLets the key
readSee accounts, posts, ideas, stats and comments
draftCreate and change drafts, ideas and media; delete unsent posts
scheduleSchedule posts and use the queue (held for approval unless the owner allows otherwise)
publishPost right away (still held for approval if approval is on) and retry failed posts (only for keys whose posts skip approval, since a retry sends at once)
replyReply to comments

A call outside the key's permissions answers 403 with code missing_scope and names the permission. Some things no key can ever do, whatever its permissions: connect or disconnect social accounts, billing, workspace and account settings, and approving posts.

The approval rule

Your AI agent writes and schedules. You approve.

  • Drafts save right away and never need approval.
  • While Agent posts wait for my approval is on in Settings > AI agents (it is on for every new workspace), posts a key schedules or posts now come back with "status": "pending_approval" and "waiting_for_approval": true, plus a message to pass on to the person.
  • The owner approves or rejects them in the app. Approved posts go out at their time, or within a minute if that time has passed. Rejected posts become drafts again, with the owner's note in approval_note.
  • Changing a scheduled post puts it back in front of the owner.
  • The owner can tick Posts skip my approval on one key (or one connected app). Its posts go out at their time without waiting. Switching approval off for the workspace does the same for every key.

GET /me tells a key which rule applies to it: approval.required. The post.pending_approval webhook fires when a post starts waiting.

Times and timezones

  • Times you send are ISO 8601. Without an offset they are read in the workspace timezone: 2026-11-03T09:00 is 9:00 for the owner. With an offset or Z they are exact: 2026-11-03T14:00:00Z. Unix seconds work too.
  • Times you get back are ISO 8601 in the workspace timezone with its offset, for example 2026-11-03T09:00:00-05:00.
  • GET /me returns the workspace timezone and now, the current time there. Agents should read the date from it rather than guess.

Rate limits

Each key (or connected app) may make 30 requests a minute on the Free plan and 120 a minute on paid plans. Every answer carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (unix seconds). Over the limit you get 429 with code rate_limited and a Retry-After header in seconds.

Plan limits apply too, because every call goes through the same checks as the app: the Free plan sends 15 posts a month and connects 3 accounts. A call over a plan limit answers 402 with code plan_limit and "upgrade": true.

Errors

Every error has the same shape. message is written for people; problems lists every problem when there is more than one thing to fix.

json
{
  "error": {
    "code": "validation_failed",
    "message": "X: 37 characters over the 280 limit",
    "problems": ["X: 37 characters over the 280 limit", "Instagram needs an image or a video"]
  }
}
StatusCodeMeaning
400invalid_request, invalid_timeSomething in the request is missing or malformed
401unauthorized, invalid_tokenNo token, or the token is wrong, revoked or expired
402plan_limitThe workspace plan's limit is reached
403missing_scope, forbiddenThe key may not do this
404not_foundNo such endpoint, or the id is not in this workspace
405method_not_allowedThe endpoint exists but not with this method (see Allow)
409conflict, idempotency_in_progressThe post already went out, the queue is paused or full, or the same request is still running
413too_largeFiles up to 95 MB
415unsupported_media_typeImages (JPG, PNG, WebP, GIF) or videos (MP4, MOV, WebM)
422validation_failed, idempotency_key_reusedPlatform limits (length, media, Reddit title), or a key reused with another body
429rate_limitedToo many requests this minute: wait Retry-After seconds
5xxserver_errorOur side. Try again in a minute

Idempotency

Send Idempotency-Key: <any unique string> (up to 255 characters) with POST /posts. If the same key comes again within 24 hours with the same body, you get the first answer back, with Idempotent-Replayed: true, and no second post is made. The same key with a different body answers 422 idempotency_key_reused. Keys are per API key. Answers of 429 and 5xx are not stored, so you can retry those.

Workspace and accounts

GET/meneeds read

The workspace (name, timezone, plan), the token's permissions, whether its posts wait for approval, its limits, and the current time in the workspace timezone. Call it first.

shell
curl https://posteveryplatform.com/api/v1/me -H "Authorization: Bearer $PEP_API_KEY"
Response
{
  "workspace": { "id": "ws_...", "name": "Northwind Coffee", "timezone": "America/New_York", "plan": "solo", "demo": false },
  "token": { "kind": "api_key", "id": "key_...", "scopes": ["read", "draft", "schedule", "reply"], "auto_publish": false },
  "approval": { "required": true, "workspace_requires_approval": true, "rule": "Drafts save right away. Scheduled and post-now requests wait for the owner's approval in PostEveryPlatform before they go out." },
  "limits": { "requests_per_minute": 120, "accounts": 10, "posts_per_month": null },
  "now": "2026-10-27T09:12:40-04:00"
}

GET/accountsneeds read

The connected social accounts: id, platform (x, linkedin, instagram, facebook, tiktok, youtube, threads, bluesky, pinterest, reddit, google, mastodon), handle, status and can_post. Accounts are connected by the owner in the app, never through the API.

shell
curl https://posteveryplatform.com/api/v1/accounts -H "Authorization: Bearer $PEP_API_KEY"

Posts

A post has one text (with optional different text per account), up to 10 images or 1 video, and one target per account. Its status is draft, pending_approval, scheduled, publishing, published, partial (some accounts failed) or failed. Each target has its own status, the live url once sent, or the error.

GET/postsneeds read

Posts whose time falls in the window (default: 7 days back to 30 days ahead), plus drafts that have no time. Posts waiting for approval always have a time, so widen the window to see ones further out.

from, toquery, time
The window
statusquery, comma separated
Only these statuses, e.g. scheduled,pending_approval
shell
curl "https://posteveryplatform.com/api/v1/posts?from=2026-11-02&to=2026-11-09&status=scheduled,pending_approval" \
  -H "Authorization: Bearer $PEP_API_KEY"

POST/postsneeds draft (+ schedule or publish)

Write a post. when: "draft" needs draft; "schedule" needs schedule; "queue" needs schedule and read; "now" needs publish. Platform limits are checked first: a post that breaks one answers 422 with every problem in problems. Answers 201.

textstring
The post text (or attach media_ids only)
account_idsarray
Ids from GET /accounts
platformsarray
Every connected account on these platforms, e.g. ["x", "linkedin"]
whenstring
draft (default without scheduled_at), schedule (default with it), now, or queue: the next free time on the posting schedule
scheduled_attime
When it should go out
media_idsarray
Ids from POST /media
overridesobject
Different text per platform or account id: {"x": "Short version"}
first_commentstring
Posted as the first comment where the platform allows it
redditobject
{"subreddit": "coffee", "title": "..."}, needed for Reddit
shell
curl -X POST https://posteveryplatform.com/api/v1/posts \
  -H "Authorization: Bearer $PEP_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: dial-in-guide" \
  -d '{"text": "How we dial in espresso every morning: https://example.com/dial-in",
       "platforms": ["x", "linkedin"], "when": "queue",
       "overrides": {"linkedin": "Every bag behaves differently. Our 10-minute dial-in routine: https://example.com/dial-in"}}'
Response
{
  "post": {
    "id": "post_...", "status": "pending_approval", "text": "How we dial in espresso every morning: https://example.com/dial-in",
    "scheduled_at": "2026-10-28T09:00:00-04:00", "published_at": null, "created_via": "api:key_...",
    "targets": [
      { "account_id": "acc_...", "platform": "x", "handle": "@northwindcoffee", "status": "pending", "url": null, "error": null, "text": null },
      { "account_id": "acc_...", "platform": "linkedin", "handle": "Northwind Coffee Co.", "status": "pending", "url": null, "error": null, "text": "Every bag behaves differently. ..." }
    ],
    "media": [], "first_comment": null, "reddit": null, "approval_note": null, "waiting_for_approval": true
  },
  "waiting_for_approval": true,
  "message": "Waiting for approval: the workspace owner approves or rejects this post in PostEveryPlatform before it goes out. Tell your user it is waiting for them."
}

GET/posts/{id}needs read

One post, with the status, live link and error per account.

shell
curl https://posteveryplatform.com/api/v1/posts/post_123 -H "Authorization: Bearer $PEP_API_KEY"

PATCH/posts/{id}needs draft (+ schedule or publish)

Change only the fields you send; the same fields as POST /posts. Drafts, scheduled, failed and waiting posts can be changed; sent ones answer 409. A changed scheduled post waits for approval again unless the key skips approval.

shell
curl -X PATCH https://posteveryplatform.com/api/v1/posts/post_123 \
  -H "Authorization: Bearer $PEP_API_KEY" -H "Content-Type: application/json" \
  -d '{"scheduled_at": "2026-11-04T12:30"}'

DELETE/posts/{id}needs draft

Drafts, scheduled posts and posts waiting for approval are removed. Posts that went out or failed are only hidden here; anything that was sent stays on the platforms.

shell
curl -X DELETE https://posteveryplatform.com/api/v1/posts/post_123 -H "Authorization: Bearer $PEP_API_KEY"

POST/posts/{id}/retryneeds publish

Sends the accounts that failed again. Only for posts with status failed or partial.

shell
curl -X POST https://posteveryplatform.com/api/v1/posts/post_123/retry -H "Authorization: Bearer $PEP_API_KEY"

Media

POST/medianeeds draft

multipart/form-data with one file field: JPG, PNG, WebP, GIF, MP4, MOV or WebM, up to 95 MB. Use the returned id in media_ids. Instagram and TikTok need media, YouTube a video, Pinterest an image.

shell
curl -X POST https://posteveryplatform.com/api/v1/media \
  -H "Authorization: Bearer $PEP_API_KEY" -F "file=@latte-art.jpg"
Response
{ "media": { "id": "med_...", "mime": "image/jpeg", "filename": "latte-art.jpg", "url": "https://posteveryplatform.com/m/...", "bytes": 482113 } }

Ideas

The ideas board: cards with a title, body, tags, a link and a status of idea, drafting or ready. The Free plan holds 50 cards.

GET/ideasneeds read

Every card; ?q= searches titles, text, tags and links.

shell
curl "https://posteveryplatform.com/api/v1/ideas?q=espresso" -H "Authorization: Bearer $PEP_API_KEY"

POST/ideasneeds draft

Adds a card. Give a title or a body. Answers 201.

shell
curl -X POST https://posteveryplatform.com/api/v1/ideas \
  -H "Authorization: Bearer $PEP_API_KEY" -H "Content-Type: application/json" \
  -d '{"title": "Dial-in video", "body": "Film the morning routine", "tags": ["video"]}'

GET/ideas/{id}needs read

One card.

shell
curl https://posteveryplatform.com/api/v1/ideas/idea_123 -H "Authorization: Bearer $PEP_API_KEY"

PATCH/ideas/{id}needs draft

Change only the fields you send, for example move it to ready.

shell
curl -X PATCH https://posteveryplatform.com/api/v1/ideas/idea_123 \
  -H "Authorization: Bearer $PEP_API_KEY" -H "Content-Type: application/json" -d '{"status": "ready"}'

DELETE/ideas/{id}needs draft

Removes the card.

shell
curl -X DELETE https://posteveryplatform.com/api/v1/ideas/idea_123 -H "Authorization: Bearer $PEP_API_KEY"

Queue

GET/queue/nextneeds read

The next free times on the workspace's posting schedule (?count= 1 to 10). A post with when: "queue" takes the first one. Answers 409 when the queue is paused, has no times, or is full for the next 8 weeks.

shell
curl "https://posteveryplatform.com/api/v1/queue/next?count=3" -H "Authorization: Bearer $PEP_API_KEY"
Response
{ "next": "2026-10-28T09:00:00-04:00", "timezone": "America/New_York",
  "slots": [ { "at": "2026-10-28T09:00:00-04:00", "label": "Wed 28 Oct, 09:00" }, ... ] }

Analytics

GET/analyticsneeds read

Posts sent and failed, views, engagement, the posting streak, numbers per platform and per day, and the top posts. ?days= 7 to 90 (default 7).

shell
curl "https://posteveryplatform.com/api/v1/analytics?days=30" -H "Authorization: Bearer $PEP_API_KEY"

GET/analytics/postsneeds read

One row per sent post per account: views, likes, comments, shares, engagements and engagement rate. ?days= 1 to 365 (default 30).

shell
curl "https://posteveryplatform.com/api/v1/analytics/posts?days=30" -H "Authorization: Bearer $PEP_API_KEY"

GET/analytics/best-timeneeds read

Average engagement by weekday (0 = Monday) and hour in the workspace timezone over the last 90 days, overall and per platform. The top 3 times appear once 10 posts have numbers.

shell
curl https://posteveryplatform.com/api/v1/analytics/best-time -H "Authorization: Bearer $PEP_API_KEY"

Comments

GET/commentsneeds read

Comments on posts sent in the last 14 days, newest first, 40 a page. New comments are checked at most every 5 minutes.

filterquery
all (default), unread, needs_reply or hidden
account_idquery
Only comments on this account
beforequery
The next cursor from the previous page
shell
curl "https://posteveryplatform.com/api/v1/comments?filter=needs_reply" -H "Authorization: Bearer $PEP_API_KEY"

POST/comments/{id}/replyneeds reply

Replies from the account the comment is on, where the platform allows it (can_reply on the comment; at most reply_limit characters). Answers 201.

shell
curl -X POST https://posteveryplatform.com/api/v1/comments/cmt_123/reply \
  -H "Authorization: Bearer $PEP_API_KEY" -H "Content-Type: application/json" \
  -d '{"text": "Thank you, Sam! See you Saturday."}'

Webhooks

Add up to 5 webhooks in Settings > AI agents. We send a POST with a JSON body when:

EventWhen
post.publishedA post went out on every account
post.partialA post went out on some accounts and failed on others
post.failedA post failed on every account
post.pending_approvalA post started waiting for the owner's approval
pingThe Send test button
json
{
  "id": "evt_...",
  "type": "post.published",
  "created": 1793178000,
  "workspace_id": "ws_...",
  "data": { "post": { "id": "post_...", "status": "published", "text": "...", "targets": [ ... ] } }
}

Headers: PEP-Signature, PEP-Event (the type), PEP-Delivery (unique per call), User-Agent: PostEveryPlatform-Webhooks/1.0. post.pending_approval also carries data.review_url, the page where the owner approves; approval links themselves are never sent.

  • post.pending_approval is sent the moment a post starts waiting. Post results are sent by the minute job right after publishing, so within about a minute.
  • Answer with any 2xx within 5 seconds on the first try of post.pending_approval, 10 seconds otherwise. Redirects are not followed.
  • Anything else is tried again after 1, 5, 30 and 120 minutes. After 5 tries we stop and mark the webhook Failing in the app; the next delivered call clears it.
  • URLs must be https and public: private, loopback and link-local addresses are refused.
  • The same event can arrive twice (a retry after a slow answer). Use id to skip events you already handled.

Check the signature

PEP-Signature: t=<unix seconds>,v1=<hex>, where the hex is HMAC-SHA256 of "<t>.<raw body>" with your webhook's signing secret (whsec_..., shown once when you add it). Compute it over the raw body exactly as received, compare in constant time, and refuse old timestamps.

Node.js
import crypto from 'node:crypto';

// raw: the request body exactly as received (string or Buffer); header: the PEP-Signature header
export function verifyPep(raw, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(String(header || '').split(',').map((p) => p.split('=')));
  const t = Number(parts.t), sig = parts.v1 || '';
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
  const want = crypto.createHmac('sha256', secret).update(`${t}.`).update(raw).digest('hex');
  return sig.length === want.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(want));
}
Python
import hashlib, hmac, time

# raw: the request body as bytes, exactly as received; header: the PEP-Signature header
def verify_pep(raw: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in (header or "").split(",") if "=" in p)
    try:
        t = int(parts.get("t", "0"))
    except ValueError:
        return False
    if abs(time.time() - t) > tolerance:
        return False
    want = hmac.new(secret.encode(), f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, parts.get("v1", ""))

Skill file, MCP and CLI

  • Skill file: /skill.md teaches any agent that reads skill files the whole API, the approval rule and good manners, with curl and PEP_API_KEY. Tell your agent: Read https://posteveryplatform.com/skill.md and use my PostEveryPlatform API key from PEP_API_KEY.
  • MCP: works with any MCP client, for example Claude, ChatGPT or Cursor. Add https://posteveryplatform.com/mcp as a custom connector and sign in; no key to paste. See the MCP guide.
  • CLI: pep is one file (pep.mjs) for Node 18 or newer, with no dependencies: pep login, pep whoami, pep accounts, pep posts, pep post "text" --accounts x,linkedin --at 2026-11-03T09:00, pep upload, pep ideas, pep comments, pep reply, and --json on every command. Get it with curl -fsSL https://posteveryplatform.com/cli/pep.mjs -o pep.mjs, then run node pep.mjs login and paste your API key.
  • OpenAPI: /api/v1/openapi.json describes every endpoint (OpenAPI 3.1), for code generators and tools that import specs.