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. 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.
#Authentication
Authorization: Bearer auray_sk_… — text:write, or the older chat:write.
#Request
#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 three 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. One of "glm-5-3-flash", "deepseek-v4-1-flash", "kimi-k3", "qwen", "kimi". |
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 three 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 slot that answered. 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". |
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 slot, which differs between them: glm-5-3-flash and deepseek-v4-1-flash run low/medium/max, kimi-k3 runs low/high/max. Sending one of these back as effort is 400 unknown_effort. One of "low", "medium", "high", "max". |
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.