Agent REST API
Keys, scopes, drafts, publishing, webhooks, limits and errors for the /api/agent/v1 REST API.
The Agent REST API lets scripts and agents read your brands, create drafts and schedule them, with a key you control. It sits beside the MCP connector: use the connector inside an AI chat, and the REST API from your own code.
- Base URL:
https://ops.rebrandos.com/api/agent/v1 - OpenAPI 3.1 description: https://ops.rebrandos.com/api/agent/v1/openapi.json
- JSON in and out. Errors are
application/problem+json. - Every plan includes the API, including during your free trial.
/generate/* and POST /repurpose endpoints answer 410 with brandos_generation_retired. For images, carousels and video, use the MCP connector or the dashboard.Quickstart
1. Create a key
- Open Settings → Agent access keys and choose Create key.
- Give it a label, pick a preset — Read-only analyst, Content agent or Publishing agent — and adjust the scopes.
- Under Brands, keep All brands (current + future) or choose Specific brands only. Pick an expiry and, optionally, an IP allowlist.
- Copy the key. It is shown once and starts with
rbos_live_.
Only owners and admins can issue a key with publish:execute, accounts:manage or automations:execute. When an operator asks for one, it waits under Pending key requests until an owner or admin approves it.
2. Check the key
curl https://ops.rebrandos.com/api/agent/v1/whoami \ -H "Authorization: Bearer $REBRANDOS_KEY"
{
"ok": true,
"keyId": "brandos_agent_key_6b1d…",
"label": "Publishing bot",
"organizationId": "org_northwind",
"scopes": ["brands:read", "accounts:read", "drafts:read", "drafts:write", "schedule:read", "publish:execute"],
"brands": ["northwind-coffee"],
"expiresAt": "2027-01-17T09:00:00.000Z",
"env": "live"
}brands is null when the key may use every brand.
3. Find the brand and channel
curl "https://ops.rebrandos.com/api/agent/v1/brands" \ -H "Authorization: Bearer $REBRANDOS_KEY"
{ "ok": true, "data": [{ "id": "northwind-coffee", "name": "Northwind Coffee", "status": "active" }], "nextCursor": null, "total": 1 }curl "https://ops.rebrandos.com/api/agent/v1/accounts" \ -H "Authorization: Bearer $REBRANDOS_KEY"
{ "ok": true, "data": [{ "brandId": "northwind-coffee", "platform": "linkedin", "account": "Northwind Coffee" }], "nextCursor": null, "total": 1 }Use a brand’s id as brand, and an account’s platform as channel.
4. Create a draft
curl -X POST https://ops.rebrandos.com/api/agent/v1/drafts \
-H "Authorization: Bearer $REBRANDOS_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: draft-2026-10-19-onboarding" \
-d '{"brand":"northwind-coffee","channel":"linkedin","title":"Onboarding week","content":"Week one at Northwind starts with a cup…"}'201 Created
{ "ok": true, "draft": { "id": "…", "title": "Onboarding week", "brand": "northwind-coffee", "channel": "linkedin", "status": "draft" } }5. Publish
Try it with dryRun first: you get the plan, the account’s capacity check and your remaining daily posts, and nothing is scheduled.
curl -X POST https://ops.rebrandos.com/api/agent/v1/publish \
-H "Authorization: Bearer $REBRANDOS_KEY" \
-H "Content-Type: application/json" \
-d '{"draftId":"DRAFT_ID","scheduledAt":"2026-10-20T09:00:00+01:00","dryRun":true}'Then send the same request without dryRun, with an Idempotency-Key:
201 Created
{
"ok": true,
"scheduled": { "scheduleId": "…", "draftId": "…", "brand": "northwind-coffee", "channel": "linkedin", "scheduledAt": "2026-10-20T08:00:00.000Z" },
"dailyCap": { "used": 1, "limit": 10, "remaining": 9, "resetAt": "2026-10-19T24:00:00Z" }
}scheduledAt must be in the future and include a time zone; leave it out to schedule five minutes from now. Follow the post with GET /publish-jobs and GET /publish-receipts, or a webhook.
POST /publish schedules straight away within the key’s limits. To make a person approve every agent post, an admin turns on Require approval before agents publish in Settings → Agent access keys. Then POST /publish returns 202 with "status": "pending_approval", and the post waits under Pending agent actions.Endpoints
| Method and path | Scope | What it does |
|---|---|---|
GET /whoami | — | The key’s identity, scopes, brands and expiry. |
GET /brands | brands:read | Brands the key can reach. |
GET /brand-profile?brandId= | brands:read | The brand’s positioning, audience, voice and pillars. |
GET /content-outcomes?brandId= | brands:read | The brand’s content outcomes and the outcome catalog. |
GET /hook-devices | brands:read | The hook-device catalog. |
GET /pillars?brandId= | pillars:read | The brand’s content pillars. |
GET /viral-formats | pillars:read | Post formats, with a recommendation from the brand’s results when you pass brandId. |
GET /accounts | accounts:read | Connected accounts per brand. |
GET /drafts | drafts:read | Drafts, optionally for one brandId. |
POST /drafts | drafts:write | Create a text draft. |
PATCH /drafts/{id} | drafts:write | Change title, content, channel or postType. POST works too. |
DELETE /drafts/{id} | drafts:write | Delete a draft. |
POST /publish | publish:execute | Schedule a draft. |
GET /publish-jobs | schedule:read | Scheduled and queued posts. |
POST /publish-jobs/{id}/cancel | schedule:write | Cancel a queued post before it starts. Works even when the workspace is read-only. |
GET /publish-receipts | analytics:read | What each network reported after publishing. |
GET /analytics | analytics:read | Performance snapshots per brand. |
GET /media | media:read | Files in a brand’s library. |
POST /subtokens | — | Mint a narrower key for a sub-agent. |
GET, POST /webhooks | webhooks:manage | List or create webhook subscriptions. |
DELETE /webhooks/{id} | webhooks:manage | Delete a subscription. |
GET /webhooks/{id}/deliveries | webhooks:manage | The last 50 delivery attempts. |
List endpoints take limit (1–200, default 50) and cursor, and return data, nextCursor and total. Pass nextCursor back as cursor until it is null. Endpoints that read social publishing data also need the workspace’s social publishing module to be active.
Authentication and scopes
Send the key in the Authorization: Bearer header on every request; keys in the URL are ignored. Keys look like rbos_live_ followed by 36 letters and digits that end in a checksum, so secret scanners can spot them.
A key can never do more than the person who created it can do right now. If that person loses access to the workspace, the key stops working (issuer_no_access); if they lose write access, its write scopes stop (issuer_cannot_write).
The scopes the REST API checks are in the endpoint table. Keys can also hold the connector’s scopes (personas, calendar, schedule:write, media:write, automations), which the REST endpoints don’t use. billing:* and keys:* can never be granted to a key.
Brand restriction
A key limited to specific brands only lists those brands, their accounts, drafts, jobs, receipts and media. Naming another brand returns 403 brand_not_permitted, and its webhooks only receive events about its own brands.
Expiry, rotation and revocation
- Every key expires. Choose 7, 30 or 90 days or 1 year when you create it. After that, requests return
401 expired. - Rotate from the key’s row in Settings. You get a new key straight away and the old one keeps working for 24 hours (
401 rotatedafter that). Rotation keeps the scopes, brands, limits and expiry. - Revoke to stop a key immediately. Its sub-tokens stop too, and so do its webhooks.
- ReBrandOS can revoke a leaked key automatically when a secret-scanning service reports it. For keys pushed to a code host, that needs ReBrandOS to be registered as a secret-scanning partner with that host (for example GitHub’s partner program), so don’t rely on it: if a key leaks, revoke it yourself straight away.
- Pause all agent access stops every key, connector and webhook in the workspace (
403 killed) until an admin turns it off.
IP allowlist
Add IPv4 or IPv6 addresses or CIDR ranges, separated by commas or spaces, for example 203.0.113.0/24, 2001:db8::/32. Requests from anywhere else return 403 ip_not_allowed. Leave it empty to allow any address. An entry that isn’t a valid address or range is rejected when you save, rather than being dropped.
Sub-tokens
Give a sub-agent a narrower key without creating one in Settings. A sub-token can only narrow its parent: fewer scopes, fewer brands, lower limits, an IP range inside the parent’s, an earlier expiry. Revoking, rotating or expiring the parent disables it, and a sub-token cannot mint its own.
curl -X POST https://ops.rebrandos.com/api/agent/v1/subtokens \
-H "Authorization: Bearer $REBRANDOS_KEY" \
-H "Content-Type: application/json" \
-d '{"label":"Caption checker","scopes":["drafts:read"],"brandIds":["northwind-coffee"],"caps":{"rateLimitPerMin":60},"expiresAt":"2026-10-26T00:00:00Z"}'201 Created
{
"ok": true,
"token": "rbos_live_…",
"apiKey": { "id": "…", "label": "Caption checker", "scopes": ["drafts:read"], "brandIds": ["northwind-coffee"],
"caps": { "rateLimitPerMin": 60, "publishesPerDay": 10, … }, "expiresAt": "2026-10-26T00:00:00.000Z",
"env": "live", "parentKeyId": "…" }
}Rate limits and fair use
- 300 requests a minute per key; sub-tokens share their parent’s budget. Every response carries
RateLimit-Limit,RateLimit-RemainingandRateLimit-Reset(Unix seconds). Over the limit you get429 rate_limitedwith aRetry-Afterheader in seconds. - 10 scheduled posts a day per key, shared with its sub-tokens, reset at 00:00 UTC. Successful
POST /publishresponses includedailyCap; over the cap you get429 publish_cap_reachedwithresetAt. Dry runs and posts waiting for approval don’t count. - A key can have lower limits than these, never higher. If you need more, contact us; see the fair-use section of the Terms.
- Request bodies are limited to 1 MB.
Idempotency
POST /drafts and POST /publish accept an Idempotency-Key header, scoped to your key. Retrying with the same key and the same body returns the first response instead of creating a second draft or post. The same key with a different body returns 409 idempotency_conflict; a retry while the first request is still running returns 409 idempotency_in_progress. Use a fresh key for each new draft or post, and the same key when you retry after a timeout.
Webhooks
Instead of polling, subscribe an https endpoint on a public host (private, loopback and link-local addresses are refused). The response includes the signing secret, once.
curl -X POST https://ops.rebrandos.com/api/agent/v1/webhooks \
-H "Authorization: Bearer $REBRANDOS_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://hooks.example.com/rebrandos","events":["post.published","post.failed"]}'201 Created
{
"ok": true,
"webhook": { "id": "…", "url": "https://hooks.example.com/rebrandos", "events": ["post.published", "post.failed"], "status": "active" },
"secret": "whsec_…",
"signatureScheme": "HMAC-SHA256 over `${X-RebrandOS-Timestamp}.${rawBody}`; compare X-RebrandOS-Signature (sha256=...)"
}Events
| Event | Sent when | data |
|---|---|---|
draft.created | A key creates a draft. | draftId, brand, channel, title, source |
draft.updated | A key updates a draft. | draftId, brand |
post.scheduled | A post is scheduled through the API or connector, or a person approves a pending agent publish. | draftId, brand, channel, scheduledAt, scheduleId, source |
post.published | A network confirms a post went live (any post in the workspace). | brand, channel, provider, status, publishJobId, publishReceiptId, providerPostId, permalinkUrl |
post.needs_action | A scheduled post was stopped before it was sent, so nothing was posted. A person must act: choose its account, review it, or pick a new time. Sent once each time a post is stopped. | brand, channel, status, publishJobId, needsAction (kind, reasonCode, reason, fix, scheduledAt, pastDue) |
post.failed | A network reports a failed post, or a person denies a pending agent publish (then data has decision: "denied"). A stopped post is post.needs_action, not post.failed. | The same fields as post.published, or draftId, brand, channel, decision |
key.revoked | Someone revokes a key in Settings. | apiKeyId |
Omit events to receive all of them. Each event is sent once; TikTok inbox deliveries are not publications and send nothing.
Request format and signature
POST /rebrandos HTTP/1.1
Content-Type: application/json
X-RebrandOS-Event: post.published
X-RebrandOS-Timestamp: 1760950800
X-RebrandOS-Signature: sha256=5d0c…
{"id":"evt_…","type":"post.published","createdAt":"2026-10-20T08:00:03.000Z","data":{ … }}The signature is a hex HMAC-SHA256 of the timestamp, a dot, and the raw request body, keyed with your secret. Verify it before you parse the body:
import crypto from 'node:crypto';
export function verifyRebrandosWebhook(headers, rawBody, secret) {
const timestamp = headers['x-rebrandos-timestamp'] || '';
const received = headers['x-rebrandos-signature'] || '';
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`, 'utf8')
.digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300; // our suggestion: 5 minutes
return fresh
&& received.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}Delivery and retries
- Answer with any 2xx within 8 seconds. Redirects are not followed.
- Network errors, 5xx and 429 are retried up to 4 attempts in total, a fraction of a second apart. Other 4xx responses are not retried.
- After 15 failed deliveries in a row the subscription is disabled.
GET /webhooksshows itsstatusandfailureCount, andGET /webhooks/{id}/deliveriesshows each attempt. - A subscription stops receiving when the key that created it is revoked or expires, and while agent access is paused.
When your plan changes
| Subscription | What the API allows |
|---|---|
| Workspace created, no card added yet | Reads only (subscription_required). Add a card in Billing to start the free trial. |
| Trial running (card on file) | Everything your role and scopes allow. |
| Trial cancelled, before it ends | Everything, until the trial ends. No charge. |
| Paid and active | Everything your role and scopes allow. |
| Payment failed, first 7 days | Everything, for 7 days from the first failed payment. The app shows a banner with the date. |
| Payment still failed after 7 days | Read-only. Writes return entitlement_blocked with reason subscription_inactive. |
| Cancelled, before the end of the paid period | Everything, until the paid-through date. |
| Cancelled, after the paid period | Read-only (subscription_inactive). |
| Trial ended without a plan (cancelled, or an older trial without a card) | Reads only (subscription_inactive or trial_expired). |
| No subscription yet | Reads only (subscription_required). |
Blocked writes return 402 entitlement_blocked with a reason. Reads, cancelling queued posts (POST /publish-jobs/{id}/cancel), deleting drafts, sub-tokens and webhook management keep working, and an owner can export the workspace’s data from the dashboard.
Errors
Errors use RFC 9457 problem details with the content type application/problem+json. code is stable; type links to the code’s row below; some codes add fields such as requiredScope, brandId, reason, limit or resetAt.
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
{
"type": "https://www.rebrandos.com/docs/api#error-insufficient_scope",
"title": "insufficient scope",
"status": 403,
"code": "insufficient_scope",
"detail": "This key lacks the publish:execute scope.",
"requiredScope": "publish:execute",
"hint": "Recreate the key with the needed scope."
}| Status | Code | Meaning |
|---|---|---|
| 400 | brand_required | The request needs a brand. Send brand in the body (drafts) or brandId in the query. |
| 400 | draft_required | POST /publish needs a draftId. |
| 400 | channel_required | The draft has no channel. Send channel with the publish request. |
| 400 | invalid_scheduled_at | scheduledAt is not an ISO 8601 date-time with a time zone. |
| 400 | scheduled_at_in_past | scheduledAt must be in the future. |
| 400 | draft_update_failed | The draft could not be updated. detail says why. |
| 400 | unsafe_url | Webhook URLs must be https on a public host. Private, loopback and link-local addresses are refused. |
| 400 | webhook_invalid | The webhook could not be created. events lists the event names you can subscribe to. |
| 400 | at_least_one_scope_required | A sub-token needs at least one scope. |
| 400 | scope_exceeds_parent | A sub-token asked for a scope its parent key does not hold. |
| 400 | brand_exceeds_parent | A sub-token asked for a brand its parent key cannot reach. |
| 400 | ip_allowlist_exceeds_parent | A sub-token’s IP allowlist must sit inside its parent’s allowlist. |
| 400 | cannot_derive_from_subtoken | Sub-tokens cannot mint their own sub-tokens. Derive from the parent key. |
| 400 | expires_at_must_be_future | A sub-token’s expiresAt must be in the future. |
| 400 | expires_at_invalid | A sub-token’s expiresAt is not a valid ISO 8601 date-time. |
| 400 | agent_key_scope_invalid | A sub-token asked for a scope that does not exist. scopes lists them. |
| 400 | parent_expired | The parent key has expired, so it cannot mint sub-tokens. |
| 400 | parent_rotated | The parent key was rotated and its grace period has ended. |
| 400 | keys_killed | Agent access is paused for the workspace, so no sub-tokens can be minted. |
| 401 | missing_key | No Authorization: Bearer header was sent. |
| 401 | invalid_key | The value is not a well-formed ReBrandOS key (prefix or checksum is wrong). |
| 401 | not_found | The key is well-formed but does not exist. (not_found with status 404 means the endpoint does not exist.) |
| 401 | revoked | The key was revoked, by a person or by secret scanning. |
| 401 | expired | The key is past its expiry date. Create a new one. |
| 401 | rotated | The key was rotated and its grace period has ended. Use the new key. |
| 401 | restriction_invalid | The key’s stored restrictions could not be read, so it fails closed. Create a new key. |
| 401 | parent_revoked | This sub-token’s parent key was revoked, rotated or expired. |
| 402 | entitlement_blocked | The workspace’s plan does not allow this write right now. reason is trial_expired, subscription_inactive or subscription_required. Reads keep working. |
| 403 | killed | An admin has paused all agent access for the workspace. |
| 403 | ip_not_allowed | The request came from an IP address outside the key’s allowlist. |
| 403 | workspace_deletion_pending | The workspace is scheduled for deletion, so agent access is suspended. |
| 403 | insufficient_scope | The key lacks the scope in requiredScope. |
| 403 | brand_not_permitted | The key is restricted to other brands. |
| 403 | issuer_no_access | The person who created the key no longer has access to the workspace. |
| 403 | issuer_cannot_write | The person who created the key no longer has write access, so write scopes stop working. |
| 403 | module_inactive | The workspace module behind this endpoint (moduleId) is not active. |
| 403 | parent_not_active | The key used to mint a sub-token is not active. |
| 404 | draft_not_found | No draft with that id in this workspace. |
| 404 | webhook_not_found | No webhook with that id in this workspace. |
| 404 | publish_job_not_found | No publish job with that id in this workspace. |
| 409 | publish_job_not_cancellable | The post has already started publishing or finished, so it can no longer be cancelled. |
| 409 | idempotency_conflict | This Idempotency-Key was already used with a different request body. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still running. Check its outcome before retrying. |
| 410 | brandos_generation_retired | The endpoint generated content and has been retired. Your AI client writes the content; ReBrandOS stores, checks and publishes it. |
| 413 | payload_too_large | Request bodies are limited to 1 MB. |
| 422 | provider_capacity_not_ready | ReBrandOS could not confirm the destination account can take the post (for example it is disconnected or out of provider quota). blockers explains. Nothing was scheduled. |
| 422 | publish_not_ready | The draft could not be scheduled. detail says why. Nothing was scheduled. |
| 429 | rate_limited | The key sent more requests this minute than its limit. Wait for Retry-After seconds. |
| 429 | publish_cap_reached | The key has scheduled its daily number of posts. The cap resets at 00:00 UTC (resetAt). |
| 500 | internal_error | Something failed on our side. Safe to retry reads; retry writes with the same Idempotency-Key. |
| 503 | entitlement_unavailable | The plan check could not run, so nothing was done. Retry later. |