Quick start
- In the app, open Settings > AI agents and press New API key. Copy the key: it is shown once.
- 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" - 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"}' - 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.
| Permission | Lets the key |
|---|---|
read | See accounts, posts, ideas, stats and comments |
draft | Create and change drafts, ideas and media; delete unsent posts |
schedule | Schedule posts and use the queue (held for approval unless the owner allows otherwise) |
publish | Post 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) |
reply | Reply 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 amessageto 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:00is 9:00 for the owner. With an offset orZthey 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 /mereturns the workspacetimezoneandnow, 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.
{
"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"]
}
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request, invalid_time | Something in the request is missing or malformed |
| 401 | unauthorized, invalid_token | No token, or the token is wrong, revoked or expired |
| 402 | plan_limit | The workspace plan's limit is reached |
| 403 | missing_scope, forbidden | The key may not do this |
| 404 | not_found | No such endpoint, or the id is not in this workspace |
| 405 | method_not_allowed | The endpoint exists but not with this method (see Allow) |
| 409 | conflict, idempotency_in_progress | The post already went out, the queue is paused or full, or the same request is still running |
| 413 | too_large | Files up to 95 MB |
| 415 | unsupported_media_type | Images (JPG, PNG, WebP, GIF) or videos (MP4, MOV, WebM) |
| 422 | validation_failed, idempotency_key_reused | Platform limits (length, media, Reddit title), or a key reused with another body |
| 429 | rate_limited | Too many requests this minute: wait Retry-After seconds |
| 5xx | server_error | Our 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.
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.
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
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_idsonly) - account_idsarray
- Ids from
GET /accounts - platformsarray
- Every connected account on these platforms, e.g.
["x", "linkedin"] - whenstring
draft(default withoutscheduled_at),schedule(default with it),now, orqueue: 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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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).
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.
curl https://posteveryplatform.com/api/v1/analytics/best-time -H "Authorization: Bearer $PEP_API_KEY"Webhooks
Add up to 5 webhooks in Settings > AI agents. We send a POST with a JSON body when:
| Event | When |
|---|---|
post.published | A post went out on every account |
post.partial | A post went out on some accounts and failed on others |
post.failed | A post failed on every account |
post.pending_approval | A post started waiting for the owner's approval |
ping | The Send test button |
{
"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_approvalis 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
idto 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.
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));
}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/mcpas a custom connector and sign in; no key to paste. See the MCP guide. - CLI:
pepis 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--jsonon every command. Get it withcurl -fsSL https://posteveryplatform.com/cli/pep.mjs -o pep.mjs, then runnode pep.mjs loginand paste your API key. - OpenAPI: /api/v1/openapi.json describes every endpoint (OpenAPI 3.1), for code generators and tools that import specs.
Comments
GET
/commentsneeds readComments on posts sent in the last 14 days, newest first, 40 a page. New comments are checked at most every 5 minutes.
all(default),unread,needs_replyorhiddennextcursor from the previous pagePOST
/comments/{id}/replyneeds replyReplies from the account the comment is on, where the platform allows it (
can_replyon the comment; at mostreply_limitcharacters). Answers201.