Development Guide
Development Guide
The Quick Guide gets one request working. This page covers what changes when the code has to keep working in production.
Two shapes of call
BeatAPI has exactly two request lifecycles, and almost every integration mistake comes from treating one as the other.
Text is synchronous: POST /v1/chat/completions, /v1/responses, /v1/messages, or the Gemini-compatible endpoint returns the result. Everything else accepts the work and hands back an ID.
The asynchronous lifecycle
- Submit —
POST /v1/images/tasks(orvideos,effects,video-analysis,music-video,ecommerce-video). A201means accepted, not finished. - Store the ID — persist
data.idanddata.request_idbefore starting any background work. A crash between the response and the write leaves a task you have paid for and cannot find. - Wait — poll
GET /v1/tasks/{task_id}, or register a webhook. - Read the result — on
succeeded, usedata.output.media. Onfailed, readdata.error_codeand the refund fields.
Polling
Three properties of that loop matter:
- It stops on terminal states, not on a guess about elapsed time. A finished task never changes again.
- It backs off with jitter. The task endpoint allows up to 120 requests per minute per key; a fixed one-second poll across a handful of concurrent tasks will exhaust that on its own.
- It treats
429as “wait”, not “fail”. HonourRetry-After.
Music Video tasks have non-terminal states that need a decision rather than more polling — storyboard_ready and requires_action. Treating them as “still working” polls forever. The Quick Guide lists every status and the action each one calls for.
Webhooks instead of polling
Register an endpoint with POST /v1/webhooks and BeatAPI posts the terminal state to it. This removes the polling loop entirely, at the cost of needing a reachable HTTPS endpoint.
Verify the signature against the raw request bytes before parsing JSON — re-serialising the body changes it and the signature will not match. See Webhooks.
Keep a slow reconciliation poll even with webhooks configured. A delivery can fail; a task that has been in processing for far longer than its model’s normal runtime is worth checking directly.
Errors
Every failure carries a stable error.code and a request_id. Log the request_id — it identifies the exact call in support.
The full list of error.code values is in the Quick Guide.
A 500 on a submit is ambiguous: the task may or may not have been created. Resubmitting blindly can pay for the same work twice. Send an Idempotency-Key on task creation and reuse it on the retry — BeatAPI returns the original task instead of creating a second one.
Rate limits
Two separate limits apply, and they fail differently.
- Request rate — requests per minute per key, returned as
429withRetry-After. The allowance rises with lifetime top-ups; the current value and the next tier are on the dashboard and inGET /api/user/self. - Concurrency — how many tasks may be processing at once, returned as
user_concurrency_exceeded. Waiting for a running task to finish is the only remedy; retrying immediately just consumes rate.
GET /v1/usage reports both: concurrency.limit and concurrency.active.
Retries that do not cost money
- Retry
429and5xx. Never retry400,401,402,403,404, or409. - Use exponential backoff with jitter, and a ceiling.
- Put an
Idempotency-Keyon every task creation so a retried submit cannot double-charge. - After a failed submit, poll before resubmitting if you have an ID.
- Cap total attempts. A permanently failing request retried forever is an outage that bills.
Cost control
- Set an output cap on text calls. Output is the expensive half;
max_tokensis the simplest lever there is. - Match the model to the step. Classification and routing do not need the top tier.
- Watch the tiered models. The GPT-5.6 and Grok families bill a higher rate past a context threshold; the DeepSeek family and Hunyuan
hy3bill a higher rate during published peak windows. A long-running agent or a scheduled batch can cross either line invisibly. Each response’s usage record shows which rate was applied. - One key per component.
by_api_keyinGET /v1/usagethen attributes spend without any work on your side. - Reconcile refunds. A failed task is refunded; if your ledger only records the reservation, your numbers will drift from the invoice.
Before going to production
- Use the exact
https://api.beatapi.ioorigin. - Keep permanent keys server-side. Browsers get a short-lived Realtime
client_secret, never an API key. - Store
data.idandrequest_idbefore starting background work. - Poll with jitter; stop at terminal or action states.
- Verify webhook signatures against raw bytes.
- Record reservations, settlements, refunds, errors, and final output URLs.
- Alert on
402— it stops every paid operation on the account at once.

