Operations
Going to production
Key handling, webhooks over polling, idempotency, terminal states, retry policy by error code, balance monitoring, rate limits and media storage.
A prototype that submits a job and polls in a while loop works. What breaks
in production is everything around it — a revoked key, a webhook that arrives
twice, a balance that runs out at 3 a.m., a media URL that expired before your
CDN fetched it. This page collects the operational decisions worth making
before you ship.
Read it in order the first time, then treat it as a checklist. Nothing here is exotic — it is the set of things that, in practice, every team building on a generation API learns the hard way in roughly this sequence: keys leak, webhooks duplicate, a retry double-charges, the balance hits zero on a Friday, and a media URL expires in the twelve hours between generating an asset and someone actually opening the page it was for.
The creative side lives in the other guides — text to video, image generation, image editing and music generation. This one is about keeping what you built there running.
API keys
- Send the key in a header, never in a query string. Both
Authorization: Bearer sk-…andx-api-key: sk-…are accepted. Query strings end up in proxy logs and browser history. - Never ship a key to a browser or a mobile binary. The public API has no CORS-safe, scoped, or short-lived key type — a key is full account access and full spend authority. Proxy generation calls through your own backend.
- Issue one key per environment and per service. Staging, production and each background worker should be separable, because revocation is per key. The only way to answer "what was this key doing" is to have kept them apart.
- Rotate by overlap, not by cutover. Create the new key, deploy it, confirm traffic has moved, then deactivate the old one. Deactivation takes effect on the next request — successful key lookups are cached for an hour, but every mutation that revokes a key drops that cache entry.
- Store keys in a secret manager, not in
.envfiles committed to the repository. Treat a leaked key like a leaked credit card: deactivate first, investigate second.
Prefer webhooks over polling
Pass a webhook URL on create and the finished task is POSTed to you. The
delivered body is byte-identical to the task-status envelope — the same
{ code, data: { taskId, status, files, output, createdTime, errorMessage } }
you would have polled for. One code path handles both.
{"model": "veo3.1-fast","input": { "prompt": "…", "aspectRatio": "16:9" },"webhook": "https://example.com/hooks/api-stock/8f2c1e9a-b40d-4c77-9a1e-secret"}
Operational notes:
- There is no signature header. Authentication is whatever you put in the URL. Use a long random path segment or query parameter, compare it in constant time, and treat the endpoint as public otherwise.
- Never trust the payload as authorisation. Look up the
taskIdin your own database before acting on it; a body claiming a task you never submitted is not your task. - Return 2xx fast. Delivery is retried 13 times with exponential backoff starting at 10 seconds — about 22 hours of attempts. Anything outside the 200–299 range counts as a failure. Enqueue the work, then respond.
- Expect duplicates. A response that times out after your handler committed still counts as a failure and will be redelivered.
Details in Webhooks.
Polling, when you must
Some environments cannot expose an inbound endpoint. Polling
GET /api/v1/task/status/{taskId} is fine — just do it on a budget.
- Match the interval to the medium. Images finish in seconds; video and music in minutes. Roughly 3 s for images, 8–10 s for audio and video. Sub-second polling buys nothing but rate-limit pressure.
- Back off on repeated
processing. Start at the interval above and grow it — for example 5 s, 10 s, 20 s, capped at 30 s. A render that has taken two minutes is unlikely to land in the next 500 ms. - Set a wall-clock deadline, and treat it as your timeout, not the task's.
The generation keeps running server-side after you stop polling. Persist the
taskIdand resume later rather than resubmitting. - Poll from a worker, not from a request handler. A user-facing request that blocks for four minutes is a bug regardless of what the API does.
- Back off hard on 429. See Rate limits.
Idempotency
The create endpoint has no idempotency key. taskId is your idempotency
anchor — everything downstream keys off it.
- Persist the
taskIdin the same transaction that records the intent to generate. If your process dies between the HTTP call and the write, you have paid for a result you can never collect. - Make webhook and poll handlers idempotent on
taskId. Both paths can deliver the same terminal state, and the webhook can deliver it more than once. An upsert keyed ontaskIdis the whole solution. - Never resubmit to "check" on a job. A second create is a second
generation and a second debit. Query the
taskIdyou already have. - Guard user-triggered retries. Debounce the button, or key the request on something stable in your own domain so a double click cannot become two charges.
Handle every terminal state
Four statuses, and only two are terminal:
| Status | Terminal | Meaning |
|---|---|---|
not_started | no | queued, not yet dispatched |
processing | no | in flight at a provider |
finished | yes | result available |
failed | yes | exhausted, refunded |
- Branch on
statusfirst.finishedis terminal and ready even whenfilesis absent; never keep polling a finished task. - Inspect the result after
finished. Media actions returnfiles, while data-producing actions — Sunolyrics,timestamped-lyrics,bpm, and Midjourneydescribe— returnoutputwithout files. - Read all of
files. A Seedream batch or a Sunomusicaction returns several entries; takingfiles[0]throws away results you paid for. - Read
errorMessageonfailedand store it. By the time a task isfailed, the platform has already retried the same provider up to its attempt limit, walked its reserve providers, and refunded the debit. Nothing further will happen to that task. - Give yourself a stuck-task path. A task that has been
processingfar past the model's normal runtime should page a human, not loop forever.
Retry, or do not retry, by error code
Errors always arrive in one shape:
{"code": 402,"error": {"message": "Insufficient balance. Please top up your account","type": "PaymentRequired","code": "insufficient_balance"}}
Use both the HTTP status and error.code. The code is stable when present, but
rate-limit responses currently use unknown_error; detect them from status
429. error.message is English prose for logs and may be reworded.
error.code | HTTP | Retry? |
|---|---|---|
invalid_input | 400 | No — fix the body first |
api_key_missing | 401 | No — fix the client |
api_key_invalid | 401 | No — key revoked, inactive or banned |
api_key_not_found | 404 | No |
auth_required | 401 | No |
insufficient_balance | 402 | Only after topping up |
task_not_found | 404 | No — wrong id or wrong account |
generation_not_found | 404 | No |
not_found | 404 | No — check the path |
unknown_error with HTTP 429 | 429 | Yes — after Retry-After |
unknown_error | 500 | Yes, with backoff and a cap |
Rules that follow from the table:
- 4xx is a client bug except for
429and402 insufficient_balance. Retrying an unchanged 400 body produces an identical 400 forever. A429is transient: wait forRetry-After, add jitter, and retry with a cap. Retryinsufficient_balanceonly after replenishing the balance. - 5xx and transport errors are worth retrying with exponential backoff and jitter — three or four attempts, then park the job.
- A create call that times out on your side may still have succeeded. Do not blind-retry it. Reconcile against your own record of the intent before spending again.
failedtasks are not errors to retry automatically. The platform already did. A content-policy rejection in particular is terminal and will fail identically on every provider.
Full list in Errors & error codes.
Watch the balance
Billing is prepaid. The balance is debited at create time, not on
completion, and refunded automatically when a task ends in failed.
-
Check the balance before a batch, not after the 402.
shcurl https://api.api-stock.com/api/v1/user/me \-H "Authorization: Bearer sk-your-key"json{"code": 200,"data": {"id": "019c1f70-2b44-70d3-9c22-6b7ad0f41e18","balance": 48250,"createdAt": "2026-01-14T09:03:11.000Z"}}balanceis in integer cents —48250is $482.50. Do not divide it in a float and compare for equality. -
Alert on a threshold, not on empty. Pick a floor that covers a day of normal spend and top up when you cross it. A 402 in production is a queue of failed jobs, not a warning.
-
Track spend over time with
GET /api/v1/stats— aggregated usage and spend per period, model, type and day. Use it to spot the model whose price changed, or the retry loop that quietly tripled your bill. -
Reconcile against your own records. Your task table plus the price you expected should match what the stats endpoint reports. A divergence usually means duplicate submissions.
See Pricing & balance.
Rate limits
The API allows 120 requests per 60 seconds per client IP and endpoint. A create route and a status route use separate buckets, while all callers behind one egress IP share the bucket for any one route.
- Budget the polls, not just the creates. A hundred in-flight jobs polled every 3 seconds is 2000 requests a minute from one IP. This is the usual way to hit the ceiling.
- Use a separate limiter bucket per endpoint in your own code, keyed by the egress IP when your workers can use more than one.
- Prefer webhooks when the fleet grows. They remove the polling traffic entirely and the limit stops being the binding constraint.
- Remember the limit is also per IP. Every worker behind one NAT gateway shares each endpoint's bucket; workers on separate egress IPs do not.
See Rate limits.
Copy the media into your own storage
Every fileUrl in files points at platform storage and is valid for 24
hours.
- Download on completion, in the same handler that records the terminal state. Do not defer it to a nightly job.
- Never hot-link a
fileUrlfrom your product. It will 404 for your users a day later, and you will not find out until they tell you. - Store your own copy plus the original URL and the
taskId, so a failed download is diagnosable and repeatable while the URL is still alive. - Verify the download. A truncated video that nobody checked is worse than a missing one, because it looks like a success.
- URLs inside
outputare not mirrored. Suno cover art, for example, points at the provider's CDN and has its own lifetime. Copy anything you intend to keep.
See Files & storage.
Logging and support
- Log the
taskIdfor every generation, always. It is the primary key of every support conversation, and without it a question about a bad render is unanswerable. - Log the request
modelandinputalongside it. Reproducing a failure needs the exact body, andinputshapes differ per model. - Log
error.codeanderror.messageverbatim on failures, anderrorMessagefromfailedtasks. The message is stable English, safe to log and safe to grep. - Do not surface raw provider errors to end users.
errorMessageis already the sanitised, user-facing text; internal provider strings are not exposed by the API. - Emit a metric per terminal state. A rising
failedrate on one model is the signal that something upstream changed.
Validate input before you spend
GET /api/v1/catalog returns every public model with its display metadata,
price table and parameter schema derived from that model's input DTO. It is
anonymous — no key required.
curl https://api.api-stock.com/api/v1/catalog
A single model:
curl https://api.api-stock.com/api/v1/catalog/veo3.1-fast
Use it to:
- Validate a form client-side against the same enums and ranges the API
enforces, so an impossible
resolutionnever becomes a 400. - Discover new models without a deploy. The catalog is the source the public site itself renders from; a model appears there as soon as it exists.
- Detect drift. If a hard-coded enum in your client no longer matches the catalog, that is your notice to update — before a user finds it.
Do not treat the catalog as a substitute for handling 400s. It is a convenience, not a contract; the DTO is the contract.
The short version
- One key per environment; rotate by overlap; never in a browser.
- Webhooks first, polling with backoff second, and a secret in the webhook URL.
taskIdis the idempotency key — persist it before you need it.- Branch on
status, then onfilesversusoutput. - Retry 5xx, never 4xx, and never a
failedtask. - Alert on a balance floor, not on 402.
- Stay under 120 requests per 60 seconds, polls included.
- Copy media within 24 hours; log the
taskIdforever.
Next steps
- Quick start — the first request, if you are still wiring up.
- Generation lifecycle — what happens between
create and
finished. - Webhooks, Polling task status.
- Errors & error codes, Rate limits, Pricing & balance.
- Models overview.
Run this with your own key
Every model in this guide is live in the API Stock catalog — one API key, one prepaid balance, no separate signup per provider.