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 }'import { createClient } from "@auray/client";
const auray = createClient(); // reads AURAY_API_KEY
const job = await auray.queue.submit("auray-ai/krea-2-turbo/text-to-image", {
input: {
"prompt": "…",
"aspect_ratio": "1:1",
"resolution": 1024
},
idempotencyKey: "8f14e45f-ea8f-4d3c-9b21-7c3e0f5a1d92",
});
console.log(job.request_id, job.status);from auray_client import Client
auray = Client() # reads AURAY_API_KEY
job = auray.queue.submit(
"auray-ai/krea-2-turbo/text-to-image",
input={
"prompt": "…",
"aspect_ratio": "1:1",
"resolution": 1024
},
idempotency_key="8f14e45f-ea8f-4d3c-9b21-7c3e0f5a1d92",
)
print(job["request_id"], job["status"]){
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "submit_job",
"arguments": {
"address": "auray-ai/krea-2-turbo/text-to-image",
"input": {
"prompt": "…",
"aspect_ratio": "1:1",
"resolution": 1024
},
"idempotency_key": "8f14e45f-ea8f-4d3c-9b21-7c3e0f5a1d92"
}
}
}const res = await fetch("https://queue.auray.run/auray-ai/krea-2-turbo/text-to-image", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.AURAY_API_KEY}`,
"content-type": "application/json",
"Idempotency-Key": "8f14e45f-ea8f-4d3c-9b21-7c3e0f5a1d92",
},
body: JSON.stringify({
"prompt": "…",
"aspect_ratio": "1:1",
"resolution": 1024
}),
});
console.log(res.status, await res.json());import os, requests
r = requests.post(
"https://queue.auray.run/auray-ai/krea-2-turbo/text-to-image",
headers={"Authorization": f"Bearer {os.environ['AURAY_API_KEY']}", "Idempotency-Key": "8f14e45f-ea8f-4d3c-9b21-7c3e0f5a1d92"},
json={
"prompt": "…",
"aspect_ratio": "1:1",
"resolution": 1024
},
)
r.raise_for_status()
print(r.json())package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
)
func main() {
body := []byte(`{
"prompt": "…",
"aspect_ratio": "1:1",
"resolution": 1024
}`)
req, err := http.NewRequest("POST", "https://queue.auray.run/auray-ai/krea-2-turbo/text-to-image", bytes.NewReader(body))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("AURAY_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "8f14e45f-ea8f-4d3c-9b21-7c3e0f5a1d92")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.Status, string(out))
}<?php
$ch = curl_init("https://queue.auray.run/auray-ai/krea-2-turbo/text-to-image");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("AURAY_API_KEY"),
"Content-Type: application/json",
"Idempotency-Key: 8f14e45f-ea8f-4d3c-9b21-7c3e0f5a1d92",
],
CURLOPT_POSTFIELDS => json_encode([
"prompt" => "…",
"aspect_ratio" => "1:1",
"resolution" => 1024,
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
]);
$response = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response, PHP_EOL;require "net/http"
require "json"
uri = URI("https://queue.auray.run/auray-ai/krea-2-turbo/text-to-image")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{ENV.fetch("AURAY_API_KEY")}"
req["Idempotency-Key"] = "8f14e45f-ea8f-4d3c-9b21-7c3e0f5a1d92"
req["Content-Type"] = "application/json"
req.body = {
"prompt" => "…",
"aspect_ratio" => "1:1",
"resolution" => 1024
}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.code, res.bodyimport java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Main {
public static void main(String[] args) throws Exception {
String body = """
{
"prompt": "…",
"aspect_ratio": "1:1",
"resolution": 1024
}""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://queue.auray.run/auray-ai/krea-2-turbo/text-to-image"))
.header("Authorization", "Bearer " + System.getenv("AURAY_API_KEY"))
.header("Idempotency-Key", "8f14e45f-ea8f-4d3c-9b21-7c3e0f5a1d92")
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}using System.Net.Http.Headers;
using System.Text;
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AURAY_API_KEY"));
using var request = new HttpRequestMessage(HttpMethod.Post, "https://queue.auray.run/auray-ai/krea-2-turbo/text-to-image");
request.Headers.Add("Idempotency-Key", "8f14e45f-ea8f-4d3c-9b21-7c3e0f5a1d92");
var body = """
{
"prompt": "…",
"aspect_ratio": "1:1",
"resolution": 1024
}
""";
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");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:
{
"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:
{
"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.
#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 submit, and the upload that requests somewhere to put a file. Reads, cancellations and asset fetches are already safe to repeat and take no key.