- Home
- Developers
- Advanced usage
Guide
Advanced usage
Webhooks, batches, idempotent retries, pagination and error handling.
What to know before you go to production: webhooks, safe retries, errors, rate limits, projects and roles, budget approval, revisions and deleting files.
Every endpoint and field is listed in the API reference; this page is about how to use them.
Receive results with webhooks
Instead of polling, register a URL. Whenever a video's status changes, Azimo POSTs an event to it. You need a key with the owner role:
curl -s https://azimo.ai/v1/webhooks \
-H "Authorization: Bearer $AZIMO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: webhook-001" \
-d '{"url": "https://example.com/hooks/azimo", "name": "Production updates"}'
The response includes a signing_secret (it starts with whsec_). It is shown only this once, so store it.
An event looks like this. Its type is video. plus the status, such as video.progress, video.needs_input, video.complete, video.rejected, video.failed or video.cancelled:
{"id": "d8009f54780243ea", "sequence": 12, "type": "video.complete", "api_version": "v1", "project_id": "c56eb945e4464aec", "created_at": 1790000000, "data": {"id": "b6bf4689035d474f", "status": "complete", "stage": "complete", "progress": 100}}
Each delivery carries two headers: Vidgen-Event-Id (the event id) and Vidgen-Signature: t=<timestamp>,v1=<signature>. The signature is the hex HMAC-SHA256(signing_secret, "<timestamp>." + raw body). Always verify the raw bytes you received; do not parse and re-serialize the JSON first:
import hashlib, hmac, time
def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
try:
parts = dict(item.split("=", 1) for item in header.split(","))
timestamp = int(parts["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - timestamp) > tolerance: # reject anything older than 5 minutes
return False
expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
The Python SDK ships the same check as Vidgen.verify_webhook(secret, header, body); in the TypeScript SDK it is Vidgen.verifyWebhook(secret, header, body).
When an event arrives:
- Once the signature checks out, return a 2xx quickly and do slow work in your own queue.
- Failed deliveries are retried for up to about 72 hours, so the same event can arrive more than once: de-duplicate by event
id. - Events can arrive out of order. When in doubt, call
GET /v1/videos/{id}for the current state. - New event types may be added; ignore any
typeyou do not recognise. - Download links expire; fetch the video again when you need fresh ones.
You can also set a callback for a single video: add webhook_url to POST /v1/videos, and use the webhook_signing_secret in the response to verify it. If you would rather not run a webhook at all, read events in order with GET /v1/events?after=<sequence>.
Retry safely with Idempotency-Key
Every POST that creates or changes something (uploads, plans, productions, revisions, cancels, answers, webhooks and so on) accepts an Idempotency-Key header of 1 to 128 characters.
- Same key, same body: you get the first result back and nothing is done twice.
- Same key, different body:
409with the codeidempotency_conflict. - New key: a new request. Submitting the same plan with a new key makes another video.
In practice: generate the key before sending, store it with your own record, and reuse it when you retry after a timeout or a dropped connection.
Handle errors
Every error has the same shape:
{"error": {"code": "validation_error", "message": "request validation failed", "details": [{"loc": ["body", "options", "duration_max"], "msg": "Input should be less than or equal to 180", "type": "less_than_equal"}], "request_id": "64ceb19e540b438f8b9a4df3bfe62ba7"}}
Branch on code, never on the wording of message. Every response carries an X-Request-ID header; include the request_id when you contact us.
| HTTP | code | What to do |
|---|---|---|
| 401 | unauthenticated | The key is missing, wrong or revoked; use a valid one |
| 402 | insufficient_balance / budget_exceeded | Not enough balance, or a spending limit was reached; top up or contact sales@azimo.ai rather than retrying |
| 403 | forbidden | Your role or project does not allow it, for example read-only key |
| 403 | contact_sales | Anything about pricing; contact sales@azimo.ai |
| 404 | not_found | The id does not exist, or is not in the current project |
| 409 | conflict / idempotency_conflict | The state does not allow it (the video has already finished, say), or the key was used for a different request |
| 413 | payload_too_large / storage_quota_exceeded | The file is over 60 MB, or your storage is full; delete unused assets |
| 415 | unsupported_media_type | That file type is not accepted |
| 422 | validation_error | A field is wrong; details says which |
| 429 | rate_limited | Too many requests; wait for Retry-After and try again |
| 500 | internal_error | Something broke on our side; contact us with the request_id |
| 503 | backend_unconfigured | This workflow is not available right now |
Rate limits and 429
- By default each key can make up to 600 API requests per minute.
- Each organization can start a limited number of productions per hour, and a key can have its own, lower limit.
- Going over returns
429with aRetry-Afterheader giving the seconds to wait.
The Python SDK automatically retries 429 and 5xx responses for GETs and for POSTs that carry an Idempotency-Key, honouring Retry-After. When polling a video, once every 10 seconds or so is enough.
Projects and key roles
Assets, plans, videos and webhooks all belong to a project. Choose one with the X-Project-ID header; without it you use your default project. An organization owner can create projects with POST /v1/projects and keys with POST /v1/api-keys, setting each key's role and optional project_id.
| Role | What it can do |
|---|---|
viewer | Read only: videos, plans, assets, events and balance |
editor | Also upload, create plans, start production, cancel, answer and revise |
owner (bound to a project) | Also manage that project's webhooks |
owner (not bound) | Organization owner: manages all projects and keys |
Give each app or client its own key, with the smallest role that does the job.
Budget approval
If a video would go past its spending limit during production, it pauses at needs_input and result.input_required reads:
{"code": "budget_approval", "actor": "customer", "message": "…", "approvable": true}
To let it continue, answer:
curl -s https://azimo.ai/v1/videos/b6bf4689035d474f/resume \
-H "Authorization: Bearer $AZIMO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: approve-b6bf-001" \
-d '{"approve_budget": true}'
In the SDK that is azimo.approve_budget(video_id); on the command line, azimo resume ID --approve-budget. Your balance still has to cover the rest, otherwise you get a 402. Steps that are already done are not redone.
Answer any other needs_input in words with {"message": "..."}, adding "assets": ["<asset id>"] when it asks for more material. A video left unanswered for too long (7 days by default) is cancelled automatically.
Revisions
You can revise a video that has ended (complete, rejected, failed, cancelled or needs_input):
curl -s https://azimo.ai/v1/videos/b6bf4689035d474f/revisions \
-H "Authorization: Bearer $AZIMO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: revise-b6bf-001" \
-d '{"prompt": "The full new brief: same product, but end on the two-cup capacity.", "options": {"subtitles": true}}'
- You get a new video whose
parent_idpoints to the original; the original stays as it is. promptreplaces the original brief entirely, so write it in full.optionschanges only the fields you send. Leave outassetsto keep the original material; if you have deleted it, you get a404, so upload it again and passassets.- A revision is a complete new production and uses your balance. While the original is still in production you get a
409.
Cancel and delete
- Cancel:
POST /v1/videos/{id}/cancel. Queued or waiting videos stop at once; one in production stops before its next step. A cancelled video is charged for what it had already used. - No charge for failed QA: a video that ends
rejected(it did not pass our quality checks) orfailed(we could not make it) is not charged. Its whole hold is returned, and your wallet entries show a zero-amountsettlesaying why. - Delete an upload:
DELETE /v1/assets/{id}. Videos already made from it are not affected, but a later revision of such a video needs the material again. - Delete a video's files:
DELETE /v1/videos/{id}, only once the video has ended (cancel it first). The film, subtitles and cover are removed and the download links stop working; the video record stays. - Brands, templates and webhooks can be deleted with
DELETEtoo. - Finished films are kept for a limited time, so download and store them promptly.