Working with the API
Addresses
A model's name is its URL, every address publishes its own contract, and the catalogue tells you which ones run today.
There is no model field in any request body. The model is the path.
POST https://queue.auray.run/auray-ai/krea-2-turbo/text-to-image
└──────┬─────┘ └──────┬─────┘ └───┬────┘
namespace model taskTwo to six segments, namespace first, then the model and usually what it does. Where a maker
versions a model, the version is part of the model's name and so of its address —
bytedance/seedream/v5/pro/text-to-image, veed/lipsync/v2 beside veed/lipsync — and each version
is an address of its own. What is never in the path is a version of this API: no v1 in front, and no
per-model client library.
#Why it is shaped that way
You can construct a URL from a name you read anywhere. A model mentioned in a changelog entry is immediately callable — nothing to look up first.
Nothing in the body identifies the model, so a request body is only the parameters. Switching models means changing the URL, not rewriting the payload.
The same shape at every address. Whatever you learn calling one address applies to the next.
#What is out there
353 addresses, each with a published request shape you can build against and a page in
the API reference. An address whose request shape is not published yet has no page, and a
submit to it refuses with model_not_described rather than guess at your body.
Available right now
A model can be described and still unavailable — paused, or not open yet. Which ones run changes without a release; until one does, a submit to it is refused before anything is charged:
{
"error": "model_not_connected",
"error_type": "NotImplementedError",
"message": "hitem3d-hi3d-split is not available yet.",
"retry": "never",
"charged": "no",
"docs": "https://auray.ai/docs/api/errors#model_not_connected",
"request_id": "75c787b6-6628-49d3-a6a0-6342199dd0a2"
}GET /v1/models reports connected, described or undescribed per address, and
GET /v1/catalogue counts them without a key. Those are the only answers that are current, so do
not cache either for long. Connected means it runs; it is not a promise that a model has been
tested.
Two more answers say that a model you could run is stopped, and why. paused means a submit is
refused with a 503 before anything is charged — the model or its product is paused — and the row
carries the code that refusal would answer and, as reason, our note when there is one;
model_disabled and vendor_disabled also send x-should-retry: false, because retrying will not
help until it is resumed. retired means the
address answers 410 model_deprecated, and the row names the successor to use. The chat slots
(kind: chat, called at /v1/chat) and the embedding model (kind: embedding) are rows too, listed
when you ask for them: ?kind= takes one kind or a comma-separated list, as in ?kind=chat,embedding.
Left out, the listing is every model address.
A catalogue model is priced in credits when you submit it: the submit's answer carries
credits_charged, and that is the whole of its price on every page and document here.
#Finding one
curl "https://api.auray.ai/v1/models?limit=50" \
-H "Authorization: Bearer $AURAY_API_KEY"import { createClient } from "@auray/client";
const auray = createClient(); // reads AURAY_API_KEY
const page = await auray.platform.models.list({ limit: 50 });
console.dir(page, { depth: null });from auray_client import Client
auray = Client() # reads AURAY_API_KEY
page = auray.platform.models.list(limit=50)
print(page){
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "search_models",
"arguments": {
"limit": 50
}
}
}const res = await fetch("https://api.auray.ai/v1/models?limit=50", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.AURAY_API_KEY}`,
},
});
console.log(res.status, await res.json());import os, requests
r = requests.get(
"https://api.auray.ai/v1/models?limit=50",
headers={"Authorization": f"Bearer {os.environ['AURAY_API_KEY']}"},
)
r.raise_for_status()
print(r.json())package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("GET", "https://api.auray.ai/v1/models?limit=50", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("AURAY_API_KEY"))
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://api.auray.ai/v1/models?limit=50");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("AURAY_API_KEY"),
],
]);
$response = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response, PHP_EOL;require "net/http"
uri = URI("https://api.auray.ai/v1/models?limit=50")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{ENV.fetch("AURAY_API_KEY")}"
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 {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.auray.ai/v1/models?limit=50"))
.header("Authorization", "Bearer " + System.getenv("AURAY_API_KEY"))
.GET()
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}using System.Net.Http.Headers;
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AURAY_API_KEY"));
using var request = new HttpRequestMessage(HttpMethod.Get, "https://api.auray.ai/v1/models?limit=50");
using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");{
"object": "list",
"data": [
{ "id": "auray-ai-krea-2-turbo-text-to-image", "address": "auray-ai/krea-2-turbo/text-to-image", "category": "text-to-image", "status": "connected", "openapi": "https://api.auray.ai/v1/models/auray-ai/krea-2-turbo/text-to-image/openapi.json" }
],
"total": …,
"described": …,
"connected": …
}total, described and connected count the whole catalogue whatever you filter by — ask the
API for today's figures.
?category=image-to-3d filters by what a model does, ?status=connected by whether it runs, and
?q=hunyuan by a word in the address.
Each address also says how its jobs live and who may run it. lifecycle has whether a full
platform queues it (queue), what a stop does (cancel), whether and how long a caller may wait
(waitable, wait_ceiling_seconds), how often to poll, when a silent job is reaped and how long one
usually takes. modality is what it makes, which is how a plan opens catalogue models — a model the
plan leaves out is refused model_not_in_plan, unless a pass you hold for it covers the job — and licence, when it is not null, names the
territories it may not be served in. The models by task index groups
every address by what it makes.
#Reading what an address takes
Every address publishes two documents, and neither needs a key:
curl https://api.auray.ai/v1/models/auray-ai/krea-2-turbo/text-to-image/openapi.jsonimport { createClient } from "@auray/client";
const auray = createClient(); // this route needs no key
const doc = await auray.platform.models.document("auray-ai/krea-2-turbo/text-to-image", "openapi.json");
console.dir(doc, { depth: null });from auray_client import Client
auray = Client() # this route needs no key
doc = auray.platform.models.document("auray-ai/krea-2-turbo/text-to-image", "openapi.json")
print(doc)const res = await fetch("https://api.auray.ai/v1/models/auray-ai/krea-2-turbo/text-to-image/openapi.json", {
method: "GET",
});
console.log(res.status, await res.json());import requests
r = requests.get(
"https://api.auray.ai/v1/models/auray-ai/krea-2-turbo/text-to-image/openapi.json",
)
r.raise_for_status()
print(r.json())package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, err := http.NewRequest("GET", "https://api.auray.ai/v1/models/auray-ai/krea-2-turbo/text-to-image/openapi.json", nil)
if err != nil {
panic(err)
}
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://api.auray.ai/v1/models/auray-ai/krea-2-turbo/text-to-image/openapi.json");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response, PHP_EOL;require "net/http"
uri = URI("https://api.auray.ai/v1/models/auray-ai/krea-2-turbo/text-to-image/openapi.json")
req = Net::HTTP::Get.new(uri)
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 {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.auray.ai/v1/models/auray-ai/krea-2-turbo/text-to-image/openapi.json"))
.GET()
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Get, "https://api.auray.ai/v1/models/auray-ai/krea-2-turbo/text-to-image/openapi.json");
using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");curl https://api.auray.ai/v1/models/auray-ai/krea-2-turbo/text-to-image/llms.txtimport { createClient } from "@auray/client";
const auray = createClient(); // this route needs no key
const doc = await auray.platform.models.document("auray-ai/krea-2-turbo/text-to-image", "llms.txt");
console.log(doc);from auray_client import Client
auray = Client() # this route needs no key
doc = auray.platform.models.document("auray-ai/krea-2-turbo/text-to-image", "llms.txt")
print(doc)const res = await fetch("https://api.auray.ai/v1/models/auray-ai/krea-2-turbo/text-to-image/llms.txt", {
method: "GET",
});
console.log(res.status, await res.text());import requests
r = requests.get(
"https://api.auray.ai/v1/models/auray-ai/krea-2-turbo/text-to-image/llms.txt",
)
r.raise_for_status()
print(r.text)package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, err := http.NewRequest("GET", "https://api.auray.ai/v1/models/auray-ai/krea-2-turbo/text-to-image/llms.txt", nil)
if err != nil {
panic(err)
}
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://api.auray.ai/v1/models/auray-ai/krea-2-turbo/text-to-image/llms.txt");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response, PHP_EOL;require "net/http"
uri = URI("https://api.auray.ai/v1/models/auray-ai/krea-2-turbo/text-to-image/llms.txt")
req = Net::HTTP::Get.new(uri)
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 {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.auray.ai/v1/models/auray-ai/krea-2-turbo/text-to-image/llms.txt"))
.GET()
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Get, "https://api.auray.ai/v1/models/auray-ai/krea-2-turbo/text-to-image/llms.txt");
using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");The OpenAPI document's components.schemas.Input is the request body: every field with its type,
whether it is required, its default and its bounds — nested the way the model takes it, so a
field written image_ref[].url in the model's own documentation is an array of objects here, not a
flat key. info["x-auray-price"] is the price rule in credits (for a catalogue model: credits per unit of what it counts — seconds, megapixels, characters, or a text's bytes where unit is utf8_byte — band by band under rule, with the most a request may ask, and under rule.inputs what it is billed for sending or choosing: each input image, each byte of a prompt, an input video's or audio's seconds — and the output's, where it runs as long — or a reference's megapixels at the most they can come to, once a request where counts is any:; per second of output where counts is any: times the seconds, as a video whose settings select a higher rate or a resolution preset on the request whose when names it; an input or a preset with max_units 0 is refused before any charge, and one with no counts is a fee on the requests its when names; a floor: in counts is the least the factors before it are counted at — a frame's megapixels, so no second of video is quoted below the lowest rate — an inv: a speed counted the other way, never below one: half the speed, twice the count — and a spoken: text each character by the time it takes to say, as the rule's sentence weighs each script, a digit, a letter read by its name and a symbol; for a catalogue language model, where unit is million_tokens: prompt_credits and answer_credits a million tokens, a long_prompt pair past its above_tokens, and what the quote counts — prompt_bytes_per_token, image_prompt_tokens, document_prompt_tokens for a request that sends a document, default_answer_tokens when settings.maxTokens is left out and cache_write_multiplier for a cached prompt; and on every address metered, true where the price is a hold — the job is charged what it cost, never above the quote — and false where the quote is the charge), info["x-auray-scope"] the scope a new key is minted
with, and info["x-auray-scopes"] every scope that also works — the older name a key may already hold. The
whole index is one line per address at api.auray.ai/llms.txt.
A body is held to that document. A field it does not list, or a value outside its bounds, is refused
before anything is charged (unknown_field, missing_field, bad_value). Our own auray-ai/ addresses are
warning first: such a body is still served, with a Deprecation header and a Link to the error, and is
refused the same way once enforcement starts, no sooner than 30 days after the warnings began. That
includes num_outputs on the video address, which the published contract never offered and which is being
withdrawn rather than published.
Fields that take a file are typed by what they take — an image, a video, a track, a mesh — and by
how it travels: as a URL, inline as base64, or as a path from Uploads. A field the
model only accepts as a file part is marked as such and refused with
unsupported_encoding if you send it, because this API sends JSON.
#Namespaces
The first segment is who publishes the model — auray-ai/ for the models behind the apps. Every
namespace is reached the same way, with the same auth and billing.
There is no practical difference, scope included: a key's scope names what it may make —
image:write, 3d:write, and text:write for a model that answers in text — and reaches every address
that makes it, whoever publishes it. (A key minted with the older scope names may carry external:write,
which still reaches every address outside auray-ai/.) The URL shape, the job states, the webhook and the
error vocabulary are identical too; each model's timing is its own lifecycle.
Our three chat slots have addresses under auray-ai/ too — auray-ai/glm-5-3-flash,
auray-ai/deepseek-v4-1-flash and auray-ai/kimi-k3 — which POST /v1/chat and /openai/v1
accept beside the slot's plain name. They are conversations rather than jobs, so they have no queue
address; GET /v1/models?kind=chat lists them by that plain name, which is what a chat turn prints as
model.
#When an address moves
An address that has been replaced answers 410 Gone and names its successor rather than disappearing:
{
"error": "model_deprecated",
"error_type": "NotFoundError",
"retry": "never",
"charged": "no",
"successor": "auray-ai/krea-2-turbo/text-to-image",
"docs": "https://auray.ai/docs/api/errors#model_deprecated",
"request_id": "54b4754a-3e17-4629-adb1-7d3f067e97d3"
}So an integration written against a retired address fails loudly with the fix in the body, instead of silently getting different output from a model that quietly changed underneath it.