Base URL: https://chaviar.app/api/v1
Posts, stories and reels use the app's generation and publishing pipelines. Email and ad creation produces drafts. Ad budgets, email sending and comment replies stay in the app.
Authentication and permissions
Send Authorization: Bearer cvk_... on every API call. Create a key in Settings → API keys; only the workspace owner can create or revoke keys. A key belongs to one brand; request bodies cannot choose a different tenant. Pro permits one active key family; Studio permits five. Free and Starter do not include API access.
Creation grants select post, story, reel, email and ad independently. Existing keys retain their existing grants. The publishing grant is off by default. A key with publishing permission still needs the content:publish scope to use auto mode or approve, schedule or publish content. LinkedIn requires human review in the app, including when the key has publishing permission.
| Scope | Access |
|---|---|
| content:read | Content, requests, channels, calendar, wallet, usage, own key policy |
| content:write | Validate/create content, edit captions, cancel/restore drafts, regenerate |
| content:publish | Approve, schedule, publish and create in auto mode; also needs content:write and publishing grant |
| library:read | Brand media library |
| insights:read | Cached performance metrics |
| webhooks:manage | Webhook subscriptions, delivery history and replay |
| keys:rotate | Rotate the current key or reduce its family's policy |
Keys created with the existing Settings controls start with all scopes. Use PATCH /key to remove scopes, restrict allowed_platforms, lower daily_token_limit, or set allow_auto_publish to false. The API cannot add authority. These reductions apply to every credential in the rotation family. Reads with the appropriate scope cover the brand; kind and destination restrictions apply to creation and mutations. Use separate brands for separate tenants.
Workflow and creation
- Read
GET /channelsandGET /wallet. - Optionally call
POST /content/validatewith the creation body. - Call
POST /contentwith a stableidempotency_key. - Poll
GET /requests/{id}every few seconds, or subscribe to webhooks. - When generation finishes, retrieve each delivery's
content_idwithGET /content/{id}. - Edit or schedule eligible drafts through the API, or open their
review_urlin the app. Select the owning brand in the app first.
{
"kind": "post",
"brief": "Announce our new Thursday evening opening hours.",
"platforms": ["ig", "fb"],
"mode": "review",
"idempotency_key": "hours-announcement-v1",
"metadata": {"campaign": "autumn-hours"}
}curl https://chaviar.app/api/v1/content \
-H "Authorization: Bearer $CHAVIAR_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"kind":"post","brief":"Announce our new Thursday evening opening hours.","mode":"review","idempotency_key":"hours-announcement-v1"}'A new accepted request returns HTTP 202, an id, status composing, kind, mode and created_at. The Location header points to /api/v1/requests/{id}. Acceptance means queued, not generated or published.
| Creation field | Contract |
|---|---|
| kind | Required: post, story, reel, email or ad |
| brief | Required non-empty text, at most 2,000 characters |
| mode | review by default; auto requires publishing permission and scope; email/ad are review-only |
| platforms | Optional non-empty list; posts support ig, fb, th, in, x, bsky; stories support ig/fb; reels accept exactly one of ig/fb |
| publish_at | Auto only: now or a future ISO timestamp with a UTC offset, within 30 days; omitted uses the brand's next suitable slot |
| expire_after | Positive duration such as 30m, 6h or 2d, at most 30d; must end after publish_at |
| idempotency_key | Optional but strongly recommended; non-empty, at most 128 characters |
| images | Objects containing exactly one url or data_url; post/email/ad accept 1, story/reel accept 4 |
| email_format | Email only: standard, leaflet_tiles, leaflet_image or auto |
| metadata | Up to 20 string fields, keys up to 64 characters, values up to 500; encoded object up to 4,000 characters |
Unknown fields are rejected. Channel selection is exact: an unavailable selected channel never broadens the request to other channels. Omitted platforms snapshot the connected, permitted channels at acceptance; key destination restrictions narrow this set. Reels choose the first eligible destination in the app's channel order. Channels connected later do not silently join an accepted request. A review request with no available destination can still generate downloadable content; an auto request with no available destination returns 409. Actual outputs and omitted channels appear in deliveries and skipped_platforms after generation.
Validation uses the same body, grants and destination rules. It returns cost, wallet affordability and currently available destinations. It does not fetch images, upload, reserve tokens, generate or publish. A valid result cannot guarantee later channel availability or model success.
Images and billing
The entire JSON body must fit within 20 MiB, including base64 expansion. Use public HTTP(S) image URLs for larger inputs. Each decoded/fetched input is capped at the configured upload limit (15 MiB by default) and normalized by the shared raster decoder. Supported raster formats include PNG, JPEG, WebP, HEIC/HEIF, AVIF, GIF, TIFF and BMP; SVG is not accepted. Image fetches validate every redirect and DNS address before connecting. Private-network URLs and credentialed URLs are refused.
| Kind | Creative tokens |
|---|---|
| post | 1 |
| story | 2 |
| reel | 5 |
| 1 | |
| ad | 1 |
A request pays for one creative, including its platform variants. Text-only caption edits are free. Regeneration creates a new review-mode request at the format's normal cost, preserving the original; it is not an in-place image or frame reroll. The regeneration endpoint requires a new brief and idempotency key. Frame-level editing and email layout editing remain in the app.
Generation failure or confirmed enqueue failure releases the reservation. Expiring/cancelling already-generated content and publishing failure do not refund successful generation. A network error or HTTP 500 is not proof that nothing was charged: retry with the same idempotency key and inspect the stored request. Interrupted jobs are reconciled without rerunning paid generation; already-produced content is linked, and confirmed stranded work is failed/refunded. Test brands may generate placeholders without tokens or live provider calls.
Requests and current status
| Status | Meaning |
|---|---|
| composing | Generation is queued or running |
| awaiting_review | At least one finished draft needs review; email/ad drafts also use this state |
| scheduled | At least one delivery is scheduled or publishing |
| partially_published | At least one channel published and another failed, is pending, or had no output |
| published | All requested destinations published; none were skipped |
| blocked | A delivery needs channel reconnection or resumption |
| failed | Generation or delivery failed; inspect safe error codes |
| expired | The deadline passed; no pending delivery may start |
| cancelled | Request or remaining draft was cancelled |
GET /requests/{id} returns current truth across all platform cards. deliveries includes each content id, platform, generation/review/delivery status, timestamps, external id, safe error and action required. In-flight deliveries use the per-delivery status publishing. Partial publication may coexist with pending work or failures. Undoing approval can change scheduled back to awaiting_review.
complete means every delivery reached a terminal publication outcome (published, failed, expired or cancelled); a review-mode draft is ready to retrieve while complete is false. Omitted/unavailable destinations count as finished omissions; a partly published request can therefore be complete with skipped_platforms. To wait for generation, check the delivery generation_status fields rather than waiting for complete. Look at each delivery when a request is partially published. skipped_platforms reports requested channels without an output card; it is populated once content exists.
Deadlines and cancellation are checked when arming and again before provider send. A provider call already in flight may finish after cancellation/deadline; completed publications remain reported as published. Cancellation cannot retract a live post. The minute housekeeping process withdraws eligible siblings, even when another sibling already published.
Idempotency is scoped to the key family and survives rotation. Repeating a stored key returns HTTP 200 with the original request, even after publish_at has passed; a different body does not replace it. Replay uses read quota. Keep the same key for transport retries; use a different key for intentionally new work. Failed stored requests also replay their existing outcome.
Endpoint reference
All paths below are relative to /api/v1. List endpoints use limit (1–100, default 25) and an opaque cursor, returning data and next_cursor. Pass the returned cursor unchanged. Lists are newest first with stable id ordering within equal timestamps. Requests list is history across the brand; idempotency_key lookup is restricted to the calling key family.
| Method and path | Purpose |
|---|---|
| POST /content/validate | Preflight a creation body |
| POST /content | Queue generation |
| GET /content | List content; optional kind and platform filters |
| GET /content/{id} | Caption, headline, ordered story/email blocks, media, format details, lifecycle and review link |
| PATCH /content/{id} | Set caption on a ready, unpublished post/story/ad; reject over-limit text |
| POST /content/{id}/actions | approve, schedule, publish, cancel or restore a social draft |
| POST /content/{id}/regenerate | New review-mode generation; body: brief and idempotency_key |
| GET /requests | Request history; optional idempotency_key lookup |
| GET /requests/{id} | Aggregate and per-delivery state |
| POST /requests/{id}/cancel | Stop remaining work; cannot recall an in-flight send |
| GET /channels | Current availability, permissions, format and caption capabilities |
| GET /library | Brand media ids and download URLs |
| GET /calendar | App calendar; required from/to dates in YYYY-MM-DD, inclusive brand-timezone range up to 90 days |
| GET /metrics | Cached channel rollups; optional comma-separated content_ids (up to 100) for content metrics |
| GET /wallet | Token balance and plan |
| GET /usage | Token ledger: positive tokens consume, negative tokens grant/refund, zero records a free edit |
| GET /key | Calling key's current policy |
| PATCH /key | Reduce scopes, destinations, publishing grant or daily token ceiling |
| POST /key/rotate | Mint replacement; optional grace_seconds (0–86400, default 3600) |
| GET /webhooks | List subscriptions |
| POST /webhooks | Create subscription with url; reveal signing_secret once |
| PATCH /webhooks/{id} | Enable/disable subscription |
| DELETE /webhooks/{id} | Delete subscription and its delivery history |
| GET /webhooks/{id}/deliveries | Paginated attempts and safe failure details |
| POST /webhooks/{id}/deliveries/{deliveryId}/replay | Retry an undelivered, idle event |
For actions, send {"action":"schedule","publish_at":"2026-12-01T09:00:00Z"} using a future date within the permitted horizon. Approve chooses the normal brand slot; publish starts now. Cancellation disarms the individual card; restore returns it to review. A cancelled or expired request cannot be reactivated through a card action: create a fresh request. Uncertain provider sends return 409 and require verification in the app before any retry.
Content media URLs are signed for five minutes; fetch the content again for fresh URLs. URL can be null when media is unavailable. Library returns user-facing media only. Metrics are cached, may be missing, and carry their collection timestamps; reading never forces a provider refresh. Calendar follows the app's field names and includes items, coverage and meta. Channel connected means currently eligible for publishing; a paused or disconnected channel is false.
Key rotation and limits
Rotation returns plaintext once. Store the replacement before the old key's grace period ends. A second attempt to rotate the expiring predecessor returns 409; the plaintext cannot be recovered after a lost response. Revoke that family and create a replacement from Settings if necessary. Rotation shares capacity, quotas, idempotency and token accounting with its predecessor. Revoking any family member in Settings revokes the family. Queued work is governed by its accepted request; cancelling a request is how to stop it.
| Limit | Pro | Studio |
|---|---|---|
| Active key families per brand | 1 | 5 |
| Writes per rolling 24 hours per family | 100 | 500 |
| Write burst per rolling minute per family | 10 | 10 |
| Reads per rolling minute per family | 60 | 60 |
Validation uses read quota. Mutations use write quota. A daily_token_limit additionally caps net API creative token usage over a rolling 24-hour window for the family; rotation cannot reset it. Rate-limit responses include Retry-After. Limiters and accepted request state are shared across web replicas.
Webhook delivery
Create up to five subscriptions per brand with a public HTTPS URL. Subscriptions receive request.updated snapshots while enabled. A minute worker observes state changes; rapid intermediate transitions can coalesce into the latest snapshot. Each event contains id, type, created_at, request_id, monotonically increasing request version and data (the request representation). Existing requests may produce an initial snapshot after subscribing.
Delivery is at least once. Persist the event id and ignore duplicates. Request versions let consumers discard stale/out-of-order snapshots. Responses with a 2xx status acknowledge the event; redirects are refused. Network errors, oversized responses and other statuses retry, up to eight attempts with exponential delays starting at 30 seconds; the minute poll determines actual delivery time. Disabled subscriptions pause pending delivery. Successful deliveries cannot be manually replayed; undelivered idle ones can. Keep handlers quick: requests time out after ten seconds and responses are capped at 64 KiB.
The Chaviar-Event-Id header contains the stable event id. Chaviar-Signature is t=<unix-seconds>,v1=<hex-hmac>. Verify HMAC-SHA256 using the secret and exactly timestamp + "." + raw_request_body. Compare signatures in constant time, reject stale timestamps (for example outside five minutes), then parse JSON and process/deduplicate the event. Every attempt gets a fresh timestamp/signature but keeps the event id. The example client includes a verification helper. Store secrets securely; list endpoints never reveal them.
Errors and retry handling
Errors use {"error":{"code":"...","message":"..."}}. Responses include X-Request-Id and Cache-Control: no-store. Branch on error.code, not the text. An error can include a request_id referring to accepted work; the X-Request-Id is the HTTP correlation id.
| HTTP | Codes |
|---|---|
| 400 | invalid_request |
| 401 | unauthorized |
| 402 | insufficient_tokens |
| 403 | key_revoked, key_expired, plan_required, kind_not_allowed, auto_not_allowed, scope_required |
| 404 | not_found |
| 405 | method_not_allowed |
| 409 | conflict, destination_unavailable, not_supported |
| 413 | payload_too_large |
| 415 | unsupported_media_type |
| 429 | rate_limited, spend_limit |
| 500 | internal |
Generation/request outcomes use sanitized codes such as generation_failed, content_blocked, publish_failed, schedule_failed, interrupted, enqueue_failed, invalid_image, insufficient_tokens, budget_exceeded and nothing_to_generate. Raw provider errors, prompts, credentials, storage keys and infrastructure costs are not part of this API. Treat future additional fields, statuses and error codes defensively.