> For the complete documentation index, see [llms.txt](https://docs.auray.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.auray.ai/documentation/getting-started.md).

# Getting started

Everything the Auray desktop can generate, your code can generate too — through the same account, the same credits and the same library. A job you start from a script appears in your Photos, Music or 3D app exactly like one you started by clicking.

{% hint style="warning" %}
**`/v1` is switched off on this deployment.** `API_V1_ENABLED` is unset, so every route under `https://api.auray.ai/v1` answers `503 api_disabled`. That decision is made before your `Origin` header is inspected and before your key is parsed, so a perfect key and a truncated one get the same answer today. Read step five before you start debugging your header.
{% endhint %}

Keys are minted under your own login in the desktop, never by the API. There is deliberately no scope for managing keys — a key that can mint keys makes revocation a suggestion — so there is no way to bootstrap one from `/v1` itself.

## Five steps to a first call

{% stepper %}
{% step %}

### Create a key

**Settings → API Keys → New key.** The sheet asks for a name and nothing else, up to 80 characters. Name it after the machine or script that will hold it: that name is all you will have to go on when you are deciding which of five keys to revoke.

A free account may hold two live keys, a paid account twenty-five. Live means neither revoked nor expired, so a key that lapsed on its own stops occupying a slot.
{% endstep %}

{% step %}

### Copy it before you close the sheet

The plaintext is shown once. What reaches the database is `HMAC-SHA-256(pepper, "<key_id>.<secret>")` and nothing else — there is no column that could answer "what was it", so nobody can show it to you again, including us.

Closing the sheet asks you to confirm, because confirming destroys the only copy. If you lose one, revoke it and mint another; that path is cheap on purpose.
{% endstep %}

{% step %}

### Put it in the environment, not in your source

```bash
export AURA_API_KEY=auray_sk_Ky7Qn2vLp4Zx_…
```

The key is 71 characters and every field is a fixed width, so a paste that lost its tail is caught by the length check rather than by a mysterious 401.
{% endstep %}

{% step %}

### Make the call

`GET /v1/me` is the first call to make, and it is deliberately not a generate. It costs no credits, has no plan gate, no content filter and no wait. When it works, the only thing it proves is that the key works — which is exactly what you need to know two minutes after creating one.

{% tabs %}
{% tab title="cURL" %}

```bash
curl -s https://api.auray.ai/v1/me \
  -H "Authorization: Bearer $AURA_API_KEY"
```

Add `-i` if you want the status line and the headers alongside the body.
{% endtab %}

{% tab title="JavaScript" %}

```javascript
// Node, not a browser. Once the surface is on, a request carrying an
// Origin header is refused whatever the key is.
const res = await fetch("https://api.auray.ai/v1/me", {
  headers: { Authorization: `Bearer ${process.env.AURA_API_KEY}` },
});

// fetch does not throw on 4xx or 5xx. The body is the interesting part
// either way, so read it before you branch.
console.log(res.status, await res.json());
```

{% endtab %}

{% tab title="Python" %}

```python
import json, os, urllib.error, urllib.request

req = urllib.request.Request(
    "https://api.auray.ai/v1/me",
    headers={"Authorization": f"Bearer {os.environ['AURA_API_KEY']}"},
)

try:
    with urllib.request.urlopen(req) as res:
        print(res.status, json.load(res))
except urllib.error.HTTPError as err:
    # Today's 503 arrives here. The body carries the reason; print it.
    print(err.code, json.load(err))
```

{% endtab %}
{% endtabs %}
{% endstep %}

{% step %}

### Read what comes back

Today, on this deployment, every key gets this:

```json
{
  "error": "api_disabled",
  "message": "The API is not enabled on this deployment.",
  "docs": "https://docs.auray.ai/api-reference/errors#api_disabled",
  "request_id": "37d99de9-d507-4aca-bb5b-b4fa3af97b96"
}
```

No `Retry-After` rides on it and no rate-limit headers do either, because nothing was counted. `request_id` is repeated in the `X-Request-Id` header and is what to quote if you write in.

Branch on `error`, not on the status. Many HTTP clients retry a 503 automatically with backoff, and this one never becomes a 200 by being asked again — there is no key that gets past it.
{% endstep %}
{% endstepper %}

{% hint style="info" %}
The `docs` field on every error points at [`docs.auray.ai/api-reference/errors`](https://docs.auray.ai/api-reference/errors)`#<code>`, and all 95 codes have an entry there — what caused it, what it carries with it, and what to change. Match on `error` rather than on `message`: the sentence is written for a person and may be reworded, the code will not be.
{% endhint %}

## What the call will answer when the surface is on

```json
{
  "object": "account",
  "id": "fa7dda30-…",
  "plan": "free",
  "credits": { "balance": 0, "monthly_allowance": 0, "period": "2026-08" },
  "key": {
    "id": "Ky7Qn2vLp4Zx",
    "scopes": ["jobs:read", "assets:read"],
    "credit_ceiling": 0,
    "credits_spent_period": 0
  },
  "request_id": "…"
}
```

Those zeros are the honest numbers for a free account, not an error: the free plan grants no monthly credits, so the key's `credit_ceiling` is zero and the key is read-only.

`balance` distinguishes two things that look alike. `null` means no wallet row exists yet — nothing was ever granted. `0` means a row holding nothing, which is not the same as "you spent it all".

`credit_ceiling` is what this one key may spend in a calendar month, which is not the same as the wallet. The balance is shared across every key and the desktop; the ceiling bounds the damage a single leaked key can do.

## What a key is made of

`auray_sk_`, a twelve-character id, an underscore, a forty-three character secret and a six-character checksum. Seventy-one characters, every field a fixed width.

```
auray_sk_Ky7Qn2vLp4Zx_<43 characters of secret><6 of checksum>
```

The id is public. It appears in our logs, in the Settings list and in the `key.id` field of `/v1/me`, and it is the thing to quote in a support ticket. The secret is never stored anywhere, in any form that can be read back.

The checksum is a typo detector and not a security control. It is `sha256` of everything before it, base64url, truncated to six characters, and it covers only the public prefix, the id and the secret — no pepper, so anyone can compute it. That buys two things: a truncated paste is rejected in microseconds with no database round trip, and a third-party secret scanner can confirm a candidate offline before reporting it. The `auray_sk_` prefix exists for the same audience. A bare forty-three character blob is indistinguishable from a hash and nothing will ever flag it.

Length, prefix, the underscore, the base64url alphabet and the checksum are all checked before any database read. A flood of malformed keys never reaches the database.

## The header

Both of these are accepted:

```http
Authorization: Bearer auray_sk_Ky7Qn2vLp4Zx_…
Authorization: auray_sk_Ky7Qn2vLp4Zx_…
```

The value is trimmed, so trailing whitespace is harmless.

{% hint style="info" %}
Only the exact string `Bearer` — capital B, one space — is stripped. A lowercase `bearer` is not, so the whole header value is taken as the key, fails the length check, and comes back as `401 invalid_api_key` rather than as anything that mentions your header.
{% endhint %}

Four things can be wrong with a key, and they are four different codes because three of them are fixed three different ways.

| Error                   | Status | What to do                                                                          |
| ----------------------- | ------ | ----------------------------------------------------------------------------------- |
| `missing_authorization` | 401    | Send the `Authorization` header.                                                    |
| `invalid_api_key`       | 401    | Malformed, or no live key matches. Check it was copied whole — it is 71 characters. |
| `key_revoked`           | 401    | Somebody revoked it. A revoked key never comes back; mint a new one.                |
| `key_expired`           | 401    | Keys expire. Mint a new one.                                                        |

Unknown key and wrong secret collapse into the same `invalid_api_key` on purpose. Telling them apart would confirm which ids exist.

## Expiry is not optional

Every key has an expiry date. The choices are 30, 90, 180 and 365 days, and the database column caps anything at 366. There is no permanent option, because a credential that never expires is a liability with nothing on the other side of it.

The Settings sheet does not ask, so a key minted there expires in **90 days**. The row in Settings shows the date, and after it passes the key answers `401 key_expired` — set a reminder before that date rather than after.

Revocation is instant and there is nothing to invalidate: the key row is read on every single request, so there is no cache and no TTL to wait out.

## Scopes are decided when the key is minted

A key carries only the scopes it was created with, and a refusal names the one that was missing:

```json
{
  "error": "insufficient_scope",
  "message": "This key does not carry the photo:write scope.",
  "required": "photo:write",
  "scopes": ["jobs:read", "assets:read"],
  "docs": "https://docs.auray.ai/api-reference/errors#insufficient_scope",
  "request_id": "…"
}
```

**Scopes cannot be added to an existing key.** There is no edit; the fix for that 403 is a new key with the scope you need, and then revoking the old one. The eight that exist:

```
jobs:read  assets:read  webhooks:write
photo:write  music:write  video:write  threed:write  chat:write
```

Because the Settings sheet asks only for a name, a key minted there gets the two defaults — `jobs:read` and `assets:read`, the two that cannot spend anything. The `scopes` and `expires_in_days` fields live on the account route the sheet posts to, `POST https://app.auray.ai/api/account/keys`, which authenticates with your signed-in desktop session rather than with an API key. An unrecognised scope there is refused with `400 unknown_scope` rather than dropped: handing back a key that looks like it can do something it cannot would move the discovery to your first 403.

One asymmetry worth knowing before it bites: listing your webhook endpoints needs `jobs:read`, not `webhooks:write`.

## What does answer today

Two things on `api.auray.ai` are outside the switch, so you can wire up a client now and turn it on later.

**The front door needs no key.** Refusing an unauthenticated request there would be refusing the only question a newcomer can ask.

```bash
curl -s https://api.auray.ai/
```

```json
{
  "name": "Auray API",
  "version": "v1",
  "docs": "https://docs.auray.ai",
  "openapi": "https://api.auray.ai/v1/openapi.json",
  "authentication": "Authorization: Bearer auray_sk_…  — create a key in Settings → API Keys"
}
```

**The OpenAPI document is static.** `https://api.auray.ai/v1/openapi.json` is a file rather than a handler, so it answers before any key check and before the enabled check — a spec you need a key to read is a spec nobody can generate a client from. It is OpenAPI 3.1.0, its single server is `https://api.auray.ai/v1`, and it describes fourteen paths. Generated clients built from it will compile today and get a 503 at runtime until the surface opens.

## Three refusals to design around now

**No CORS, and no browser.** No `Access-Control-*` header of any kind is sent, so a browser could not read the response anyway, and a request that arrives carrying an `Origin` header is refused with `403 browser_origin_refused` whatever the key is — and told to rotate, because a secret key in front-end code is a key that has been published. Some HTTP clients set `Origin` by default outside a browser; if you get this from a backend, remove the header.

**A free key cannot generate.** The free plan's `generate` bucket has a capacity of zero, which is not the same as being out of tokens. It is a `403 plan_has_no_api_generate` rather than a `429`, so retrying never helps — a bucket of zero never refills into one. A free key can still list jobs and fetch assets from work you created in the desktop.

**A new account runs at a fifth of its limits for 24 hours.** It is the cheapest control against mass registration that costs a real developer nothing they will notice.

{% hint style="info" %}
Upgrading is **Settings → Subscription**, and Stripe is in test mode on this deployment: only test cards are accepted and nothing is charged.
{% endhint %}

## Next

The [API reference](https://docs.auray.ai/api-reference) covers the rest: the four rate-limit buckets and their per-plan numbers, the three ways to get a result, what each of the five products accepts, webhook signatures and retries, and a section on what this API does not do.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.auray.ai/documentation/getting-started.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
