Authentication
VoxBurst accepts two forms of Bearer authentication: API keys and Cognito session JWTs. Both are sent the same way, in the Authorization header. Some endpoint groups require one or the other specifically — see the notes throughout this page and Scopes below.
API Keys
Authorization: Bearer vb_live_xxxxxxxxxxxxxAPI keys are prefixed with vb_live_. Keys created in the VoxBurst dashboard always use this prefix.
Create and manage your API keys in the VoxBurst dashboard .
Keep your API keys secret. Never commit them to version control or expose them in client-side code.
Cognito Session (JWT) Authentication
In addition to API keys, VoxBurst accepts a Cognito-issued JSON Web Token in the same
Authorization header:
Authorization: Bearer <cognito-access-or-id-token>There is no separate endpoint or request format for JWT auth — the API distinguishes a Cognito token from an API key by its structure, and both are accepted on any route that supports either.
- Access and ID tokens expire 1 hour after issuance.
- Refresh tokens last 30 days and are used to obtain a new access/ID token pair without requiring the user to sign in again.
- Tokens come from the standard Cognito login flow used by the VoxBurst dashboard
(
app.voxburst.io). This page does not document that login/token-issuance flow in detail — there is currently no dedicated public doc page for it. If you are building an integration that needs to mint or refresh Cognito tokens directly (rather than using an API key), talk to VoxBurst support before building against this.
A Cognito session is required — an API key with any scope, including a wildcard key, is
not sufficient — for the operations listed in
There is no invitations:write: sending invitations,
resending invitations, and accepting invitations.
A Cognito session is also accepted as an alternative to a wildcard-scoped API key for any endpoint group that no scoped key can reach.
Scopes
API keys carry a list of scopes that control which requests the key can make. Use the minimum scopes required for your use case.
How scopes are named and matched
Scopes follow the form <resource>:<read|write>. Resource names are lowercase and
hyphenated (api-keys, audit-logs).
Granularity is method-based, not endpoint-based. Every endpoint belongs to one route group, and the group plus the HTTP method determine the single scope required:
GETandHEADrequire the group’s:readscope.- Every other method (
POST,PUT,PATCH,DELETE) requires the group’s:writescope.
There are two exceptions to the GET rule — GET /v1/ai/suggest-time/{accountId} and
GET /v1/billing/portal both require their group’s :write scope, because both change
state. See Two GET endpoints require a :write scope.
A key needs exactly one matching scope per request. Holding :write does not imply
:read — a key that only reads and writes posts needs both posts:read and posts:write.
Some groups are read-only by design and define no :write scope. For those groups any
non-GET request is rejected with 403, regardless of which scopes the key holds. The
table below marks them.
Scope reference
| Scope | Route group | Grants |
|---|---|---|
posts:read | /v1/posts, /v1/batch | Read posts, post status, and batch job state |
posts:write | /v1/posts, /v1/batch | Create, update, delete, publish, and import posts; submit batch operations |
accounts:read | /v1/accounts | Read connected social accounts |
accounts:write | /v1/accounts | Connect, update, and disconnect social accounts |
media:read | /v1/media, /v1/uploads | Read media assets and upload records |
media:write | /v1/media, /v1/uploads | Create uploads and delete media assets |
webhooks:read | /v1/webhooks | Read webhook endpoints and delivery history |
webhooks:write | /v1/webhooks | Create, update, delete, and test webhook endpoints |
analytics:read | /v1/analytics, /v1/reports | Read analytics and reports (read-only group) |
workspaces:read | /v1/workspaces | Read workspace details, members, and settings |
workspaces:write | /v1/workspaces | Update workspace details, members, and settings |
api-keys:read | /v1/workspaces/{id}/api-keys | List API keys and their usage |
api-keys:write | /v1/workspaces/{id}/api-keys | Create, update, rotate, and delete API keys |
schedule:read | /v1/calendar | Read the posting calendar and schedule configuration |
schedule:write | /v1/calendar | Update the posting calendar and schedule configuration |
queues:read | /v1/approval-queue | Read approval queue state (read-only group) |
personas:read | /v1/personas | Read personas and their modules |
personas:write | /v1/personas | Create, update, and delete personas |
audiences:read | /v1/audience (/v1/audiences also matches) | Read audience data |
audiences:write | /v1/audience (/v1/audiences also matches) | Create, update, and delete audience data |
users:read | /v1/users | Read user profile information (read-only group) |
inbox:read | /v1/inbox | Read comments, mentions, and inbox items |
inbox:write | /v1/inbox | Reply to, hide, delete, and sync inbox items |
contacts:read | /v1/contacts | Read contacts and their tags |
contacts:write | /v1/contacts | Create, update, delete, and tag contacts |
sequences:read | /v1/sequences | Read sequences, steps, and enrollments |
sequences:write | /v1/sequences | Create, update, delete sequences and manage enrollments |
broadcasts:read | /v1/broadcasts | Read broadcasts and delivery results |
broadcasts:write | /v1/broadcasts | Create, update, delete, and send broadcasts |
audit-logs:read | /v1/audit-logs | Read workspace audit logs (read-only group) |
platforms:read | /v1/platforms | Read platform capabilities and metadata (read-only group) |
hashtag-sets:read | /v1/hashtag-sets | Read saved hashtag sets |
hashtag-sets:write | /v1/hashtag-sets | Create, update, and delete hashtag sets |
ai:read | /v1/ai | Read AI status, credit usage, usage history, and image job state. Does not cover GET /v1/ai/suggest-time/{accountId} — see below |
ai:write | /v1/ai | Run AI generation (enhance, hashtags, images, post content), submit and retry image jobs, and call GET /v1/ai/suggest-time/{accountId}. All of these spend workspace AI credits |
billing:read | /v1/billing | Read current plan, subscription, usage, scheduled post count, and credit balance. Does not cover GET /v1/billing/portal — see below |
billing:write | /v1/billing | Create checkout sessions, preview and change plan, buy credits and add-ons, cancel and resume a subscription, and create a Stripe billing portal session (GET /v1/billing/portal) |
invitations:read | /v1/workspaces/{id}/invitations | List a workspace’s pending invitations (read-only group — there is no invitations:write, see below) |
comment-automations:read | /v1/comment-automations | Read comment automations, their run logs, and their resolved posts |
comment-automations:write | /v1/comment-automations | Create, update, and delete comment automations |
* | All mapped route groups | Wildcard. Must be the sole scope in the array; cannot be combined with named scopes. |
Any scope string not in this table is rejected — see scope validation below.
Batch operations use the posts scopes
/v1/batch has no scope of its own. It is mapped onto posts:read and posts:write:
GET /v1/batch/limitsrequiresposts:readPOST /v1/batchrequiresposts:write
This is deliberate. A batch request dispatches its sub-operations internally without a
second scope check, so a separate batch:write scope would grant whatever a batch can
execute. Reusing the posts scopes keeps a batch key’s reach identical to what the same
key could already do one request at a time. A key holding posts:write can therefore
call POST /v1/batch; a key holding only posts:read cannot.
Uploads share the media scopes
/v1/uploads has no scope of its own either. It is mapped onto media:read and
media:write, the same pair that covers /v1/media. A key that can create uploads needs
media:write; there is no uploads:* scope to request.
Two GET endpoints require a :write scope
The method rule (GET → :read) has two deliberate exceptions. Both are GET requests
that are not reads in any meaningful sense, so both are carved out to the :write scope
of their group. This is intentional, not a bug — do not expect a :read-only key to work
on either.
| Endpoint | Required scope | Why |
|---|---|---|
GET /v1/ai/suggest-time/{accountId} | ai:write | Reserves and spends workspace AI credits before calling the provider, exactly like the POST routes. A nominally read-only key must not be able to incur cost. |
GET /v1/billing/portal | billing:write | Creates a Stripe billing portal session and returns its URL. Anyone holding that URL can change the payment method, switch plan, or cancel the subscription. That is a billing mutation wearing a GET. |
Every other endpoint in these two groups follows the normal rule: GET /v1/ai/status,
GET /v1/ai/usage, GET /v1/ai/image-jobs, GET /v1/billing/current,
GET /v1/billing/usage and GET /v1/billing/credits all require only the :read scope.
If a new credit-spending or state-mutating GET is added under /v1/ai or /v1/billing
in future, it will be carved out the same way and announced in the
changelog.
There is no invitations:write
invitations:read is the only invitation scope, and it grants exactly one endpoint:
GET /v1/workspaces/{id}/invitations, which lists a workspace’s pending invitations.
That is ordinary tenant-scoped data.
Invitation mutations are bound to a person, not to a workspace, and are not available to API keys at all:
| Endpoint | Result with any API key |
|---|---|
POST /v1/invitations | 403 AUTHORIZATION_ERROR |
POST /v1/workspaces/{id}/invitations | 403 AUTHORIZATION_ERROR |
POST /v1/invitations/{id}/resend | 403 AUTHORIZATION_ERROR |
GET /v1/invitations | 403 AUTHORIZATION_ERROR |
POST /v1/invitations/{token}/accept | 403 AUTHORIZATION_ERROR |
DELETE /v1/invitations/{id} | 403 AUTHORIZATION_ERROR |
The 403 names the reason in its message. There is no scope, wildcard included, that
changes this — sending an invitation records and displays who sent it, and it grants a
person workspace access that would outlive the key. Use a signed-in workspace admin
(Cognito session) for these operations.
GET /v1/invitations/{token}, the lookup used by the accept-invite page, is public and
requires no authentication or scope.
comment-automations:* does not replace the plan entitlement
The comment-automations scopes are an API-access gate only. They are checked
independently of, and in addition to, the plan entitlement for the feature.
Comment automations are available on the PRO and AGENCY plans. A key holding
comment-automations:write on a workspace whose plan does not include the feature still
receives 403 with the code PLAN_UPGRADE_REQUIRED. Both gates must pass:
- The key must hold the matching
comment-automations:read/comment-automations:writescope. - The workspace plan must include the comment automations feature.
Granting the scope does not upgrade the plan, and upgrading the plan does not add the scope to an existing key.
Endpoint groups that no scoped key can reach
The scope table above is the complete map. Any authenticated endpoint group that does not
appear in it has no scope mapping, so every key with named scopes receives 403 on it
with the message This route is not accessible with this API key. Only a key issued with
the wildcard * scope can call those groups.
If you receive that 403 with a correctly scoped key, that is the intended behaviour, not
a bug. Use a wildcard key, or a Cognito session, for those calls.
/v1/ai, /v1/billing and workspace invitation listing were previously in this
category and are now covered by named scopes — see ai:*, billing:* and
invitations:read in the table above. Invitation mutations remain unavailable to API
keys entirely, including wildcard keys.
Administrative endpoints (/v1/admin/...) are never reachable with an API key —
including a wildcard key. They always return 403.
Scope validation is enforced on API key creation and updates. Submitting an unrecognized scope string returns 400 VALIDATION_ERROR with the message "Invalid scope(s): <scope>". The wildcard scope (*) must be the only scope specified — combining it with named scopes (e.g. ["*", "posts:read"]) is rejected.
Scope errors
Both cases return HTTP 403 with the code AUTHORIZATION_ERROR:
| Situation | Message |
|---|---|
| The route has a scope and the key lacks it | API key missing required scope: <scope> |
| The route has no scope mapping (and the key is not wildcard) | This route is not accessible with this API key |
Rate Limiting
This section is a summary. See Rate Limits for the full reference, including recommended client backoff behavior and endpoint-specific limits.
Rate limits are applied per API key and vary by plan. Higher plans receive higher
limits. Do not hard-code a specific limit — read the current value from the
X-RateLimit-Limit response header on every request, since the effective limit
can change without notice.
Rate limit information is returned in response headers:
X-RateLimit-Limit: <requests allowed in the current window>
X-RateLimit-Remaining: <requests remaining in the current window>
X-RateLimit-Reset: <unix timestamp when the window resets>When rate limited, the API returns 429 Too Many Requests with a Retry-After header indicating when to retry.
Rate Limit Headers
Every authenticated response includes headers to help you manage request pacing.
Rate limiting is applied after authentication succeeds, so 401 responses (missing or
invalid credentials) and unauthenticated public endpoints (like GET /v1/health) do not
include these headers:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in the current window |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the limit resets |
Example Response with Rate Limit Headers
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: <requests allowed in the current window>
X-RateLimit-Remaining: <requests remaining in the current window>
X-RateLimit-Reset: <unix timestamp when the window resets>
{
"id": "post_abc123",
"content": "Hello from VoxBurst!",
"status": "scheduled"
}Use different API keys for development and production environments to limit the blast radius of a leaked key.
Environment Variables
Store your API key as an environment variable:
# .env
VOXBURST_API_KEY=vb_live_xxxxxxxxxxxxxcurl https://api.voxburst.io/v1/accounts \
-H "Authorization: Bearer $VOXBURST_API_KEY"Or with the TypeScript SDK:
import { VoxBurstClient } from '@voxburst/sdk'
const client = new VoxBurstClient({ apiKey: process.env.VOXBURST_API_KEY! })Health Check
The API exposes two health endpoints under the /v1 prefix:
| Endpoint | Description |
|---|---|
GET /v1/health | Returns 200 when the API is up |
GET /v1/ready | Returns 200 when the API is up and dependencies are healthy |
curl https://api.voxburst.io/v1/healthThe health endpoints are at /v1/health — not /health. Requests to https://api.voxburst.io/health (without /v1) return 403 from the CloudFront distribution layer and do not reach the API. Always use the /v1/ prefix.
Both endpoints are unauthenticated and do not require an API key.
Resource ID Format
All VoxBurst resource IDs (post IDs, account IDs, etc.) follow the cuid2 format:
- Exactly 25 characters total
- Starts with the letter
c - Followed by 24 lowercase alphanumeric characters (
a–z,0–9) - Example:
cmp6v6bhf000369v60htcu3vc
Passing a malformed ID returns 400 Bad Request — not 404. The API validates the ID format before attempting a database lookup. Handle both 400 and 404 when an ID might be user-supplied:
{
"error": {
"code": "BAD_REQUEST",
"message": "Invalid path parameter 'id': Invalid ID format"
}
}Idempotency
VoxBurst supports idempotency for all write operations (POST, PUT, PATCH, DELETE). This lets you safely retry requests on network failures without creating duplicate resources.
How to use
Add an Idempotency-Key header to any write request:
POST /v1/posts
Authorization: Bearer vb_live_xxxxxxxxxxxxx
Idempotency-Key: create-post-2026-06-01-campaign-a
Content-Type: application/jsonKey format
| Rule | Value |
|---|---|
| Header name | Idempotency-Key (case-insensitive) |
| Allowed characters | Alphanumeric, hyphens (-), underscores (_) |
| Min length | 1 character |
| Max length | 255 characters |
| TTL | 24 hours — keys are purged after expiry |
Behavior
| Scenario | Behavior |
|---|---|
| First request | Executes normally, caches response against the key |
| Repeat request (same key, same body) | Returns the cached response with Idempotency-Key-Used: true header — no duplicate action |
| Repeat request (same key, different body) | Returns 409 Conflict — key is already locked to the original payload |
| Concurrent requests with same key | Returns 409 Conflict with code IN_PROGRESS — only one request proceeds |
| Key expired (>24h) | Treated as a new request — processed normally |
Response headers
| Header | Description |
|---|---|
Idempotency-Key-Used | true when the response is a replay of a cached result |
X-VoxBurst-Supports-Idempotency | true on every response — confirms the endpoint supports idempotency |
Recommended key strategies
# Use a UUID per attempt
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
# Use a stable identifier for your operation
Idempotency-Key: schedule-post-campaign-123-slot-4
# Use a hash of the request intent for agent workflows
Idempotency-Key: post-${workspaceId}-${contentHash}-${scheduledFor}Error codes
| Code | HTTP | Cause |
|---|---|---|
IDEMPOTENCY_CONFLICT | 409 | Key already used with a different request body |
IDEMPOTENCY_IN_PROGRESS | 409 | A request with this key is currently being processed |
IDEMPOTENCY_INVALID_KEY | 400 | Key format invalid (invalid characters or out of length range) |
Idempotency keys are scoped to your workspace. A key from one workspace cannot conflict with the same key in another workspace.
Contract precedence
When prose descriptions, inline examples, SDK documentation, and the OpenAPI spec ever appear to conflict, this ordering determines which source is authoritative:
- API source code — the implementation is the ground truth. All other sources describe it.
- This documentation — pages under
/api-reference/*reflect the current implementation, validated against the codebase before publishing. - OpenAPI spec (
GET /v1/docs/openapi.yaml) — machine-readable contract. Kept in sync with implementation; the source code takes precedence on conflict. - SDK docs (TypeScript, Go) — SDK method signatures and types are derived from the REST contract. Where SDK behavior diverges from REST, the REST contract wins.
- Examples — illustrative, not normative. Examples may omit optional fields or use placeholder values.
In practice: if you encounter a mismatch between a docs example and a field table, trust the field table. If a field table conflicts with an actual API response, file a bug — the implementation may have changed without a docs update.
Backwards compatibility: non-breaking changes (new optional fields, new endpoints, new enum values) are deployed without notice. Your client should ignore unknown fields. Breaking changes (field removal, type changes, endpoint removal) are announced at least 30 days in advance in the changelog.
Error Responses
Authentication failures return:
{
"error": {
"code": "AUTHENTICATION_ERROR",
"message": "Invalid or missing API key"
}
}| Status | Code | Cause |
|---|---|---|
401 | AUTHENTICATION_ERROR | Missing or invalid API key |
403 | AUTHORIZATION_ERROR | Valid key, but it lacks the scope for this route — or the route is not reachable with an API key at all |
429 | RATE_LIMITED | Rate limit exceeded |