Start here
Ask a question
/v1/chaton https://api.auray.ai/v1Answers in the same call.
Answers in the same call. stream:false returns the finished answer; stream:true writes Server-Sent Events carrying the same frames. conversation_id is optional — omit it for a one-off and a thread is minted. A stream carries an SSE comment line whenever nothing was written for fifteen seconds. A blocking answer not ready in twenty seconds is committed as 200 at once and its body starts with spaces, which JSON parsers skip; a failure after that is the error body under that 200 — internal_error, with charged: unknown, retry: poll_first and the turn's job_id: read what it cost at GET /jobs/{id} before sending it again. On our own chat models closing the connection stops the turn: it settles at what was delivered, and a turn that produced nothing — no text, no reasoning, no tool call — and reported no usage is refunded in full. On a chat model another company runs, closing the connection once we have sent the turn to the model stops only what you receive: the answer is finished and kept, and charged as the finished answer, for its prompt and what it wrote, never more than its reserve; a turn one of our time limits cuts off — the turn's deadline, a silence, or the wait for its first word — is charged in full, for its prompt and the longest answer it allowed, never more than its reserve: its maker writes the answer to its end and charges for it. A turn you stop before we sent it is refunded whole on any model. Every case is in the chat settlement table, https://auray.ai/docs/jobs#chat-settlement.
#Authentication
Authorization: Bearer auray_sk_… — text:write, or the older chat:write.
#Request
#Headers
| Name | Type | Required | Description |
|---|---|---|---|
Idempotency-Key | string | no | Your own id for this turn: the same as idempotency_key in the body, which wins when both are sent. A header outside the pattern is folded into a key inside it (its SHA-256) rather than refused, so the same header is still the same key. A turn is never replayed: the same key again is 409 in_flight while the first turn is running and 409 key_settled once it has finished, and key_settled carries the first turn's message_id — read that turn rather than sending it again. |
#Body
application/json, required.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
model | string | no | — | Required unless conversation_id names a thread of yours, whose own model is then used — the model the thread was made with is the default for its turns, and a different one is always named here explicitly (and wins). The first five are the slots. qwen and kimi are the names these slots carried until 2026-09-14 and are still accepted, mapped to glm-5-3-flash and deepseek-v4-1-flash respectively; they named models neither slot has served since 2026-09-12 and will be withdrawn once integrations have moved. No model and no thread of yours is 400 unknown_model, as it always was. A catalogue language model whose GET /models row carries chat answers here too, named by its address (auray-serverless/model/…, its catalogue slug also accepted) — no row carries it until such models are in force at the chat doors. One of "glm-5-3-flash", "deepseek-v4-1-flash", "kimi-k3", "qwen3-8", "glm-5-3", "qwen", "kimi", "auray-serverless/model/openai-gpt-5-nano", "auray-serverless/model/openai-gpt-5-mini", "auray-serverless/model/openai-gpt-5-4-nano", "auray-serverless/model/openai-gpt-5-4-mini", "auray-serverless/model/openai-gpt-5-4", "auray-serverless/model/openai-gpt-5-5", "auray-serverless/model/anthropic-claude-haiku-4-5", "auray-serverless/model/anthropic-claude-sonnet-4-6", "auray-serverless/model/anthropic-claude-opus-4-7", "auray-serverless/model/anthropic-claude-opus-4-8", "auray-serverless/model/anthropic-claude-fable-5", "auray-serverless/model/google-gemma-4-31b", "auray-serverless/model/google-gemini-3-1-flash-lite", "auray-serverless/model/google-gemini-3-flash", "auray-serverless/model/google-gemini-3-5-flash", "auray-serverless/model/google-gemini-3-1-pro", "auray-serverless/model/xai-grok-4-3", "auray-serverless/model/minimax-m3", "auray-serverless/model/zai-glm-5-1". |
effort | string | no | "balanced" | One of "light", "balanced", "deep". |
text | string | yes | — | |
conversation_id | string | no | — | |
stream | boolean | no | false | |
include_reasoning | boolean | no | false | |
idempotency_key | string | no | — | Your own id for this turn. CHAT DOES NOT REPLAY: unlike the four job products, sending the same key twice is refused — 409 key_settled once the first turn finished, 409 in_flight while it is still running — key_settled always carries the original message_id; in_flight carries it once the turn's row exists and null before that. Read that turn rather than re-sending. Idempotency-Key: as a header is accepted as an alias, and the body's key wins when both are sent. No colon, because a credit key is <product>:<user>:<this>. |
#Example request
curl -X POST https://api.auray.ai/v1/chat \
-H "Authorization: Bearer $AURAY_API_KEY" \
-H "content-type: application/json" \
-d '{
"model": "glm-5-3-flash",
"text": "In two sentences: why does a retried request need an idempotency key?"
}'import { createClient } from "@auray/client";
const auray = createClient(); // reads AURAY_API_KEY
const reply = await auray.platform.chat.turn({ model: "glm-5-3-flash", text: "In two sentences: why does a retried request need an idempotency key?" });
console.log(reply.content);from auray_client import Client
auray = Client() # reads AURAY_API_KEY
reply = auray.platform.chat.turn(model="glm-5-3-flash", text="In two sentences: why does a retried request need an idempotency key?")
print(reply["content"]){
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "chat",
"arguments": {
"model": "glm-5-3-flash",
"text": "In two sentences: why does a retried request need an idempotency key?"
}
}
}const res = await fetch("https://api.auray.ai/v1/chat", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.AURAY_API_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({
"model": "glm-5-3-flash",
"text": "In two sentences: why does a retried request need an idempotency key?"
}),
});
console.log(await res.json());import os, requests
r = requests.post(
"https://api.auray.ai/v1/chat",
headers={"Authorization": f"Bearer {os.environ['AURAY_API_KEY']}"},
json={
"model": "glm-5-3-flash",
"text": "In two sentences: why does a retried request need an idempotency key?"
},
)
r.raise_for_status()
print(r.json())package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
)
func main() {
body := []byte(`{
"model": "glm-5-3-flash",
"text": "In two sentences: why does a retried request need an idempotency key?"
}`)
req, err := http.NewRequest("POST", "https://api.auray.ai/v1/chat", 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")
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/chat");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("AURAY_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"model" => "glm-5-3-flash",
"text" => "In two sentences: why does a retried request need an idempotency key?",
], 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://api.auray.ai/v1/chat")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{ENV.fetch("AURAY_API_KEY")}"
req["Content-Type"] = "application/json"
req.body = {
"model" => "glm-5-3-flash",
"text" => "In two sentences: why does a retried request need an idempotency key?"
}.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 = """
{
"model": "glm-5-3-flash",
"text": "In two sentences: why does a retried request need an idempotency key?"
}""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.auray.ai/v1/chat"))
.header("Authorization", "Bearer " + System.getenv("AURAY_API_KEY"))
.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://api.auray.ai/v1/chat");
var body = """
{
"model": "glm-5-3-flash",
"text": "In two sentences: why does a retried request need an idempotency key?"
}
""";
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()}");#Responses
#200 OK
The finished turn, or an SSE stream when stream:true.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | string | no | — | |
job_id | string | no | — | The job's one id on every host: id here, request_id on the queue host (the queue protocol's name for it). |
object | string | no | — | One of "chat.turn". |
status | string | no | — | One of "succeeded", "failed". |
conversation_id | string | no | — | |
conversation_created | boolean | no | — | |
history_scrubbed | boolean | no | — | True when the conversation this turn continues was scrubbed seven days after its last activity: its earlier turns are gone, and this answer was written without them. Continue in a new conversation, or send the context again. |
history_budget_bytes | integer | no | — | Your plan's chat.context_budget in force (GET /plans prints it per plan): how much of the thread this turn could send, this message included, counted in bytes, not tokens — each message's UTF-8 text plus 64, and 128 a turn. This message counts first and is always sent whole, even alone over the budget; earlier messages are kept newest first while the total fits, one longer than a quarter of the budget shortened to that quarter, and the oldest past it are left out. |
history_trimmed | object | no | — | What the history budget left out of the thread this turn re-sent, null when it sent all of it. The model answered without what was dropped; continue in a new conversation, or send the context that matters again. Also sent as the X-Auray-History-Trimmed header. May be null. |
history_trimmed.dropped | integer | yes | — | Earlier messages left out whole, oldest first. |
history_trimmed.shortened | integer | yes | — | Earlier messages cut to a quarter of the budget to fit, each marked as shortened; a cut answer keeps its end. |
message_id | string | no | — | |
content | string | no | — | |
reasoning | string | no | — | |
usage | object | no | — | May be null. |
finish_reason | string | no | — | Why the model stopped: stop when it finished, length when max_output_tokens or the ceiling stopped it — the plan's, or the model's own where it cannot write the plan's inside one turn's time (GET /openai/v1/models prints it per model) — tool_calls; null when the stream ended before it said. length beside status: failed and error: output_limit_reached means the whole limit — the token ceiling, or the time one turn may run — went on reasoning before a word of answer, and the turn was refunded — send it again with a lower effort. May be null. |
credits_reserved | integer | no | — | Chat reserves the worst case and settles down to what the model produced. 0 for a turn a promotion paid for (waived: true): nothing was taken from the wallet. |
credits_charged | integer | no | — | What the turn cost — the ledger's net. 0 for a turn a promotion paid for (waived: true). Null, with settled_unknown: true, when the ledger could not be read. May be null. |
credits_remaining | integer | no | — | The wallet after this turn; null, with settled_unknown: true, when it could not be read. May be null. |
product | string | no | — | Always chat. One of "chat". |
model | string | no | — | The chat model that answered: a slot by its own name, or a catalogue language model by its published address (auray-serverless/model/…), as its catalogue jobs print it. A request may still name a slot by its pre-2026-09-14 alias (qwen, kimi); a response never does — it is always the slot's own name, so the old spellings are not in this enum. One of "glm-5-3-flash", "deepseek-v4-1-flash", "kimi-k3", "qwen3-8", "glm-5-3", "auray-serverless/model/openai-gpt-5-nano", "auray-serverless/model/openai-gpt-5-mini", "auray-serverless/model/openai-gpt-5-4-nano", "auray-serverless/model/openai-gpt-5-4-mini", "auray-serverless/model/openai-gpt-5-4", "auray-serverless/model/openai-gpt-5-5", "auray-serverless/model/anthropic-claude-haiku-4-5", "auray-serverless/model/anthropic-claude-sonnet-4-6", "auray-serverless/model/anthropic-claude-opus-4-7", "auray-serverless/model/anthropic-claude-opus-4-8", "auray-serverless/model/anthropic-claude-fable-5", "auray-serverless/model/google-gemma-4-31b", "auray-serverless/model/google-gemini-3-1-flash-lite", "auray-serverless/model/google-gemini-3-flash", "auray-serverless/model/google-gemini-3-5-flash", "auray-serverless/model/google-gemini-3-1-pro", "auray-serverless/model/xai-grok-4-3", "auray-serverless/model/minimax-m3", "auray-serverless/model/zai-glm-5-1". |
effort | string | no | — | NOT THE VALUE YOU SENT. The request takes a ladder — light, balanced, deep — and this reports what that mapped to on the chosen model, which differs between them: glm-5-3-flash and deepseek-v4-1-flash run low/medium/max, kimi-k3 and glm-5-3 run low/high/max, qwen3-8 runs low/medium/xhigh, auray-serverless/model/openai-gpt-5-nano, auray-serverless/model/openai-gpt-5-mini, auray-serverless/model/openai-gpt-5-4-nano, auray-serverless/model/openai-gpt-5-4-mini, auray-serverless/model/openai-gpt-5-4 and auray-serverless/model/openai-gpt-5-5 run low/medium/high, auray-serverless/model/google-gemma-4-31b and auray-serverless/model/zai-glm-5-1 run —/—/high. auray-serverless/model/anthropic-claude-haiku-4-5, auray-serverless/model/anthropic-claude-sonnet-4-6, auray-serverless/model/anthropic-claude-opus-4-7, auray-serverless/model/anthropic-claude-opus-4-8, auray-serverless/model/anthropic-claude-fable-5, auray-serverless/model/google-gemini-3-1-flash-lite, auray-serverless/model/google-gemini-3-flash, auray-serverless/model/google-gemini-3-5-flash, auray-serverless/model/google-gemini-3-1-pro, auray-serverless/model/xai-grok-4-3 and auray-serverless/model/minimax-m3 have no thinking level, and report null. A rung shown — sends none, and reports null. Sending one of these back as effort is 400 unknown_effort. One of "high", "low", "max", "medium", "xhigh", null. May be null. |
error | string | no | — | Present only when status is failed, and then it is the reason. |
waived | boolean | no | — | Present, and true, when a promotion paid for the turn and the wallet was not charged. |
settled_unknown | boolean | no | — | Present, and true, when the answer was delivered and its figures could not be read. |
Headers:
| Header | Description |
|---|---|
RateLimit-Limit | Burst capacity. |
RateLimit-Remaining | |
RateLimit-Reset | Seconds until refill. |
X-RateLimit-Limit | |
X-RateLimit-Remaining | |
X-RateLimit-Reset | Unix seconds. The X- spelling is what client libraries read. |
X-Request-Id | |
X-Auray-History-Trimmed | dropped=<n>, shortened=<n> when your plan's history budget left part of the thread out of this turn (history_trimmed in the body); absent when it sent all of it. |
#400 Bad Request
#401 Unauthorized
#402 Payment Required
#403 Forbidden
#404 Not Found
conversation_not_found — the conversation exists and belongs to somebody else. An id you have never used is NOT an error: chat mints the conversation at that id and reports conversation_created: true.
The error body: error, message, docs (the entry that explains it) and request_id.
#409 Conflict
#413 Payload Too Large
#429 Too Many Requests
#500 Internal Server Error
credit_reserve_failed or chat_start_failed — ours, not yours. Where a charge had already landed it is reversed; retry with a fresh idempotency key, since a reversal leaves the old one settled.
The error body: error, message, docs (the entry that explains it) and request_id.
#502 Bad Gateway
#503 Service Unavailable
#504 Gateway Timeout
model_timeout — the model took the request and stopped answering before its first word, or answered 408. The reserve is returned at once; retry with a new idempotency key.
The error body: error, message, docs (the entry that explains it) and request_id.
#Status codes
| Status | Meaning |
|---|---|
200 | The finished turn, or an SSE stream when stream:true. |
400 | Bad Request |
401 | Unauthorized |
402 | Payment Required |
403 | Forbidden |
404 | conversation_not_found — the conversation exists and belongs to somebody else. |
409 | Conflict |
413 | Payload Too Large |
429 | Too Many Requests |
500 | credit_reserve_failed or chat_start_failed — ours, not yours. |
502 | Bad Gateway |
503 | Service Unavailable |
504 | model_timeout — the model took the request and stopped answering before its first word, or answered 408. |
Every refusal names its code, and every code is explained at Errors.