The Reelspan API is plain HTTPS and JSON. This page covers the rules every endpoint shares; each endpoint's own page covers its fields.
Base URL
https://api.reelspan.com/v1Every endpoint lives under /v1. The machine-readable OpenAPI document describes all of them, and the API reference is built from it.
Send API keys to
api.reelspan.com. The dashboard's own address (app.reelspan.com) answers a request made with an API key with 421 anderror_code: "USE_API_HOST", naming the right address.
Authentication
Send your key in the Authorization header on every request:
curl https://api.reelspan.com/v1/me \
-H "Authorization: Bearer $REELSPAN_API_KEY"Authorization: Apikey $REELSPAN_API_KEY works too, as Upload-Post clients send it. Create keys in the dashboard under Developers → API keys. A key can be limited in two ways:
- Scopes:
publish,read,profiles,analyticsandwebhooks. A call outside the key's scopes gets 403 witherror_code: "INSUFFICIENT_SCOPE". - Profiles: a key limited to some profiles gets 403
PROFILE_FORBIDDENfor any other profile, and posts or jobs of other profiles answer 404 as if they didn't exist.
A missing, wrong, revoked or expired key gets 401. After 10 failed attempts from one IP address in 5 minutes, that address gets 429 with error_code: "AUTH_RATE_LIMITED" and Retry-After: 300. More in Authentication.
Request formats
| Endpoint | Body |
|---|---|
POST /v1/posts/video, /photos, /document |
multipart/form-data: files and form fields together |
POST /v1/posts/text, POST /v1/youtube/thumbnail |
JSON or multipart/form-data |
POST /v1/media/upload |
multipart/form-data, one field named file |
| Everything else | JSON body (Content-Type: application/json) or query parameters |
In multipart forms:
- Lists repeat the field with
[]in its name:-F "platform[]=tiktok" -F "platform[]=youtube". - Booleans are the strings
trueorfalse. - Media can be a file or a public https URL in the same field, or the
media_idof a file you uploaded earlier.
In JSON (text posts), send the list as "platform": ["x", "bluesky"]; "platform[]" works too.
Responses
A successful answer has "success": true and the data at the top level:
{ "success": true, "request_id": "9b1f6c2e8a3d4f50b7e1c9a2d6f3e8b1", "results": { "…": "…" } }An error has "success": false, a readable message, and usually a machine-readable error_code to branch on. Some errors add fields, such as usage on a monthly-limit error or invalid_platforms when none of the networks can be used:
{ "success": false, "message": "This API key does not have the \"publish\" scope", "error_code": "INSUFFICIENT_SCOPE" }An unexpected server error is { "success": false, "error": "Internal server error" } with 500. Errors lists the codes.
Per-network results
A post to several networks never fails as a whole because one network did. When a post finishes within the request, results holds one object per network:
{
"success": true,
"request_id": "9b1f6c2e8a3d4f50b7e1c9a2d6f3e8b1",
"results": {
"tiktok": { "success": true, "url": "https://www.tiktok.com/@mybrand/video/7441…", "post_id": "7441…" },
"x": { "success": true, "url": "https://x.com/mybrand/status/18…", "post_id": "18…", "warnings": ["The text was posted as a thread of 2 posts."] },
"pinterest": { "success": false, "error": "…", "error_code": "…" }
},
"usage": { "count": 41, "limit": null, "last_reset": "2026-11-01T00:00:00.000Z" }
}success,urlandpost_idsay where it went live.erroranderror_codesay why it didn't.warningslists anything changed on the way (links removed on X, a long text split into a thread, a first comment that couldn't be posted).skipped: truewith askip_reasonmeans the network was never tried, for example because the profile has no account on it. Skipped networks aren't charged.
The top-level success only says the request was accepted, so always read each network's own success. Post status returns the same results as a list.
Status codes
| Code | Meaning |
|---|---|
200 |
Done. For a post: finished (with results) or moved to the background (with request_id). |
201 |
Created (a profile, an approval link). |
202 |
Scheduled or queued: the answer carries a job_id. |
400 |
Something in the request is wrong; message says what. |
401 |
No key, or the key is invalid, revoked or expired. |
403 |
The key lacks the scope or the profile, or the workspace is suspended. |
404 |
The profile, post, job or file doesn't exist (or the key can't see it). |
409 |
A conflict: the same Idempotency-Key still running, a request_id already used, a scheduled post already publishing, nothing to retry, or an account that must be reconnected (reauth_required). |
413 |
The file is larger than your plan allows. |
421 |
An API key was sent to the dashboard's address instead of api.reelspan.com. |
429 |
Rate limit, monthly upload limit, or the daily test-post limit. |
500 |
Reelspan's fault. Safe to retry with an Idempotency-Key. |
502 |
A network refused or didn't answer: a lookup (Pinterest boards, TikTok creator info, Facebook Page details…) or a change to a live post (unpublish, edit, repost). error_code says why. |
Idempotency
Networks drop, and a retried POST could publish twice. Send an Idempotency-Key header (any string up to 200 characters, such as a UUID) on the four publish endpoints, and a repeat with the same key returns the first request's result instead of posting again:
curl -X POST https://api.reelspan.com/v1/posts/text \
-H "Authorization: Bearer $REELSPAN_API_KEY" \
-H "Idempotency-Key: 6f1c2a7e-weekly-tip-48" \
-H "Content-Type: application/json" \
-d '{"user": "my-brand", "platform": ["x"], "title": "Weekly tip"}'- A key is remembered for 24 hours, per workspace and per kind of post (video, photos, text, document).
- A replay answers with the first request's current state, the header
Idempotent-Replayed: trueand"idempotent_replay": truein the body. - While the first request with a key is still open (including its wait of up to 59 seconds for results), a second one gets 409
IDEMPOTENCY_KEY_IN_USE. Wait a few seconds and send it again. X-Idempotency-Keyworks as another name for the header.X-Request-Idis a third name, used only when neither of the others is sent, and it also becomes the post'srequest_id(below).
You can also pick the post's id yourself: send request_id (8 to 100 letters, digits, _ or -), as a field or as the X-Request-Id header, and you can poll it even if the answer never arrived. A header in any other shape (a proxy's own trace id, say) is ignored. Reusing a request_id gets 409 REQUEST_ID_IN_USE. The SDKs add an Idempotency-Key to every post for you. See Retrying safely.
Rate limits
Each workspace has a request limit per minute and per 10 minutes, set by its plan (Plans and limits). Every answer to an API-key request carries the window closest to running out:
| Header | Meaning |
|---|---|
X-RateLimit-Limit |
Requests allowed in the window |
X-RateLimit-Remaining |
Requests left in it |
X-RateLimit-Reset |
When it resets, in Unix seconds |
Past the limit you get 429 with error_code: "RATE_LIMITED" and a Retry-After header in seconds. The monthly upload limit (Free plan) is separate: a post that would go over it gets 429 with your usage. See Rate limits.
Paging
List endpoints page in one of three ways:
| Endpoint | Parameters | Done when |
|---|---|---|
History GET /v1/posts |
page (from 1) and limit (10, 20, 50 or 100) |
page × limit ≥ total |
Scheduled posts GET /v1/scheduled |
offset and limit (1 to 200, default 50) |
offset + limit ≥ total |
Media library GET /v1/library |
limit (1 to 100) and before |
next_before is null; otherwise pass it as before |
Webhook deliveries and approval links return the newest entries only, without pages.
Ids
| Id | Looks like | What it names |
|---|---|---|
request_id |
9b1f6c2e8a3d4f50b7e1c9a2d6f3e8b1 (or your own) |
One post request, across all its networks. Use it for status, retry, history and post analytics. |
job_id |
sched_5d0e… |
A scheduled or queued post. Use it to read, edit or cancel it. A scheduled post has a request_id too. |
media_id |
med_… |
A file uploaded with Upload media, to post or keep in the library. |
external_id |
yours | Any id from your own system (up to 255 characters), sent with a post (as a field or the X-External-Id header) and returned in history, status and webhooks. |
Test keys
A test key (rs_test_…) runs every check a live key does, then simulates the post: each network comes back success: true with a post_id that starts with test_ and no url, and status answers carry "test": true. Nothing reaches the networks and nothing counts against your plan. A workspace can send 500 test posts a day; past that the answer is 429 TEST_LIMIT.
A test post still needs the networks connected to the profile, as a real post would. To try things with nothing connected, post to the sandbox network. See Test mode.