Get started
Credits
One balance, shared by the desktop and the API. What spends it, what never does, and what comes back.
Credits are the only unit of spend. One balance per account, and both the desktop and the API draw on it.
Nothing here is charged per user, per project or per seat.
#What a credit does
You spend credits on generations. What one costs depends on what you asked for — a longer video
costs more than a shorter one, a denser mesh more than a rough one — and the cost is known before
the job runs, not after (or, for a model charged what the job cost, the most it can be — below):
the submit's answer carries credits_charged and credits_remaining before a single frame has rendered.
The price rule for an address is published with its contract. GET /v1/models/<address>/openapi.json
carries it as x-auray-price, and the ## Price section of /v1/models/<address>/llms.txt says
the same thing in a sentence. Neither needs a key.
#A quote that is a hold
Most catalogue models — every one priced by a band or by the token — are charged what the job cost.
Their x-auray-price says metered: true, and the quote is a hold: the submit takes the most the
request can cost (credits_quoted), the job settles to what it actually cost — never more than the
quote — and the rest comes back on its own.
A settled read of the job (GET /v1/jobs/{id} with settled: true) says what it finally cost in
credits_charged. In the app, Generate says up to for these models, and the job's receipt shows
what it holds until it settles, then what it cost. Every other address, ours included, says
metered: false: its quote is the charge.
#What never spends
- Reading anything. Listing models, reading a job, polling a status, listing webhook endpoints.
- A refused request. Anything caught before inference — wrong scope, malformed body, no credits, ceiling reached — costs nothing.
- A failed job. If it starts and then fails, the charge is reversed automatically.
- A cancelled job. What hadn't been made yet comes back — per product, in the settlement table. A catalogue model's job is the exception: it cannot be stopped once it starts. In the desktop its button says Stop watching, which is what it does — the job finishes and is charged for what it delivers.
- Downloading what you made. Asking for a job's signed asset URLs costs no credits; it counts
the files' size, in whole megabytes, against the
egressrate-limit bucket — a daily allowance that depends on your plan, reported in the answer asmegabytes_charged— which is why the download route is separate from the status poll. The bucket counts the signing, once per call, not the fetch: a signed link can be fetched again until it expires, and asking for the links again counts the size again.
#Monthly grants
| Plan | Per month | Credits |
|---|---|---|
| Free | $0 | 50 |
| Pro | $50 | 2,000 |
| Max | $150 | 7,000 |
| Ultimate | $500 | 25,000 |
Credits arrive every month. On a monthly plan they come with each payment; on a plan paid by the year you pay once, and they arrive every month of that year, from the day you pay. The numbers above are read from the same table the wallet itself reads; they cannot drift from what you are charged.
#What a month's credits make
An estimate, counted from the same price rules the submit charges by and from what each plan can run — images in the largest batch one job makes, a clip at the cheapest price, a song of a minute. Every plan that runs Photos, Video, Music or 3D runs the whole of it. Only those four are counted; every other model's address publishes its own price rule, as above. The same lines sit under every paid plan on Pricing.
| Plan | Example | Credits each | Estimate a month |
|---|---|---|---|
| Free | Batches of 4 images at 1K | 7 | About 7 |
| 5-second clips | 5 | About 10 | |
| 60-second songs | 17 | About 2 | |
| Textured 3D models | 13 | About 3 | |
| Pro | Batches of 4 images at 1K | 7 | About 285 |
| 5-second clips | 5 | About 400 | |
| 60-second songs | 17 | About 117 | |
| Textured 3D models | 13 | About 153 | |
| Max | Batches of 4 images at 1K | 7 | About 1,000 |
| 5-second clips | 5 | About 1,400 | |
| 60-second songs | 17 | About 411 | |
| Textured 3D models | 13 | About 538 | |
| Ultimate | Batches of 4 images at 1K | 7 | About 3,571 |
| 5-second clips | 5 | About 5,000 | |
| 60-second songs | 17 | About 1,470 | |
| Textured 3D models | 13 | About 1,923 |
#Passes
A pass is unlimited use of one model for a day or a week from payment, one job at a time, without spending credits. Where one is on sale, it is bought once in the app and never renews; buying it again adds its time after the one you hold. Each pass works where it says — the app, the API, MCP — and covers the jobs it lists; a job it does not cover is treated as if you held no pass. A pass for a catalogue model runs that model on any plan, Free included.
GET /v1/me lists them in passes: which model, until when, where each works (doors), what it
covers (covers, and covers_lines in words), whether a job of it is running, and whether your plan
runs its jobs (runs_on_plan). A job a pass paid for costs nothing, and two answers say so — the
submit's answer on queue.auray.run (credits_charged: 0) and GET /v1/jobs/{id}:
"coverage": { "by": "pass", "pass": "seedance-2-5" }The submit's answer on queue.auray.run also says when a pass of yours for the model did not pay for a
job it charged — {"by": null, "miss": {"pass": "video", "why": "door"}}, why being door (the
pass does not work the way the job was sent) or shape (it does not cover a job like this one). A run
on auray.run, and MCP's run_model, answers the job's output, which says neither: read
GET /v1/jobs/{id} with its request_id to see what paid.
A pass takes a job at a time. Through the API and MCP, a second job the pass covers, sent before the
first finishes, is refused 429 pass_busy with a Retry-After, and nothing is charged; in the app it
waits its turn. A job the pass does not cover is not held back by it.
Send X-Auray-Covered-Only: 1 with a submit to run it only if a pass or a promotion pays for it: a job
neither pays for is refused 409 not_covered, uncharged — or, where your plan runs the model only
through a pass, refused as the plan refuses it (model_not_in_plan, pass_does_not_cover).
#Ceilings
A key can carry its own monthly cap, separate from the balance. Once that key has spent it in a calendar month (UTC), it stops generating until the next month begins, and every other key keeps working.
Worth setting on anything you do not fully control.
#Watching the balance
curl https://api.auray.ai/v1/me \
-H "Authorization: Bearer $AURAY_API_KEY"import { createClient } from "@auray/client";
const auray = createClient(); // reads AURAY_API_KEY
console.log(await auray.platform.me());from auray_client import Client
auray = Client() # reads AURAY_API_KEY
print(auray.platform.me()){
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_account",
"arguments": {}
}
}const res = await fetch("https://api.auray.ai/v1/me", {
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/me",
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/me", 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/me");
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/me")
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/me"))
.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/me");
using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");{
"object": "account",
"id": "86e97633-…",
"plan": "ultimate",
"credits": { "balance": 38807, "monthly_allowance": 5000, "period": "2026-09" },
"key": { "id": "55oy2jFo5cmq", "scopes": ["jobs:read", "assets:read", "photo:write"], "credit_ceiling": 500, "credits_spent_period": 332 },
"request_id": "961dd3b6-59b4-4e26-bb8b-e4c28f1cbf96"
}curl https://api.auray.ai/v1/usage \
-H "Authorization: Bearer $AURAY_API_KEY"import { createClient } from "@auray/client";
const auray = createClient(); // reads AURAY_API_KEY
console.log(await auray.platform.usage());from auray_client import Client
auray = Client() # reads AURAY_API_KEY
print(auray.platform.usage()){
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_account",
"arguments": {
"usage": true
}
}
}const res = await fetch("https://api.auray.ai/v1/usage", {
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/usage",
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/usage", 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/usage");
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/usage")
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/usage"))
.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/usage");
using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");{
"object": "usage",
"period": "2026-09",
"credits": { "balance": 38807, "monthly_allowance": 5000, "spent_this_period": 572, "refunded_this_period": 24, "net_this_period": 548 },
"by_product": [
{ "product": "photo", "origin": "api", "spent": 364, "refunded": 0, "jobs": 91, "net": 364 },
{ "product": "photo", "origin": "app", "spent": 22, "refunded": 0, "jobs": 4, "net": 22 },
{ "product": "chat", "origin": "api", "spent": 30, "refunded": 24, "jobs": 3, "net": 6 }
],
"keys": [
{ "object": "api_key", "id": "…", "key_id": "55oy2jFo5cmq", "name": "…", "credit_ceiling": 500, "credits_spent_period": 332, "last_used_at": "…", "revoked_at": null }
],
"keys_truncated": false,
"request_id": "…"
}origin tells the desktop's spend (app) apart from your code's (api); both are the same
balance. keys lists your secret keys, most recently used first, up to a hundred of them;
keys_truncated is true when you hold more.
When the balance cannot cover a request, it is refused with insufficient_credits before anything
runs, and the body says how many were needed.
#Rate limits are a separate thing
Running out of credits and going too fast are different refusals, and they look different on purpose.
Five buckets — read, write, generate, interactive (chat and language-model calls) and
egress — refill continuously, and every response
reports where you stand in both RateLimit-* and X-RateLimit-* headers. Hitting one answers
429 with a Retry-After; it is about pace, not about money, and waiting fixes it.
See Errors for both families in full.