Authentication
Every request to the REST API and to the MCP server carries an API key as a bearer token.
curl https://reel.money/api/v1/account \
-H "Authorization: Bearer rm_your_key"
A key is created in ReelMoney under Settings, then API keys, at https://reel.money/brands/<brand_id>/api_keys. The token is shown once, at creation. If it is lost, revoke it and make another one.
Facts worth knowing before you build:
- A key belongs to one brand and acts on that brand by default. Only a key carrying the
brands:allscope can reach the account's other brands, per request, withbrand_id. - Plan limits, quota and credits are the brand owner's.
GET /api/v1/accountreports them. - A brand can hold up to ten keys, so give each agent its own and revoke the one that misbehaves.
- Keys can carry an expiry date. An expired key answers
key_expired, a revoked key answerskey_revoked. - No CORS headers are sent, on purpose. Call the API from a server or an agent runtime, never from a browser page where the key would be readable.
Scopes
A key carries one or more scopes, ticked when it is created. A scope it does not carry is a wall, not a warning.
| Scope | What it allows |
|---|---|
brand:read | Read your brand profile, plan, quota, videos and slideshows. |
media:read | Browse your media library, media packs and stock media. |
media:write | Upload files and add them to your media library. |
content:write | Create and edit slideshows and videos, and start renders. |
posts:read | Read your posts and scheduled posts, and run post checks. |
posts:write | Publish and schedule posts, and cancel scheduled posts. |
analytics:read | Read post metrics and account analytics. |
brands:all | Work across every brand this account owns, choosing the brand per request. |
Publishing is deliberately separate. A key with content:write but no posts:write can research, generate and render all day and still cannot put anything in front of an audience. That is the setting to start an agent on.
Each endpoint names the scope it needs in /openapi.json as x-required-scope, and the endpoint reference repeats it per group.
One key, many brands
brands:all is the opt in exception to the one key, one brand rule. A key that carries it may act on any brand the same account owns: send the optional brand_id query parameter (or the X-Brand-Id header, the parameter winning) on any brand facing endpoint, with an id from GET /api/v1/brands. Leave brand_id out and the key works on its own brand, exactly as every key always has, so nothing changes for an existing integration. Without the scope, a brand_id naming a different brand answers 403 missing_scope with brands:all in details.required_scope, and an id the account does not own answers 404 not_found, whatever the scopes. Quota, credits and rate limits stay the key owner's in every case.
Keys made before this vocabulary
Older keys carry read, write or admin. They keep working exactly as they did and are read like this:
| Old scope | Reads as |
|---|---|
read | brand:read, media:read, posts:read, analytics:read |
write | content:write, media:write, posts:write |
admin | brand:read, media:read, media:write, content:write, posts:read, posts:write, analytics:read, brands:all |
A key with no scopes at all is treated as read. It can never write. Edit such a key in the dashboard to tick the permissions you want instead.
What a failure looks like
A missing or bad token answers 401 with an RFC 6750 challenge header, which is what an OAuth capable client looks for:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="ReelMoney", error="invalid_token"
{
"success": false,
"errors": [
{ "code": "invalid_token", "message": "The API key is not valid.", "retryable": false }
]
}
A valid key without the right scope answers 403 and names the scope it wanted, so an agent can tell a person exactly which box to tick:
{
"success": false,
"errors": [
{
"code": "missing_scope",
"message": "The API key does not have the permission this endpoint requires.",
"retryable": false,
"details": {
"required_scope": "posts:write",
"key_scopes": ["brand:read", "content:write"]
}
}
]
}
Branch on code, never on the message text. Messages are written for people and may be reworded. Codes are a contract. See the error reference.
Response shape
Every success looks like this:
{ "success": true, "data": { }, "meta": { } }
Every failure looks like this:
{ "success": false, "errors": [{ "code": "quota_exceeded", "message": "...", "retryable": false }] }
List endpoints take page and per_page (1 to 50, 25 by default) and report meta.pagination with page, per_page, total_pages and total_count.
Rate limits
Ceilings are counted per key, not per account, so one noisy agent cannot starve another.
| Bucket | Ceiling per key |
|---|---|
| Every endpoint | 300 requests per 5 minutes |
| Generation (create slideshow, render slideshow, create video) | 20 requests per 1 hour |
| Posting (publish or schedule) | 30 requests per 1 hour |
Every response carries the current bucket in headers:
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 287
X-RateLimit-Window: 300
Over a ceiling the API answers 429 rate_limited with a Retry-After header and details.retry_after_seconds. Wait that long, then continue. Do not spin.
Social platforms enforce their own separate caps. Those surface as platform_rate_limited, and POST /api/v1/posts/validate reports the headroom left per account before you spend it.
Idempotency keys
The create endpoints (POST /api/v1/slideshows, POST /api/v1/slideshows/{id}/render, POST /api/v1/videos, POST /api/v1/posts) accept an Idempotency-Key header. Send a fresh unique value, such as a UUID, per logical action.
curl -X POST https://reel.money/api/v1/slideshows \
-H "Authorization: Bearer rm_your_key" \
-H "Idempotency-Key: 5a1f9b2c-7f1e-4c73-9c6d-1b2f3a4d5e6f" \
-H "Content-Type: application/json" \
-d '{ "context": "..." }'
- Same key, same body: the stored response is replayed with
meta.idempotent_replayset to true. Nothing runs twice. - Same key, different body: 409
idempotency_conflict. One key belongs to one request forever. - Same key while the first request is still running: 409
idempotency_conflictwithdetails.in_flight. - No header: nothing is stored and the request behaves normally.
Keys are scoped to the API key and the exact path, and they are kept for 24 hours. This is what makes a timeout safe to retry: send the same key again and you get the first answer instead of a second slideshow.
Housekeeping
- Rotate a key by creating the new one, moving the agent over, then revoking the old one.
- Revoking is instant.
last_used_atand the request count on each key show what an agent has actually been doing. - If a key leaks, revoke it first and ask questions after. There is no way to unpublish a post that has already gone out.
Reading this as an agent? Every page is also plain markdown: /api-docs/authentication.md