Docs

Working with the API

Idempotency

Send a key with every submit. Replay it and you get the same job back, not a second charge — and the key names the job.

A network timeout does not tell you whether the request arrived. Retrying blindly is how you get charged twice for one image.

Send an Idempotency-Key and the retry is safe.

curl -X POST https://queue.auray.run/auray-ai/krea-2-turbo/text-to-image \
  -H "Authorization: Bearer $AURAY_API_KEY" \
  -H "Idempotency-Key: 8f14e45f-ea8f-4d3c-9b21-7c3e0f5a1d92" \
  -H "Content-Type: application/json" \
  -d '{ "prompt": "…", "aspect_ratio": "1:1", "resolution": 1024 }'

The header and an idempotency_key field in the body are the same thing; send either. One to 64 characters of letters, digits, dot, dash or underscore. A UUID per submit is the simple choice.

#The key is the job's name

The submit answers request_id: "photo_8f14e45f-…" — the product, an underscore, and the key you sent. That is the id every other route takes: the status poll, the result, GET /v1/jobs/{id}, the assets, the cancel. There is no second id to store.

#What a replay does

Send the same key twice and the second request returns the same job, at whatever status it has now, with "replayed": true and nothing charged:

200 OK, the second time
{
  "status": "IN_QUEUE",
  "request_id": "photo_docs-mtpb1vnh",
  "job_id": "photo_docs-mtpb1vnh",
  "queue_position": 0,
  "logs": [],
  "metrics": {},
  "product": "photo",
  "provider_job_id": "f30c5f765770468097181c2bcb31fd4d",
  "model": "auray-ai/krea-2-turbo/text-to-image",
  "engine": null,
  "attribution": null,
  "credits_charged": 0,
  "credits_remaining": 38807,
  "expected_seconds": 39,
  "replayed": true,
  "status_url": "…", "response_url": "…", "cancel_url": "…"
}

That holds for the whole life of the job. A replay that arrives while the job is still waiting in the queue gets that waiting job — "status": "IN_QUEUE", its queue_position, "replayed": true — and the job keeps the body it was first sent with: whatever the replay carries is not read, so a different prompt under the same key never replaces the one that is queued, and the replay's credits_quoted and expected_seconds are that first body's — what the job will be charged when it starts, and how long it will take. Replaying after it has finished gives you the finished job — one that failed before it ever reached a model included, answered with its failure and nothing charged. And it is the job that answers, wherever the replay was sent: the same key sent to another address of the same product — two catalogue models, say — hands back the first job, naming the address it was submitted to and pointing its status_url, response_url and cancel_url there.

#A key belongs to the key that created it

An idempotency key is owned by the API key that first used it. If a different API key on the same account sends the same idempotency key, it is refused — whether that job has started or is still waiting in the queue, where it is charged to the key that queued it when it starts:

409 Conflict
{
  "error": "idempotency_key_owned",
  "error_type": "ConflictError",
  "retry": "new_key",
  "charged": "no",
  "docs": "https://auray.ai/docs/api/errors#idempotency_key_owned",
  "request_id": "54b4754a-3e17-4629-adb1-7d3f067e97d3"
}

Without that rule, two services that happen to generate the same identifier would silently collect each other's jobs. A 409 says the collision happened; a 200 would have hidden it.

Same key, same job, every time. Different key, refused. Two 409s are about the key rather than a job: in_flight while the first request with it is still being charged and handed to its model — poll the job, it is on its way — and key_settled for a key whose first request was refunded before any job was recorded under it, which nothing will ever answer: send a new key.

Generate keys per submit, not per user or per prompt

An idempotency key derived from something stable — a user id, a hash of the prompt — will collide the moment the same input is submitted twice on purpose, and the second one will hand back the first job instead of generating anything.

#A key is used once, for good

A key does not expire. It stays the job's name for as long as the job's record is kept, which is longer than what the job made: past the 7 days after which its results are deleted and it leaves GET /v1/jobs (Jobs), a resend under the same key is still answered as that job, "replayed": true, and nothing new runs.

And a replay does not compare bodies. The same key with a different prompt, another model or other settings is handed the first job, and what the resend carries is not read. So a key that has done its job is spent: to make something new, send a new key.

#Each door that takes a key

The submit is the door this page is about, and it is not the only one. Each door below reads the Idempotency-Key header and an idempotency_key field in the body — the body's wins where both are sent — and each does something different with a key it has seen before.

DoorSent again with the same key
A submit, POST queue.auray.run/<address>, and the same body on auray.runThe same job, at whatever status it has now, "replayed": true and nothing charged.
POST /v1/uploads — where a key is requiredThe same object: the same path and read link with a fresh upload_url, and a second PUT replaces the bytes.
POST /v1/chatNever a second turn: 409 in_flight while the first is still running, 409 key_settled once it has finished, carrying its message_id — read that turn instead.
POST /v1/chat/conversationsThe same thread, answered 200 with "replayed": true and the model and title the first send gave it.
/openai/v1/chat/completions, /responses and /completions, on one of our own chat modelsAs POST /v1/chat: 409 in_flight, then 409 key_settled. The key names the turn, which GET /v1/jobs/chat_<key> reads.
The same three, on a catalogue language modelThe same job, waited on again and answered when it finishes, with no second charge. One that names no key is given one for 10 minutes.
/openai/v1/embeddings409 in_flight while the batch is embedded and 409 key_settled once it has been served; a key whose batch was refunded whole is not used up (Embeddings).

A key is one to 64 letters, digits, dot, dash or underscore. A submit refuses any other with 400 invalid_idempotency_key, in the header or the body, and so do an upload, a chat turn and embeddings for a body idempotency_key. Every other key — an Idempotency-Key header on any door but the submit, and a body key on POST /v1/chat/conversations and the chat completions doors — is not refused: it is folded into one inside that set (its SHA-256), so the same key is still the same request.

#When to send one

Always, on every submit. It costs one header.

It matters most where a retry is automatic and invisible: a job queue, a serverless function that may be replayed, an HTTP client with retries switched on. Those are exactly the places a duplicate charge appears without anyone deciding to retry.

#What it does not cover

Idempotency protects the doors in the table above and nothing else. Reads, cancellations, deletions and asset fetches are already safe to repeat and take no key; registering a webhook URL you already have resets that endpoint rather than adding a second one. Every other write — a Cinema Studio project, a Community comment — reads no key, so a resend of it is a second one.

Start typing to search every page of the documentation.

Up arrow Down arrow to moveEnter to openEscape to close