Amux AI

Create task

Last updated September 1, 2026

The unified asynchronous generation endpoint. One path for every modality — submit, then fetch by ID.

The unified asynchronous generation endpoint. One path serves every modality. It currently covers images, and future video or audio support will keep the same path and response shape.

Every request parameter and response field is listed below.

POSThttps://gateway.amux.ai/v1/tasks

Authorization

header
AuthorizationstringRequired

Bearer <your Amux AI key> An Amux key created in the console. If you lose it, you can view it again on the keys page.

Preferstring

wait=N keeps the current request open for up to N seconds. If the task finishes in time, the endpoint returns 200 with the full result; otherwise it returns 202.

N is capped at 90. Use it when you want to avoid an extra poll without switching to SSE.

Idempotency-Keystring

Retrying with the same key does not create a second task and does not reserve funds a second time; the original task is returned.

Idempotency keys are scoped to the workspace. When retrying the same submit, reuse the same key.

Request

application/json
modelstringRequired

A catalog model ID, optionally with a :provider suffix.

The modality comes from the model — there is no separate

field. Naming an image model makes this an image task.

promptstringRequired

The prompt.

webhook_urlstring

Where to POST the result once the task reaches a terminal state;

see the "Callbacks" section above for the shape.

https only, and it must not point at a private network — we

deliver from our servers. The address is checked at submit time,

so a bad one fails immediately rather than after the task has run.

Response

200response

Either already terminal (a Prefer: wait that finished) or a replay of the same idempotency key.

Kept separate from 202 on purpose: 202 says "I took on a piece of work", and neither of these took on anything new.

202response

Accepted, and the reserve is already held. Use id to fetch the result.

202 rather than 200 because this call produced nothing — it only took

on a piece of work.

400response

Malformed request. Also covers stream: true and partial_images above 0 — this endpoint is asynchronous; synchronous streaming lives on the vendor-compatible endpoints (/v1/images/generations).

402response

The available balance cannot cover the reserve. **Available = balance

minus expired lots minus reserves already held.**

503response

Too many tasks in flight. Retry later; the response carries Retry-After.

When to use it

A high-quality image takes 80-115 seconds in practice. On the synchronous endpoint, any timeout along the way (reverse proxy, gateway, your client's default) leaves you without the image after the money has been spent — the upstream already generated and billed it.

The asynchronous flow separates submission from retrieval: submission returns immediately, generation continues in the background, and the result can be retrieved later by polling or webhook.

How it relates to the synchronous endpoint

Billing is identical: an upper bound is reserved at submit time, then settled against actual usage with the difference released.

One difference: the reserve happens at submit. An insufficient balance fails right there, rather than after the work is already done.

The model determines the modality

The model determines the task type. An image model creates an image task, and future video or audio models will follow the same rule. There is no separate type field in the request body.

The body comes in two shapes

Use application/json for pure generation. For edits with reference images, switch to multipart/form-data: put the reference images in image or image[], the mask in mask, and keep the other parameters under the same field names.

Deliver results with webhook_url

When the task reaches a terminal state, the platform POSTs this payload to that address:

{
  "event": "task.succeeded",
  "sent_at": "2026-09-01T04:31:00.000Z",
  "task": { "id": "task_…", "status": "succeeded", "output": { "images": [...] }, "usage": {...}, "cost": "0.005925" }
}

task uses the same schema as the retrieve endpoint, so the same parsing logic can be reused.

Verify the signature

The request carries Amux-Signature: t=<unix seconds>,v1=<hex>, an HMAC-SHA256 over `${t}.${raw body}`. The secret lives in your workspace settings.

Without signature verification, any party that learns the URL can forge callback payloads.

import { createHmac, timingSafeEqual } from 'node:crypto'

function verify(rawBody, header, secretHex) {
  const m = /t=(\d+),v1=([0-9a-f]+)/.exec(header)
  if (!m) return false

  // ⚠️ Check freshness first so stale callbacks can be rejected
  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(m[1]))
  if (age > 300) return false

  const expected = createHmac('sha256', Buffer.from(secretHex, 'hex'))
    .update(`${m[1]}.${rawBody}`)
    .digest('hex')

  // ⚠️ Use a constant-time comparison to avoid timing-based signature leakage
  return timingSafeEqual(Buffer.from(expected), Buffer.from(m[2]))
}

⚠️ Verify against the raw request bytes, not a JSON.parse → stringify round trip; re-serialization changes key order or whitespace and will break the signature.

Retries and deduplication

Anything outside 2xx (including 3xx) and any timeout counts as a delivery failure. After the first try we back off 10s / 1m / 5m / 30m / 1h, retrying at most five more times (six in total, spanning about two hours).

  • Return 2xx quickly and move downstream processing onto your own queue;
  • Deduplicate on task.id, because retries may deliver the same event more than once.

Callback address limits

https only, and never a private network. The address is validated at submit time, so an invalid target fails immediately.

Redirects are not followed. The callback endpoint must return 2xx directly.

This endpoint does not stream

stream: true, and partial_images above 0, return 400 here. (partial_images: 0 is the default and is accepted.)

Streaming would mean this request runs the work to completion and pushes it down the connection — the task never enters the queue, and the result is gone when the connection drops. Surviving a dropped connection is the reason this endpoint exists.

To get the result within the request, use Prefer: wait below: it returns exactly the same JSON as the retrieve endpoint, so you maintain one parser. For low-res preview frames during generation, use the vendor-compatible /v1/images/generations with stream: true.

Briefly wait for completion: Prefer: wait

Send Prefer: wait=60 to keep the current request open for up to 60 seconds. If the task finishes in time, the endpoint returns 200 with the full result, using the same schema as the retrieve endpoint.

If the task is still running when the wait window ends, the endpoint returns 202 and polling continues as usual.

N is capped at 90. Use it when you want to avoid an extra poll without switching the call to SSE.

Retry protection: Idempotency-Key

With Idempotency-Key, retrying the same create request does not create a second task and does not reserve funds a second time. The endpoint returns the original task.

This is useful when the request was sent but the client did not receive the response.

Idempotency keys are scoped to the workspace. When retrying the same submit, reuse the same key.

cURL
curl https://gateway.amux.ai/v1/tasks \
  -H "Authorization: Bearer $AMUX_API_KEY" \
  -H "Prefer: <value>" \
  -H "Idempotency-Key: <value>" \
  -d '{
    "model": "openai/gpt-image-2",
    "prompt": "A ginger cat running through fresh snow"
  }'
{
  "id": "task_01M1CG35C16CJ790D00BV1RBVM",
  "status": "queued",
  "model": "openai/gpt-image-2",
  "created_at": "<string>",
  "output": {
    "images": [
      {
        "index": 0,
        "url": "<string>",
        "layer": {
          "zIndex": 0,
          "role": "background",
          "bboxAbsolute": [
            0
          ],
          "bboxNormalized": [
            0
          ],
          "name": "<string>",
          "description": "<string>"
        }
      }
    ],
    "videos": [
      {
        "index": 0,
        "url": "<string>"
      }
    ]
  },
  "usage": {
    "input_tokens": 0,
    "output_tokens": 0,
    "total_tokens": 0,
    "output_video_seconds": 0
  },
  "cost": "0.2108",
  "notes": [
    "dropped audio: this model decides the soundtrack itself"
  ],
  "error": {
    "message": "<string>",
    "code": "<string>"
  }
}