Poll, stream, and cancel tasks
Guide
Use the Task model for lifecycle states and result fields.
List, cancel, and stream
Section titled “List, cancel, and stream”| Endpoint | Use when |
|---|---|
GET /api/v1/tasks | List recent tasks for your account or key. Use query filters from the API Reference for status/date pagination. |
GET /api/v1/tasks/{taskId} | Read one task descriptor. This is the canonical poll endpoint. |
POST /api/v1/tasks/{taskId}/cancel | Request cancellation for an active task when the API key has cancel scope. Pre-submit work can be cancelled immediately; post-submit work requires explicit Provider confirmation. |
GET /api/v1/tasks/{taskId}/stream | Subscribe to server-sent task status events instead of polling. Still handle reconnects and terminal-state reads. |
The task list returns a customer-safe pricing summary with the quote version
and final billed components (key, label, and credits). It does not expose
internal pricing limits, pricing-rule configuration, or provider/model
availability flags. Use creditsRequired and creditsCharged for the task
totals, and the pricing.components array when you need the itemized final
amount.
Cancellation is bounded by the Provider submission fence:
- Before the Provider request starts, a successful
200response atomically marks the taskCANCELLED, refunds the exact credits deducted at creation, and removes the inactive queue job. - After submission starts, OmnAPI cancels and refunds only when the Provider explicitly confirms remote cancellation.
- If submission outcome is unknown, the Provider does not support confirmed
cancellation, or cancellation times out/fails, the API returns
409 CONFLICT. The task remains active, is not refunded, and must continue to be polled. Direct Suno, Producer, and MV tasks currently follow this rule after submission.
Treat only 200 with success: true as a settled cancellation. On 409, read
details.cancellationReason for the diagnostic reason and continue polling the
original task. This prevents an already accepted Provider job from completing
for free after a local-only cancellation.
The stream endpoint is a standard text/event-stream response:
curl -N https://api.omnapi.com/api/v1/tasks/task_01H.../stream \ -H "x-api-key: $OMNAPI_KEY" \ -H "Accept: text/event-stream"| SSE event | data payload | Client action |
|---|---|---|
connected | { taskId, status, task } initial snapshot | Render immediately; it may already be terminal |
update | Latest task descriptor | Replace the locally cached task |
complete | Terminal task descriptor | Persist outputs and close the stream |
reconnect | { taskId, reason, pollUrl } plus an SSE retry hint | Close, poll pollUrl, then reconnect if still active |
error | { message } | Close and confirm terminal state through the poll URL |
Comment heartbeats (: ping) arrive every 25 seconds and carry no business
state. Always perform a final GET /api/v1/tasks/{taskId} after a disconnect
whose last event was not complete.
For webhook delivery, prefer Webhook Events on
production backends. SSE is useful for user-facing dashboards and short-lived
interactive sessions. Because the stream endpoint requires x-api-key, browser
clients should use fetch streaming or a backend proxy rather than native
EventSource, which cannot set custom headers.
Polling
Section titled “Polling”Back off with a cap. Polling faster will not make generation complete sooner and can consume your rate-limit budget.
The following is a client-side example policy, not a promise about OmnAPI’s internal polling cadence. Start quickly for interactive work, then cap the interval:
| Attempt | Wait before next |
|---|---|
| 1 | 2s |
| 2 | 4s |
| 3 | 8s |
| 4 | 15s |
| 5+ | 30s (capped) |
The downloadable polling helper checks HTTP failures,
honors Retry-After, and stops at a local deadline. A local timeout does not
cancel the server task; resume with the same task ID. It never creates a replacement.
type Task = { taskId: string; status: string; [key: string]: unknown };
/** A deadline stops this client; it does not cancel the server task. */export async function pollTask( taskId: string, apiKey: string, options: { timeoutMs?: number; signal?: AbortSignal; fetch?: typeof fetch; now?: () => number; sleep?: (ms: number, signal: AbortSignal) => Promise<void>; } = {},): Promise<Task> { const now = options.now ?? Date.now; const deadline = now() + (options.timeoutMs ?? 45 * 60_000); const request = options.fetch ?? fetch; const deadlineSignal = AbortSignal.timeout(options.timeoutMs ?? 45 * 60_000); const signal = options.signal ? AbortSignal.any([options.signal, deadlineSignal]) : deadlineSignal; const sleep = options.sleep ?? ((ms: number, signal: AbortSignal) => new Promise<void>((resolve, reject) => { signal.throwIfAborted(); const abort = () => { clearTimeout(timer); reject(signal.reason); }; const timer = setTimeout(() => { signal.removeEventListener('abort', abort); resolve(); }, ms); signal.addEventListener('abort', abort, { once: true }); })); for (let attempt = 0; now() < deadline; attempt++) { signal.throwIfAborted(); const response = await request(`https://api.omnapi.com/api/v1/tasks/${encodeURIComponent(taskId)}`, { headers: { 'x-api-key': apiKey }, signal: AbortSignal.any([signal, AbortSignal.timeout(30_000)]), }); let waitMs = Math.min(2000 * 2 ** Math.min(attempt, 4), 30_000); if (response.status === 429 || response.status === 503) { const retry = response.headers.get('Retry-After'); if (retry) { const seconds = Number(retry); const delay = Number.isFinite(seconds) ? seconds * 1000 : Date.parse(retry) - now(); if (Number.isFinite(delay)) waitMs = Math.max(waitMs, delay); } } else { if (!response.ok) throw new Error(`Task query HTTP ${response.status}; preserve taskId ${taskId}.`); const task = await response.json() as Task; if (typeof task.status !== 'string') throw new Error('Invalid task response; preserve the original receipt.'); if (['COMPLETED', 'FAILED', 'CANCELLED'].includes(task.status)) return task; } if (now() + waitMs >= deadline) break; await sleep(waitMs, signal); } throw new Error(`Polling deadline reached; resume task ${taskId}, do not create a replacement.`);}