Poll the job
GET
/v1 /jobs /{id}| status | stage | Meaning |
|---|---|---|
| queued | queued | queue_position counts the jobs ahead. API jobs may be scheduled after console jobs. |
| running | vocal_separation (Vocal separation) separation (Speaker separation) enhancement (Noise reduction) super_resolution (Bandwidth extension) si_sdr (SI-SDR check) | Duplex Conversation Separation, in order. Multi Mic Separation reports one step, run. |
| succeeded | done | result is set, billed_usd equals the quote. |
| failed | failed | error says why. A failed job is never charged. |
| canceled | canceled | Stopped at your request. Never charged. |
| Field | Type | Description |
|---|---|---|
| progress | 0..1 | Whole job. Only moves forward. |
| steps[] | object[] | name, state, progress. |
| queue_position | int | null | Jobs ahead. Null once running. |
| queue_wait_seconds | float | null | Estimated seconds of work ahead. Null once running, or unknown. |
| eta_seconds | float | null | Low end of eta_range_seconds; the worker's time left once running. Floor 5 s while queued, 1 s once running. |
| eta_range_seconds | [float, float] | null | Seconds until the result, low and high. Null in the create response and when unknown; the first read fills it. |
| eta_updated_at | datetime | null | When the estimate was computed. |
| delay_reason | object | null | reason is queue (position counts from 1 and ahead counts only the jobs waiting before it; ahead 0 means the job is next and waiting for a free slot), scaling (boot_seconds until a server takes work; ahead only once a server is already booting, not while the first one is still being launched), retry (ahead only), capacity (no server can start) or paused; the last two carry no other field. Null when the job can start now. |
| worker_alive | bool | null | A worker has reported recently. |
| heartbeat_age_seconds | float | null | Seconds since that report. |
| gpu_degraded | bool | null | Worker alive but declining jobs. Null when unknown. |
while :; do
JSON=$(curl -sS --fail-with-body -H "Authorization: Bearer $DUETA_API_KEY" \
https://dueta.ai/ v1/ jobs/$JOB_ID) || { echo "$JSON"; exit 1; } # 4xx/ 5xx: the error body
echo "$JSON" |
jq -r '[.status, .stage, (.progress * 100 | floor | tostring) + "%"] | join(" ")'
case $(echo "$JSON" | jq -r .status) in
succeeded) break ;;
queued|running) ;;
failed|canceled) echo "$JSON" | jq -r .error; exit 1 ;;
*) echo "$JSON"; exit 1 ;; # not a job at all: an error body
esac
sleep 2
doneResponse
{
"status": "running",
"stage": "separation",
"progress": 0.62,
"eta_seconds": 34,
"eta_range_seconds": [34, 51],
"eta_updated_at": "2026-10-19T00:50:12Z",
"delay_reason": null,
"queue_position": null,
"worker_alive": true,
"heartbeat_age_seconds": 0.8,
"steps": [
{ "name": "vocal_separation", "state": "done", "progress": 1.0 },
{ "name": "separation", "state": "running", "progress": 0.42 },
{ "name": "enhancement", "state": "pending", "progress": 0.0 },
{ "name": "super_resolution", "state": "skipped", "progress": 0.0 },
{ "name": "si_sdr", "state": "pending", "progress": 0.0 }
]
}Subscribe to events
GET
/v1 /jobs /{id} /eventsEach event: job frame is the GET body. event: done ends the stream and carries only id and status.
# -N so curl prints frames as they arrive. Each data: line is the full job.
curl -N -H "Authorization: Bearer $DUETA_API_KEY" \
https://dueta.ai/ v1/ jobs/$JOB_ID/events
# event: job
# data: {"id":"0f9c1a7e-...",
# "status":"running",
# "stage":"separation",
# "progress":0.62,...}
#
# : keepalive
#
# event: done
# data: {"id":"0f9c1a7e-...",
# "status":"succeeded"}Receive callbacks
In Organization settings, save an HTTPS URL and test it to enable job events; per-job callbacks override it with their own secret and still support HTTP. A test’s 2xx confirms acceptance, not HMAC verification.
| Field | Type | Description |
|---|---|---|
| X-Dueta-Event | header | job.running, job.succeeded, job.failed, job.canceled; upload.receiving, upload.ready, upload.failed. |
| X-Dueta-Delivery-Id | header | Same across retries; match the signed body’s delivery_id and drop a repeat. |
| X-Dueta-Timestamp | header | Unix seconds. Each attempt is stamped and signed when it is sent. |
| X-Dueta-Signature | header | v1= plus the hex HMAC. |
| User-Agent | header | DUETA-Webhooks/1.0 (+https://dueta.ai/docs/track-a-job) |
| event, type, data | body | The event name, job or upload, and the resource. |
| delivery_id, sent_at, sequence | body | sent_at has millisecond precision. Compare sequence to order two callbacks for one job: larger is newer, same on every retry. A retry of an older state stops once a newer final event was delivered. |
# Register on the job (or on an upload, same field). The secret is in THIS
# response and never again. Each job or upload has its own secret: store it by its id.
# Console organization webhook: store its one-time secret in DUETA_WEBHOOK_SECRET
# on your dedicated receiver; do not register a per-job URL to inherit it.
curl -X POST https://dueta.ai/ v1/ jobs/ separation \
-H "Authorization: Bearer $DUETA_API_KEY" \
-H "Content-Type: application/ json" \
-d '{"inputs": ["upl_9f2c7a41d0e8"],
"callback_url": "https://yours.example.com/ hooks/ dueta"}'
# -> {"id": "...", "callback_secret": "whsec_...store-this-now", ...}
# One delivery, as your server sees it:
# POST / hooks/ dueta
# X-Dueta-Event: job.succeeded
# X-Dueta-Delivery-Id: 3f9c1e7a2b4d4c8e9a0b1c2d3e4f5a6b
# (same on every retry: drop a repeat)
# X-Dueta-Timestamp: 1787881026
# X-Dueta-Signature: v1=<hex HMAC-SHA256 of "<timestamp>.<raw body>">
# User-Agent: DUETA-Webhooks/ 1.0 (+https://dueta.ai/ docs/ track-a-job)
# {"event": "job.succeeded", "sent_at": 1787881026.123,
# "sequence": 1787881026123456, "type": "job", "data": {...the job...}}Cancel
POST
/v1 /jobs /{id} /cancelNever charged. A running job answers canceling and ends canceled. Resubmitting the same inputs creates a new job.
curl -X POST https://dueta.ai/ v1/ jobs/$JOB_ID/cancel \
-H "Authorization: Bearer $DUETA_API_KEY"
# queued -> {"status": "canceled", ...} hold released now
# running -> {"status": "canceling", ...} "canceled" at the worker's next tickDelete
DELETE
/v1 /jobs /{id}POST
/v1 /jobs /{id} /restoreCases
- 204
- Moved to the trash: 404 on every read.
- Trash
- 30 days.
GET /v1/jobs?trashed=truelists it; restore brings it back. Then audio and results are deleted. - Queued or running
- Canceled first. Never charged.
- 404
- Unknown or already deleted.
- Billing
- The job still counts in usage and receipts.
curl -X DELETE https://dueta.ai/ v1/ jobs/$JOB_ID \
-H "Authorization: Bearer $DUETA_API_KEY"
# -> 204. In the trash: GET / v1/ jobs/ $JOB_ID, its stems are 404.
curl -X POST https://dueta.ai/ v1/ jobs/$JOB_ID/restore \
-H "Authorization: Bearer $DUETA_API_KEY"
# -> 200, the job as it was. Within 30 days.Cases
- 409
- Already settled: done.
- worker_alive: false
- Queued jobs fail after 10 min.
- Heartbeat
- Silent 120 s: failed, hold released.
- Callbacks
- 6 attempts over ~12 min: 1 s, 5 s, 30 s, 2 min, 10 min. Unordered.
- Deliveries
- Every attempt and its status:
GET /v1/jobs/{id}/deliveries. - Redeploy
- Pending callbacks resume after API restarts. Deduplicate delivery IDs.
- EventSource
- No Authorization header. Use fetch.
- List
GET /v1/jobs?limit=20&after=the previousnext_after. All job routes.- Filter
status,type(repeatable),q(file name or id prefix),created[gte],created[lt]:GET /v1/jobs?status=failed&type=separation.- Sort
sort(createdorcreated_at,status,type,duration,charge),direction(asc,desc). Sorted pages useoffset.