PulsarSocial

Let your agent upload, schedule and post.

An MCP server, a CLI and a REST API over the same scheduler the dashboard uses. Point Claude, Cursor or your own agent at it and it can ingest media, validate against every platform’s rules, and schedule to Instagram, Facebook, TikTok, X, Pinterest and YouTube.

https://getpulsarsocial.com/mcphttps://getpulsarsocial.com/api/v1npx -y @pulsarsocial/mcpnpx pulsarsocial

Quickstart

Create a key, connect an MCP client, and get a first post on the calendar. About five minutes.

1

Create an API key

In the dashboard, go to Settings → API keys and create a key. Choose a scope preset while you are there — draft is the default and the one to start with: an agent using it can compose posts, but every post lands as a draft for you to approve.

what a key looks like
utsk_live_xxxxxxxxxxxx.your-secret-here
The secret half is shown once, at creation. A key belongs to exactly one workspace — there is no organization_id parameter anywhere in the API, because the key already decides which workspace you are talking to.

Every plan can create keys, free Hobby included. Plans differ by how fast you may call: 20 requests a minute on Hobby, then 60, 120 and 300. See pricing.

2

Connect Claude

Point Claude Code at the remote MCP server. Nothing to install.

terminal
claude mcp add --transport http pulsarsocial \
  https://getpulsarsocial.com/mcp \
  --header "Authorization: Bearer utsk_live_xxxxxxxxxxxx.your-secret-here"

For Claude Desktop, Cursor, VS Code and anything else that only speaks stdio, see MCP setup below.

3

Ask for a post

The agent should do three things in order: list your connected accounts, validate a draft payload against the platform rules, then create the post.

prompt
Draft a post announcing our Tuesday launch and put it on
the calendar for 9am Tuesday, Instagram and X. Validate it
before you create anything.

With a draft key it comes back as a draft, waiting for you in the dashboard. With a write key it is scheduled for real.

The workflow that works

Every posting task follows the same three steps, and skipping one is how agents produce payloads that fail:

list_accountsvalidate_postcreate_post
  • list_accounts — every target needs a real social_account_id. They cannot be guessed from a handle.
  • validate_post — a dry run. Writes nothing, consumes no quota, never touches a live account, and returns per-target errors. Iterate here until it is clean.
  • create_post — one call, all targets. Read the response rather than assuming it scheduled.

API keys

One key, one workspace. Everything below — the connector, the skill, the CLI, the REST API — authenticates with the same string.

Create one in the dashboard under Settings → API keys. The secret half is shown once, at creation; after that only a masked prefix is stored, so a lost key has to be revoked and replaced rather than recovered.

what a key looks like
utsk_live_xxxxxxxxxxxx.your-secret-here

Pick the narrowest scope that works

PresetAn agent holding it can
readLook at posts, accounts, media and usage. Change nothing.
draftWrite posts as drafts for a human to approve. The default, and the one to start with.
writeSchedule and publish straight to live accounts, with nobody reviewing first.
There is no organization_idparameter anywhere in the API. The key decides which workspace you are talking to, which is why a key from one workspace returns 404 — not 403 — against another’s ids.

What each plan gets

PlanKeysRequests / min
Hobby (free)320
Starter1060
Growth25120
Scale100300

Every plan has API access, the free one included — plans differ by ceiling, not by capability. See pricing.

MCP server

Give Claude, ChatGPT, Gemini or your own agent direct access to your workspace — see what is connected, draft a post, schedule it. Pick your client.

Claude desktop & web

Open Settings → Connectors → Add custom connector. Paste this as the URL, leave the OAuth fields empty, and click Add.

Remote MCP server URL
https://getpulsarsocial.com/mcp?apiKey=utsk_live_xxxxxxxxxxxx.your-secret-here

This URL contains your API key — treat it like a password. Rotate it any time from Settings → API keys.

Claude Code

One command, and the key travels as a header rather than in the URL.

terminal
claude mcp add --transport http pulsarsocial \
  https://getpulsarsocial.com/mcp \
  --header "Authorization: Bearer utsk_live_xxxxxxxxxxxx.your-secret-here"
Raw endpoint details
Endpoint:  https://getpulsarsocial.com/mcp
Transport: Streamable HTTP (stateless)
Auth:      Authorization: Bearer utsk_live_xxxxxxxxxxxx.your-secret-here
           …or ?apiKey= in the URL, for clients with no header field

Clients that can only spawn a local process use the @pulsarsocial/mcp stdio bridge, which forwards JSON-RPC to this same endpoint. It implements no tools of its own, so the tool list never goes stale.

stdio bridge
npx -y @pulsarsocial/mcp
VariableRequiredDefault
PULSARSOCIAL_API_KEYyes
PULSARSOCIAL_BASE_URLnohttps://getpulsarsocial.com

Agent skill

A file that teaches an agent the rules before it posts, so it gets one right on the first attempt instead of the third.

The MCP server gives an agent the tools. This gives it the judgement: validate before creating, that X counts 280 weighted characters where every link costs 23, that a draft-scoped key saves rather than schedules, and that reusing an idempotency key on retry is what stops a double-post.

Save it as pulsarsocial/SKILL.mdin your agent’s skills folder — for Claude Code that is ~/.claude/skills/pulsarsocial/SKILL.md — and it loads itself whenever a posting task comes up.

pulsarsocial/SKILL.md
---
name: pulsarsocial
description: Schedule and publish social media posts through PulsarSocial (getpulsarsocial.com) — Instagram posts/reels/stories, Facebook Pages, TikTok videos, X/Twitter tweets, YouTube videos and Shorts, and Pinterest pins — from one content calendar. Use when asked to draft, schedule, queue, plan, publish, reschedule, cancel or audit social posts, build a content calendar or posting plan, check which social accounts are connected, upload or ingest media for a post, or check posting quota. Covers the PulsarSocial MCP tools (list_accounts, validate_post, create_post, …) and the REST API at https://getpulsarsocial.com/api/v1.
---

# PulsarSocial

PulsarSocial schedules one post to many platforms at once. You write the content, pick which connected accounts it goes to, give it a time, and PulsarSocial publishes it.

Everything below assumes you have the PulsarSocial MCP tools available. If you don't, the same operations exist as REST at `https://getpulsarsocial.com/api/v1` with an `Authorization: Bearer utsk_live_…` header, and the shapes are identical.

## The one workflow that works

```
list_accounts  →  validate_post  →  create_post
```

Do not skip steps. Each one exists because the step after it fails without it.

### 1. `list_accounts` — always first

Every target in a post needs a real `social_account_id`. These are opaque ids belonging to the user's workspace; you cannot guess them, derive them from a handle, or reuse one from an earlier conversation. Call `list_accounts` at the start of any posting task, even if the user only mentioned one platform.

It also tells you what is *possible*. If the user says "post this to LinkedIn" and `list_accounts` returns no LinkedIn account, say so immediately rather than constructing a payload that will 404.

**Connecting an account is not something you can do.** It is an interactive OAuth flow in a browser — the user has to visit getpulsarsocial.com and click through the platform's consent screen. There is no API for it, by design. If an account is missing, tell the user to connect it in the dashboard and stop.

### 2. `validate_post` — use it constantly

`validate_post` takes the exact payload you would pass to `create_post` and tells you whether it would succeed. It is a dry run:

- writes nothing
- consumes no quota
- never touches a live social account
- returns per-target errors and warnings, not one generic "invalid"

This is the single most useful tool in the set. Platform rules are fiddly and inconsistent, and the cost of guessing wrong on `create_post` is a real post on a real account. So: **build a payload, validate it, fix what it complains about, validate again, and only then create.** Two or three validate round-trips before a create is normal and correct, not wasteful.

Validate again after any edit, including ones that look harmless. Adding a link to an X post can push it over the limit on its own (see below).

### 3. `create_post`

Only after a clean validate. Pass an `Idempotency-Key` (see below). Then **read the response** — do not assume what happened.

## Scheduling, and how to post right now

`scheduled_at` is an ISO timestamp with an offset, and it is the publish moment — there is no separate "post now" flag.

**To publish immediately, pass the current time.** A timestamp slightly in the past is accepted and treated as due now (there is a few-minutes' grace, which also absorbs clock skew between you and the server); anything older than that is rejected as `SCHEDULED_AT_IN_PAST`. Do not try to fake immediacy by scheduling a minute or two ahead — that just makes the user wait.

Two things worth telling the user rather than letting them discover:
- Staging starts about **8 minutes before** `scheduled_at`, so a post scheduled sooner than that begins uploading right away. This is normal, not an early publish.
- "Immediately" still means *seconds to minutes*, not instant. The media has to be validated, uploaded to the platform and encoded by it before it goes live. A large video takes longer.

When the user gives a wall-clock time ("post at 11:30"), resolve **AM vs PM explicitly** and echo the full resolved timestamp back to them. Getting this wrong silently schedules a post 12 hours away, which reads to the user as a stuck upload rather than a mistake.

## Draft scope: read the response

API keys carry one of three scope presets: `read`, `draft`, `write`. `draft` is the default and the safe one.

With a **`draft`-scoped key, `create_post` saves a DRAFT and does not schedule it.** The post sits in the workspace for a human to review and approve. The response says so explicitly — its status is a draft status and there is no scheduled publish.

This means: never report "scheduled for 9am Tuesday" because you called `create_post` and got a 200. Read the returned status and tell the user what actually happened — "saved as a draft in PulsarSocial, approve it in the dashboard to schedule it." Getting this wrong makes the user think something is queued when it isn't.

A `write`-scoped key schedules and publishes for real. If the user's key is `write`, treat `create_post` as an irreversible action against their audience and confirm before firing.

## Per-platform rules that will bite you

`validate_post` enforces all of these. This list is here so you can get it right on the first attempt instead of the third.

### X (Twitter)
The limit is **280 weighted characters**, not 280 characters:
- Every URL counts as **23** characters regardless of its actual length. A long tracking URL and a short link cost exactly the same.
- CJK characters (Chinese, Japanese, Korean) and most emoji count as **2**.
- Latin text counts as 1.

So a 250-character draft plus one link is 273 and fits; plus two links it is 296 and does not. Never estimate this by eye — validate.

### YouTube
- Descriptions cap at **5,000 UTF-8 bytes**, not 5,000 characters. Emoji are 4 bytes each and accented Latin is 2, so a description that is comfortably under 5,000 characters can still be rejected. If a description is emoji-heavy, assume the byte count is much higher than the character count.
- Titles cap at 100 characters.

### Instagram
- Captions cap at **2,200 characters**.
- `post_and_story` is not one publish — it **forks into two independent targets**. Either can fail on its own, so the result can be partial: the feed post lands and the story doesn't. Report per-target results, not a single verdict.

### TikTok
- Requires an explicit **music-usage agreement flag** on the payload. There is no sensible default for it because it is a legal acknowledgement — if the user has not given it, ask, don't assume.
- If the workspace's TikTok app is **unaudited**, posts are restricted to **`SELF_ONLY` visibility**. Anything else is rejected. This is a TikTok platform restriction, not a PulsarSocial one, and it cannot be worked around.
- **`post_mode`** picks what "posting" means, and the two are genuinely different outcomes:
  - `"direct"` (default) publishes to the profile.
  - `"draft"` uploads the media to the creator's **TikTok inbox** and posts nothing. They get a notification, open the TikTok app, and finish the post themselves — choosing the caption, sound and privacy in TikTok's own editor.
- In `draft` mode TikTok **ignores** `privacy_level`, `disable_comment`, `disable_duet`, `disable_stitch`, `auto_add_music` and both brand toggles, because the creator sets all of that themselves. Sending them is harmless but they will not take effect — so never tell the user you set a privacy level on a TikTok draft.
- A TikTok draft reaches `PUBLISHED` in PulsarSocial once the media is **delivered to the inbox**, which is not the same as being live on TikTok. There is no permalink, because no post exists yet. Say "sent to your TikTok drafts — open TikTok to finish it", never "posted".
- `post_mode: "draft"` is unrelated to `is_draft` and to the `draft` key scope. `is_draft` parks a post inside PulsarSocial; `post_mode` decides what TikTok does once it *is* sent. A post can be one, both or neither.

### Pinterest
- Requires a **`board_id`**. A pin with no board has nowhere to go. Get valid boards from the account's entry in `list_accounts` before building the payload.

### Facebook
- Page posts only. Personal profiles are not publishable via the platform API.

## Quota is per post, not per target

One post fanned out to six platforms is **one** unit of quota. Six separate single-platform posts are six.

So when a user wants the same content on Instagram, TikTok, X, YouTube, Pinterest and Facebook, put all six targets in **one** `create_post` call. Splitting it into six calls costs 6× the quota for an identical outcome, and gives the user six calendar entries to manage instead of one.

Split into separate posts only when the *content itself* genuinely differs per platform — a different caption for X than Instagram, say. Per-target caption overrides are usually the better answer even then; check `validate_post` output before deciding you need separate posts.

Use `get_usage` to check remaining quota before a large batch.

## Idempotency: reuse the key on retry

Mutating calls accept an `Idempotency-Key`.

- Generate one fresh UUID per **logical** post.
- On a retry — timeout, 5xx, network error, tool error you're unsure about — **send the exact same key again**. The server recognises it and returns the original result instead of creating a second post.
- Generating a new key on retry is how you double-post to someone's real audience. This is the most damaging mistake available in this API, and it is entirely avoidable.

If a `create_post` fails ambiguously and you did not set an idempotency key, do **not** blindly retry. Call `list_posts` first and check whether it actually landed.

## Media

Two paths:

- **`ingest_media`** — hand it a public URL and PulsarSocial fetches it server-side. Cap is 100 MB. This is the easy path and covers most cases.
- **Upload URL flow** (`/media/upload-url` then `/media/register` in REST) — for larger files or bytes you hold locally.

`list_media` shows what is already in the workspace library. Check there before re-ingesting the same asset.

Media has its own per-platform constraints (aspect ratio, duration, codec). `validate_post` checks these too, which is another reason to validate before creating.

## Reading and fixing posts

- `list_posts` — keyset paginated. Responses are `{ data: [...], next_cursor, has_more }`. Follow `next_cursor` while `has_more` is true; do not assume page numbers.
- `get_post` — full detail including per-target status.
- `reschedule_post` — move a scheduled post. Prefer this over delete-and-recreate; it keeps history and costs no extra quota.
- `cancel_post` — stop a scheduled post from publishing, keep the record.
- `delete_post` — remove it entirely.

## Errors

Errors come back as `{ error, code, request_id }`. Quote the `request_id` to the user when reporting a failure they may need support for.

| Status | Meaning | What to do |
| --- | --- | --- |
| 401 | Key invalid or revoked | Stop. The user needs a new key from Settings → API keys. |
| 402 `PLAN_REQUIRED` | Plan limit or key cap hit | Stop. Retrying will not help. |
| 403 `INSUFFICIENT_SCOPE` | Key's scope doesn't cover this | Stop. Do not try a different tool to get around it — tell the user their key is read/draft scoped. |
| 404 | Unknown id, or an id from another workspace | Re-run `list_accounts` / `list_posts`; a stale id from earlier in the conversation is the usual cause. |
| 422 | Validation failed | Fix the payload and `validate_post` again. |
| 429 | Rate limited | Back off for `Retry-After` seconds. Limits are per key per minute: 20 / 60 / 120 / 300 by plan. |

401, 402 and 403 are terminal. Report them and stop; they will not resolve by trying again.

## Things to just know

- A key is scoped to **one workspace**. `organization_id` is never a request parameter — the key determines the workspace, and you cannot address another one.
- Times: always confirm the user's intended timezone before scheduling. "Tuesday at 9" is ambiguous and a wrong timezone means a post at 3am.
- API access is on every plan, free included. Plans differ by rate limit: 20/min on Hobby, then 60 / 120 / 300.
- Full docs: <https://getpulsarsocial.com/docs>

Command line

Post, schedule and manage media straight from your terminal, or from a CI job.

Sign in

export PULSARSOCIAL_API_KEY=utsk_live_xxxxxxxxxxxx.your-secret-here

List your accounts

npx pulsarsocial accounts

Schedule & publish

Write the post as JSON, check it, then create it.

npx pulsarsocial validate post.json
npx pulsarsocial create post.json

validate writes nothing and consumes no quota, and exits non-zero if the post would be rejected — so the two chain safely with &&.

Install it once with npm install -g pulsarsocial and drop the npx.

All commands
CommandDoes
whoamiWorkspace, plan, scopes, rate limit
usageQuota used and remaining
accountsConnected accounts and their ids
postsScheduled and published posts
post <id>One post in full, per-target status
mediaThe workspace media library
ingest <url>Pull a public URL into the library
validate <file>Dry run — writes nothing
create <file>Create a post
cancel <id>Stop a scheduled post, keep the record
retry <id>Retry a failed post
reschedule <id> <time>Move it (ISO 8601)
delete <id>Remove it entirely

Options: --key, --base-url, --json, --idempotency-key.

Scripting and exit codes

--json emits the raw response instead of tables:

npx pulsarsocial posts --json | jq '.data[] | select(.status=="failed")'
ExitMeans
0Success
1Might work later — network, rate limit, server error
2Will never work as sent — bad key, wrong plan, insufficient scope, invalid post
create always sends an Idempotency-Key. If one is interrupted, pass the same value back with --idempotency-key and the server returns the original result rather than posting twice.

Connecting accounts is not in the API

Linking an Instagram, Facebook, TikTok, X, Pinterest or YouTube account is an interactive OAuth flow: the platform shows its own consent screen in a browser, and in several cases the person has to pick which Page, board or channel to grant. That cannot be driven headlessly, and shipping an API that pretends otherwise would just fail confusingly.

So: connect accounts once at getpulsarsocial.com, in the browser. After that they appear in list_accounts and in GET /api/v1/accounts, and everything else is programmable.

If an agent reports that it cannot find an account for a platform, that is the answer — the account has not been connected yet. There is no endpoint that would fix it.

Tools

Tools are filtered by the key’s scopes. A read-only key does not get refused when it calls create_post — it never sees the tool at all, which stops an agent from planning around a capability it does not have.

ToolDoesScope
list_accountsConnected accounts and their idsaccounts:read
list_mediaMedia already in the workspace librarymedia:read
ingest_mediaFetch a public URL server-side (100 MB cap)media:write
validate_postDry run. No writes, no quota, no live account touchedposts:read
create_postCreate a post across every target at onceposts:draft (+ posts:write to schedule)
get_postOne post, with per-target statusposts:read
list_postsKeyset-paginated post listposts:read
cancel_postStop a scheduled post, keep the recordposts:write
reschedule_postMove a scheduled postposts:write
delete_postRemove a post entirelyposts:write
get_usageQuota used and remainingusage:read

Why validate_post matters more than it sounds

Platform rules are inconsistent enough that a payload which looks obviously fine is often not. validate_post takes the exact object you would pass to create_post and reports what each target would reject — for free, and without publishing anything. Two or three validate round-trips before a create is normal.

Per-platform rules it catches

  • X — 280 weighted characters. Every URL counts as 23 no matter its real length, and CJK characters and most emoji count double. A 250-character draft plus two links does not fit.
  • YouTube — descriptions cap at 5,000 UTF-8 bytes, not characters. Emoji are four bytes each, so an emoji-heavy description can fail well under 5,000 characters. Titles cap at 100 characters.
  • Instagram — captions cap at 2,200 characters, and post_and_story forks into two independent targets that succeed or fail separately. Report per-target results, not one verdict.
  • TikTok — needs an explicit music-usage agreement flag, which is a legal acknowledgement and has no sensible default. Apps that have not completed TikTok’s audit are restricted to SELF_ONLY visibility; that is TikTok’s rule and cannot be worked around.
  • Pinterest — a pin needs a board_id. Valid boards come back with the account in list_accounts.
  • Facebook — Pages only. Personal profiles are not publishable through the platform API.

Quota is per post, not per target

One post fanned out to six platforms is one unit of quota. Six single-platform posts are six. Put every target in a single create_post call unless the content itself genuinely differs per platform — and even then, per-target overrides are usually the better answer than separate posts.

REST API

Everything the MCP server does is also plain HTTP, with identical shapes.

terminal
curl https://getpulsarsocial.com/api/v1/accounts \
  -H "Authorization: Bearer utsk_live_xxxxxxxxxxxx.your-secret-here"

Conventions

  • Base URL $https://getpulsarsocial.com/api/v1. Every request needs Authorization: Bearer utsk_live_….
  • Collections return { data: [...], next_cursor, has_more } and paginate by keyset. Follow next_cursor while has_more is true; there are no page numbers.
  • Errors return { error, code, request_id }. Quote the request_id when reporting a problem.
  • organization_id is never accepted in a request body. The key determines the workspace.
  • Mutating calls accept an Idempotency-Key header.

Endpoints

EndpointDescriptionScope
GET/meKey identity, workspace, plan, granted scopes
GET/usageQuota used and remaining for the periodusage:read
GET/accountsConnected social accounts and their idsaccounts:read
GET/mediaMedia librarymedia:read
POST/media/ingestFetch a public URL server-side. 100 MB capmedia:write
POST/media/upload-urlSigned URL for a direct upload. Larger filesmedia:write
POST/media/registerRegister an upload once the bytes have landedmedia:write
DELETE/media/:idDelete a media itemmedia:write
GET/postsList posts. Keyset paginatedposts:read
POST/posts/validateDry run. Writes nothing, no quotaposts:read
POST/postsCreate a post across all targetsposts:draft (+ posts:write to schedule)
GET/posts/:idOne post with per-target statusposts:read
POST/posts/:id/cancelStop a scheduled post, keep the recordposts:write
POST/posts/:id/rescheduleMove it. Cheaper than delete-and-recreateposts:write
POST/posts/:id/retryRetry failed targetsposts:write
DELETE/posts/:idDelete a postposts:write

Validate, then create

terminal
curl https://getpulsarsocial.com/api/v1/posts/validate \
  -H "Authorization: Bearer utsk_live_xxxxxxxxxxxx.your-secret-here" \
  -H "Content-Type: application/json" \
  -d '{
    "caption": "Launching Tuesday. Here is what changed.",
    "scheduled_at": "2026-08-04T09:00:00-05:00",
    "targets": [
      { "social_account_id": "acc_2f9…", "kind": "post" },
      { "social_account_id": "acc_7c1…", "kind": "post" }
    ]
  }'

A clean validate returns no target errors. Then send the same body to POST /api/v1/posts with an Idempotency-Key.

With a draft-scoped key, that create saves a draft and does not schedule it. The response says so. Read the returned status rather than treating 200 as “scheduled”.

Posting immediately

There is no post_now flag — set scheduled_at to the current time. A timestamp a few minutes in the past is accepted and treated as due now, so a client clock running slightly behind the server does not fail; older than that returns SCHEDULED_AT_IN_PAST.

Immediate does not mean instant. Media is validated, uploaded to each platform and encoded there before anything goes live, so allow seconds to minutes depending on file size. Staging also begins about 8 minutes ahead of scheduled_at, so a post scheduled sooner than that starts uploading straight away.

TikTok drafts

A TikTok target accepts options.post_mode. The default direct publishes to the profile; draftuploads the media to the creator’s TikTok inbox and posts nothing — they finish it in the TikTok app, choosing the caption, sound and privacy themselves.

In draft mode TikTok ignores privacy_level and every interaction toggle, and there is no permalink because no post exists yet. The target reaching PUBLISHED means the media was delivered to the inbox, not that it is live.

Media

Two ways to get bytes into a post.

  • Ingest a URL POST /media/ingest (or the ingest_media tool) fetches a publicly reachable URL server-side. 100 MB cap. This covers most cases.
  • Direct upload POST /media/upload-url returns a signed URL, you PUT the bytes to it, then POST /media/register to add it to the library. Use this for larger files or local bytes.

Check list_media before re-ingesting an asset you already have. Media carries its own per-platform constraints — aspect ratio, duration, codec — and validate_post checks those too, which is one more reason to validate before creating.

Scopes

A key carries one of three presets. Pick the least it needs — an agent cannot exceed its key, so the scope is the actual safety boundary, not the prompt.

read

Look, don’t touch

Accounts, posts, media and usage, read-only. Mutating tools are not even listed.

draft

Default · recommended

Everything read can do, plus create_post — but posts are saved as drafts for a human to approve, never scheduled.

write

Publishes for real

Schedules and publishes to live accounts, and can cancel, reschedule and delete. Give this out deliberately.

Presets expand into the granular scopes named in the tables above — accounts:read, media:write, posts:draft, posts:write, usage:read. GET /api/v1/me returns exactly what a key was granted.

Draft behaviour is the part agents get wrong. A 200 from create_post on a draft key means “saved as a draft”, not “scheduled”. The response distinguishes them; anything reporting back to a human should read it.

Plans & rate limits

PlanAPI accessRequests / min / keyKeys
Hobby — freeYes203
Starter — $19Yes6010
Growth — $49Yes12025
Scale — $99Yes300100

Rate limits are per key per minute, so splitting traffic across two keys on the same workspace genuinely doubles headroom — up to the key cap. Exceeding a cap returns 402 PLAN_REQUIRED at key-creation time, not at request time.

A rate-limited response carries Retry-After plus RateLimit-* headers. Honour Retry-After rather than retrying on a fixed timer.

Posting quota is separate from the rate limit, and it counts posts, not targets or requests. One post to six platforms is one unit. Check GET /api/v1/usage before a large batch.

Idempotency

Every mutating call accepts an Idempotency-Key header. Use it, and reuse it on retry.

terminal
curl https://getpulsarsocial.com/api/v1/posts \
  -H "Authorization: Bearer utsk_live_xxxxxxxxxxxx.your-secret-here" \
  -H "Idempotency-Key: 6f1c9e2a-4b77-4a10-9d3e-8f2b5c0a71de" \
  -H "Content-Type: application/json" \
  -d @post.json
  • Generate one fresh UUID per logical post.
  • On a retry — timeout, 5xx, dropped connection — send the same key again. The server returns the original result instead of creating a second post.
  • Generating a new key on retry is how you double-post to a real audience. It is the most damaging mistake available here and the easiest to avoid.
  • If a create failed ambiguously and no key was sent, do not blindly retry — call GET /api/v1/posts and check whether it landed.

Errors

Every error is { error, code, request_id }. 401, 402 and 403 are terminal — retrying will not resolve them.

StatusCodeMeaningDo
401UNAUTHORIZEDKey invalid, revoked or deletedStop. Issue a new key in Settings → API keys.
402PLAN_REQUIREDPlan does not include API access, or a cap was hitStop. Upgrade or free up a key slot.
403INSUFFICIENT_SCOPEThe key’s scopes do not cover this callStop. Do not route around it with another endpoint.
404NOT_FOUNDUnknown id, or an id belonging to another workspaceRe-list. A stale id from earlier is the usual cause.
422VALIDATION_FAILEDPayload rejected, with per-target detailFix it and run validate again.
429RATE_LIMITEDPer-key per-minute limit exceededBack off for Retry-After seconds.

Cross-workspace ids return 404 rather than 403 on purpose — a key should not be able to probe for the existence of resources it cannot reach.

Machine-readable summary

Point an agent at /docs/llms.txt for a plain-text version of this page.

Open llms.txt