Webhook Events
OmnAPI emits webhooks for two layers of task lifecycle:
- Task-level:
task.completed,task.failed,task.cancelled— fired exactly once per task at terminal state. Backwards-compatible with the v1 event set. - Stage-level:
task.stage.started,task.stage.completed,task.stage.failed— fired for every named stage in long-running tasks (currently MV storyboard + finalize). Lets you build progress bars or observability dashboards without polling.
Both layers fire to the same destination URL, marked by the event field
in the JSON envelope.
Subscribing
Section titled “Subscribing”Pass webhookUrl in the task creation request:
curl -X POST https://api.omnapi.com/api/v1/suno/songs \ -H "x-api-key: om_live_..." \ -d '{ "mode": "simple", "prompt": "upbeat lo-fi", "config": { "webhookUrl": "https://yourapp.com/webhooks/omnapi" } }'The dashboard also lets you set a default destination for account-level task
events. Per-task webhookUrl takes precedence for that one request.
Envelope shape
Section titled “Envelope shape”Every event uses this outer envelope:
{ "event": "task.completed", "eventId": "evt_01H...", "deliveredAt": "2026-05-30T14:23:11.482Z", "task": { "id": "task_01H...", "userId": "usr_...", "modelCode": "mv-storyboard", "featureCode": "generate-storyboard", "status": "COMPLETED", "creditsRequired": 250, "creditsCharged": 250, "createdAt": "...", "completedAt": "...", "durationMs": 187432, "inputParams": { /* original POST body */ }, "outputResults": { /* final task output */ }, "metadata": { "appSlug": "mv-maker" } }}task is the same shape returned by GET /api/v1/tasks/:id. Stage events
add an inner stage block (see below).
Task-level events
Section titled “Task-level events”task.completed
Section titled “task.completed”Fired once when a task transitions to COMPLETED. task.outputResults
carries the final payload, creditsCharged reflects the actual cost
(may be less than creditsRequired if the generated output costs less than
the reserved amount).
task.failed
Section titled “task.failed”Fired once on FAILED. The envelope adds:
{ "task": { "errorCode": "MV_TOO_MANY_FAILED_SCENES", "errorMessage": "5/8 scene images failed (threshold 0.3)" }}A refund (back to creditAccount.balance) is issued atomically with the
status transition — by the time you receive the event, the refund is
already settled.
task.cancelled
Section titled “task.cancelled”Fired when a task is cancelled before it completes. Same envelope as
task.failed minus errorCode/errorMessage. Refund also atomic.
Stage-level events
Section titled “Stage-level events”Long-running tasks (currently MV storyboard generation + MV finalize)
break their work into NAMED stages tracked by TaskStageTracker. Each
stage emits three events: started → completed (success path) or
started → failed (failure path).
Stage envelope
Section titled “Stage envelope”{ "event": "task.stage.completed", "eventId": "evt_01H...", "deliveredAt": "...", "task": { /* same shape as above */ }, "stage": { "name": "scenes", "description": "Plan scenes", "startedAt": "...", "completedAt": "...", "durationMs": 47213, "attemptCount": 1, "maxAttempts": 2, "payload": { "sceneCount": 8, "genre": "rap", "mvParadigm": "performance" } }}stage.payload is stage-specific — each adapter populates it via
ctx.setPayload({...}). Don’t assume any fixed schema; treat unknown
keys as metadata.
MV storyboard stage timeline
Section titled “MV storyboard stage timeline”When you submit POST /api/v1/mv with mode: "studio", you’ll see this
sequence:
| # | Stage name | Typical duration |
|---|---|---|
| 1 | resolve-song | <1s (cache hit) or 5-15s (Suno fetch) |
| 2 | analyze | <1s (pure CPU) |
| 3 | emotion | 5-30s |
| 4 | concept | 5-30s |
| 5 | character-anchor | 15-40s (only when concept.needsCharacterAnchor=true) |
| 6 | narrative | 5-30s |
| 7 | scenes | 10-60s |
| 8 | scene-images | 60-240s (N parallel image gens, ~25s each p99) |
Followed by the task-level task.completed. Total wall-clock 3-10min for a
typical 60-second song.
MV scene render stage timeline
Section titled “MV scene render stage timeline”The render task is simpler — single render submit + poll. No stage events
emitted (the entire task IS one stage). You get task.completed directly.
MV finalize stage timeline
Section titled “MV finalize stage timeline”Currently single-stage — task.completed directly, no task.stage.*.
task.stage.failed
Section titled “task.stage.failed”Stage failures are RETRYABLE per the stage’s maxAttempts budget. You
receive task.stage.failed per attempt that bombs:
{ "event": "task.stage.failed", "stage": { "name": "scenes", "attemptCount": 1, "maxAttempts": 2, "errorCode": "LLM_TIMEOUT", "errorMessage": "stage timed out after 120s" }}Then either:
- The next attempt produces
task.stage.completed→ pipeline continues - All attempts fail → the WHOLE TASK fails → you also receive
task.failed
A stage failure does NOT necessarily mean the task failed — wait for the task-level event to know the final outcome.
Ordering + delivery guarantees
Section titled “Ordering + delivery guarantees”| Guarantee | Strength |
|---|---|
Within a task, task.stage.started precedes the corresponding task.stage.completed/failed | Strong — they’re sent in order from the same worker. |
| Stages within a task are delivered in execution order | Strong — workers fire events synchronously from the stage tracker. |
| Across tasks: events arrive in any order | No guarantee — we don’t serialize across users / tasks. |
| Exactly-once delivery | At-least-once — retries can re-deliver. Dedupe by eventId. |
| Delivery latency | <5s p99 from event creation; <60s including retry attempts. |
We retry failed webhook deliveries up to 5 times with exponential backoff
(30s → 1m → 5m → 15m → 1h). HTTP 2xx is treated as success. After 5
failures the event is marked FAILED and visible in the dashboard’s
Webhook Events page where
you can manually re-trigger.
Verifying signatures
Section titled “Verifying signatures”Treat the destination URL as a secret endpoint and restrict it to your own
domain. Store eventId and reject duplicates in your application.
Best practices
Section titled “Best practices”- Acknowledge fast. Respond 2xx within 30s, then process async. Slow webhooks consume worker connection pool.
- Dedupe by
eventId. Retries can re-deliver any event. - Don’t depend on stage events for billing logic — billing settles at task-level. Use stage events ONLY for UX / observability.
- Subscribe to
task.stage.completed, nottask.stage.startedunless you’re building a live progress bar. Started events are noisy. - Capture
eventId+deliveredAtin your observability stack — useful for debugging missed events.
See also
Section titled “See also”- Async Jobs — task lifecycle + status enum
- MV API — the canonical multi-stage flow
- Dashboard → Webhooks — see delivery history + manually retry failed events