# Create a character Source: https://docs.aurous-labs.com/api-reference/characters/create-character POST /v1/characters Upload existing ref images or synthesize 4 ref poses from a description. `POST /v1/characters` creates a character — a reusable identity asset you can attach to an image or video generation via `character_id` (see [Create an image](/api-reference/images/create-image) or [Create a video](/api-reference/videos/create-video)). There are two **mutually exclusive** flows: * **Upload flow**: pass `upload_ids` (1–6) collected from [`POST /v1/characters/uploads/init`](/api-reference/characters/upload-init). The platform moves the bytes to character storage and the response comes back with `status: ready` — immediately usable. * **Synthesize flow**: pass `generate: true` plus an `attributes` object describing who the character is. The platform dispatches a multi-image generation task that produces 4 ref poses (`portrait`, `front`, `side`, `back`). The response returns `status: synthesizing` while generation is in flight, or `status: reviewing` if synthesis completed before the response returned (typical for fast runs). Either way, poll [`GET /v1/characters/{id}`](/api-reference/characters/retrieve-character) until `status: reviewing`, then call [`POST /v1/characters/{id}/save`](/api-reference/characters/save-character) to mark it `ready`. Send `upload_ids` **or** `generate: true` — never both, never neither. Sending both or neither returns `400 parameter_invalid_combination`. ## Upload flow Use this when you already have ref images. Mint one `upload_id` per file via `POST /v1/characters/uploads/init`, PUT the bytes, then create the character. ```bash cURL theme={null} curl -X POST https://api.aurous-labs.com/v1/characters \ -H "X-Api-Key: $AUROUS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "name": "Aurora", "upload_ids": [ "upl_01HXMQ7Z3K8Y2VNABCDEFGHJKM", "upl_01HXMQ87RKZQA0YBYV1V47TPS6" ] }' ``` ```typescript Node.js theme={null} const character = await fetch("https://api.aurous-labs.com/v1/characters", { method: "POST", headers: { "X-Api-Key": process.env.AUROUS_API_KEY!, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID(), }, body: JSON.stringify({ name: "Aurora", upload_ids: ["upl_01HXMQ7Z3K8Y2VNABCDEFGHJKM", "upl_01HXMQ87RKZQA0YBYV1V47TPS6"], }), }).then((r) => r.json()); console.log(character.id, character.status); // char_..., "ready" ``` ```python Python theme={null} import os, uuid, requests r = requests.post( "https://api.aurous-labs.com/v1/characters", headers={ "X-Api-Key": os.environ["AUROUS_API_KEY"], "Idempotency-Key": str(uuid.uuid4()), }, json={ "name": "Aurora", "upload_ids": [ "upl_01HXMQ7Z3K8Y2VNABCDEFGHJKM", "upl_01HXMQ87RKZQA0YBYV1V47TPS6", ], }, ).json() print(r["id"], r["status"]) # char_..., "ready" ``` ## Synthesize flow Use this when you want the platform to generate the refs from a description. The `attributes` object is locked at v1.0 to 7 typed fields plus a free-text `additional_details` catch-all; on this synthesize flow every field drives generation (on the upload flow only `additional_details` is used, and only as a best-effort hint — see [Upload flow](#upload-flow)). Synthesize burns credits at create time (4 generation dispatches), so prefer to estimate cost via `POST /v1/images/estimate` on the equivalent prompt if you need a budget guardrail in your UI. ```bash cURL theme={null} curl -X POST https://api.aurous-labs.com/v1/characters \ -H "X-Api-Key: $AUROUS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "name": "Aurora", "generate": true, "attributes": { "gender": "female", "age": 28, "ethnicity": "northern european", "hair_color": "auburn", "hair_style": "long waves", "eye_color": "green", "body_type": "athletic", "additional_details": "scar across left cheekbone, freckles" } }' ``` ```typescript Node.js theme={null} const character = await fetch("https://api.aurous-labs.com/v1/characters", { method: "POST", headers: { "X-Api-Key": process.env.AUROUS_API_KEY!, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID(), }, body: JSON.stringify({ name: "Aurora", generate: true, attributes: { gender: "female", age: 28, ethnicity: "northern european", hair_color: "auburn", hair_style: "long waves", eye_color: "green", body_type: "athletic", additional_details: "scar across left cheekbone, freckles", }, }), }).then((r) => r.json()); console.log(character.id, character.status); // char_..., "synthesizing" (or "reviewing" if synth was already done) ``` ```python Python theme={null} import os, uuid, requests r = requests.post( "https://api.aurous-labs.com/v1/characters", headers={ "X-Api-Key": os.environ["AUROUS_API_KEY"], "Idempotency-Key": str(uuid.uuid4()), }, json={ "name": "Aurora", "generate": True, "attributes": { "gender": "female", "age": 28, "ethnicity": "northern european", "hair_color": "auburn", "hair_style": "long waves", "eye_color": "green", "body_type": "athletic", "additional_details": "scar across left cheekbone, freckles", }, }, ).json() print(r["id"], r["status"]) # char_..., "synthesizing" (or "reviewing" if synth was already done) ``` ## Status transitions | Flow | Initial | After processing | Customer action | | ------------------- | -------------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Upload | `ready` | — | Use immediately on `POST /v1/images` or `POST /v1/videos`. | | Synthesize | `synthesizing` (or `reviewing` if synth completed before the 201 returned) | `reviewing` | Poll `GET /v1/characters/{id}`. When `reviewing`, call `POST /v1/characters/{id}/save` to publish; or call `POST /v1/characters/{id}/resynthesize` to retry. | | Synthesize (failed) | `synthesizing` | `failed` | `error_message` populated. Call `POST /v1/characters/{id}/resynthesize` to retry (reuses the stored `attributes`); if it returns `422 uploads_expired`, delete via `DELETE /v1/characters/{id}` and recreate with fresh `upload_ids`. | ## Limits * **Rate limit**: bucket `characters_synthesize` — 15 requests/min sustained, 30 burst per team. Both flows ride this bucket since the synthesize discriminator is decided server-side after the request lands. * **Idempotency**: pass `Idempotency-Key` (any opaque value, 1–256 chars). Same key + same body within 24h replays the cached response with `Aurous-Idempotent-Replayed: true`. Same key + different body returns `409 idempotency_key_in_use`. See [Idempotency](/idempotency) for details. Always send `Idempotency-Key` on synthesize-flow create — it dispatches 4 paid generations, so a network retry without a key can double-charge. ## Errors | Code | HTTP | When | | ------------------------------- | ---- | ----------------------------------------------------------------- | | `parameter_invalid_combination` | 400 | Sent both `upload_ids` and `generate: true`, or neither. | | `upload_invalid` | 400 | One of the `upload_ids` is unknown, expired, or already consumed. | | `balance_too_low` | 402 | Synthesize-flow dispatch when team credits \< cost. | | `idempotency_key_in_use` | 409 | Same `Idempotency-Key` was used with a different body. | | `too_many_requests` | 429 | Burst > 30 or sustained > 15/min. | ## Common pitfalls * The synthesize flow returns 201 immediately, but the character is **not** usable until it transitions to `ready` — either automatically (upload flow) or via `POST /v1/characters/{id}/save` (synthesize flow). Calling `POST /v1/images` or `POST /v1/videos` with a `synthesizing` or `reviewing` `character_id` returns `400 character_not_ready`. * An upload flow with a single ref still works. On the upload flow your reference images define identity, so the 7 typed `attributes` (gender, age, etc.) are stored and echoed back but do **not** shape the generated refs — only `additional_details` is applied there, and only as a best-effort hint (your reference images are the primary signal). The synthesize flow (`generate: true`) is the opposite: all `attributes` drive generation, so don't send empty `attributes` there. * The `attributes` schema is locked for v1.0; new attributes go into `additional_details` until a v1.1 bump introduces them as typed fields. # Delete a character Source: https://docs.aurous-labs.com/api-reference/characters/delete-character DELETE /v1/characters/{id} Cancel-review a reviewing character or soft-delete any other state. `DELETE /v1/characters/{id}` removes a character. The branching is automatic, based on `status`: * **`reviewing`** → **hard-delete (cancel-review)**. The 4 generated refs are discarded, the row is removed entirely. Subsequent retrieval returns 404. Use this when synthesis produced refs you don't want to keep. * **Any other status** (`ready`, `failed`, `synthesizing`, `deleted`) → **soft-delete**. Sets `deleted_at`, transitions `status` to `deleted`. The row remains for ID stability (you can still `GET /v1/characters/{id}` for audit), but it is excluded from `GET /v1/characters` and rejected by `POST /v1/images` or `POST /v1/videos`. This endpoint never charges credits and does not refund credits already spent on synthesis. ## When to use * **Cancel-review**: the synthesize flow produced refs you reject; you want the character gone, not archived. * **Soft-delete `ready`**: retire a character you no longer plan to use. Past generations that already reference it on `GET /v1/images/{id}` are unaffected, but the character can no longer be attached to new requests. * **Soft-delete `failed`**: clean up a failed synthesis attempt you don't want to retry. To retry instead, call [`POST /v1/characters/{id}/resynthesize`](/api-reference/characters/resynthesize-character) — no delete needed. Only fall back to delete + recreate (with fresh `upload_ids`) if resynthesize returns `422 uploads_expired`. ## Examples ```bash cURL theme={null} curl -X DELETE https://api.aurous-labs.com/v1/characters/char_01HXMQ7Z3K8Y2VNABCDEFGHJKM \ -H "X-Api-Key: $AUROUS_API_KEY" ``` ```typescript Node.js theme={null} const result = await fetch( `https://api.aurous-labs.com/v1/characters/${id}`, { method: "DELETE", headers: { "X-Api-Key": process.env.AUROUS_API_KEY! } }, ).then((r) => r.json()); console.log(result.deleted); // true ``` ```python Python theme={null} import os, requests r = requests.delete( f"https://api.aurous-labs.com/v1/characters/{character_id}", headers={"X-Api-Key": os.environ["AUROUS_API_KEY"]}, ).json() print(r["deleted"]) # True ``` ## Limits * **Rate limit**: bucket `characters_post` — 30 requests/min sustained, 60 burst per team. ## Errors | Code | HTTP | When | | -------------------- | ---- | ---------------------------------------------------------------------------------------- | | `resource_not_found` | 404 | Unknown ID or cross-team. The 404 is intentional — cross-team existence is never leaked. | ## Common pitfalls * A `reviewing` character is **hard-deleted** — the row vanishes. Subsequent `GET /v1/characters/{id}` returns 404. If you cared about the refs, call `POST /v1/characters/{id}/save` first. * Soft-deleting a `ready` character does **not** affect generations you already produced — their `output_urls` continue to work and the character ID still appears on past `GET /v1/images/{id}` responses. * Calling DELETE a second time on the same character returns 404, since soft-deleted rows are filtered out for IDOR safety. Treat the first 200 as the only success signal you need. # List characters Source: https://docs.aurous-labs.com/api-reference/characters/list-characters GET /v1/characters Page through your team's characters, newest first. `GET /v1/characters` returns a cursor-paginated list of your team's characters, ordered by `created_at` descending. Soft-deleted characters are excluded. ## When to use * Powering a "pick a character" UI in your own product. * Auditing which characters are still in `synthesizing` / `reviewing` and need attention. * Bulk-syncing the catalog into your local database. ## Pagination Pass `limit` (1–100, default 20) and `starting_after` (the `next_cursor` value from the previous response, which is the `id` of the last character returned). Stop when `next_cursor` is `null`. The cursor convention is consistent across every paginated V1 endpoint — `starting_after` to advance, `next_cursor` to receive the next anchor. ## Examples ```bash cURL theme={null} # First page curl "https://api.aurous-labs.com/v1/characters?limit=20" \ -H "X-Api-Key: $AUROUS_API_KEY" # Subsequent pages — `starting_after` is the `id` of the last character # returned (also surfaced as `next_cursor` on the previous response). curl "https://api.aurous-labs.com/v1/characters?limit=20&starting_after=char_01HXMQ7Z3K8Y2VNABCDEFGHJKM" \ -H "X-Api-Key: $AUROUS_API_KEY" ``` ```typescript Node.js theme={null} async function* paginate() { let cursor: string | null = null; do { const url = new URL("https://api.aurous-labs.com/v1/characters"); url.searchParams.set("limit", "20"); if (cursor) url.searchParams.set("starting_after", cursor); const page = await fetch(url, { headers: { "X-Api-Key": process.env.AUROUS_API_KEY! }, }).then((r) => r.json()); for (const c of page.data) yield c; cursor = page.next_cursor; } while (cursor); } for await (const c of paginate()) console.log(c.id, c.name, c.status); ``` ```python Python theme={null} import os, requests cursor = None while True: params = {"limit": 20} if cursor: params["starting_after"] = cursor page = requests.get( "https://api.aurous-labs.com/v1/characters", headers={"X-Api-Key": os.environ["AUROUS_API_KEY"]}, params=params, ).json() for c in page["data"]: print(c["id"], c["name"], c["status"]) cursor = page["next_cursor"] if not cursor: break ``` ## Limits * **Rate limit**: bucket `characters_get` — 120 requests/min sustained, 240 burst per team. ## Common pitfalls * The list does **not** include `deleted` characters. To enumerate everything for an audit, you need to track deletions out-of-band — deletes are soft and the character stays for IDs to remain valid forever, but the list endpoint hides them. * `next_cursor` is the last returned character's `id` (the opaque `char_`). Pass it back as `starting_after` on the next request — don't synthesize cursors yourself. * The fields returned per character are the same as `GET /v1/characters/{id}` (no shrunk "summary" shape); pages are bounded by `limit`, not by payload size. # Regenerate a single ref pose Source: https://docs.aurous-labs.com/api-reference/characters/regenerate-ref POST /v1/characters/{id}/refs/regenerate Re-run synthesis for one pose without re-doing the other three. `POST /v1/characters/{id}/refs/regenerate` re-runs the synthesize pipeline for **one** pose, leaving the other refs intact. Use it when three of the four synthesized refs look right but the fourth needs another try. The endpoint returns **200** with the updated character resource (Stripe pattern — the resource exists and this is a state transition, not a new-resource creation). The character transitions to `status: synthesizing` while the new ref mints; poll [`GET /v1/characters/{id}`](/api-reference/characters/retrieve-character) until `status` returns to `reviewing` (then call `POST /v1/characters/{id}/save`) or `ready`. This endpoint **costs credits** — one generation per call. Estimate cost via `POST /v1/images/estimate` on the equivalent prompt if you need a budget guardrail in your UI. ## When to use * The synthesize flow produced three good refs and one bad one; you want to fix only the bad one. * A `failed` regen needs another attempt without touching other poses. If you want to regenerate **all** four poses, call [`POST /v1/characters/{id}/resynthesize`](/api-reference/characters/resynthesize-character) instead. ## Body | Field | Required | Description | | ----------------- | -------: | ---------------------------------------------------------------------------------------------- | | `pose` | yes | One of `portrait`, `front`, `side`, `back`, `other`. | | `prompt_override` | no | Forward-compat slot. Currently a no-op — the orchestrator does not honor it yet. Safe to omit. | ## Examples ```bash cURL theme={null} curl -X POST https://api.aurous-labs.com/v1/characters/char_01HXMQ7Z3K8Y2VNABCDEFGHJKM/refs/regenerate \ -H "X-Api-Key: $AUROUS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"pose": "front"}' ``` ```typescript Node.js theme={null} const character = await fetch( `https://api.aurous-labs.com/v1/characters/${id}/refs/regenerate`, { method: "POST", headers: { "X-Api-Key": process.env.AUROUS_API_KEY!, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID(), }, body: JSON.stringify({ pose: "front" }), }, ).then((r) => r.json()); console.log(character.status); // "synthesizing" ``` ```python Python theme={null} import os, uuid, requests r = requests.post( f"https://api.aurous-labs.com/v1/characters/{character_id}/refs/regenerate", headers={ "X-Api-Key": os.environ["AUROUS_API_KEY"], "Idempotency-Key": str(uuid.uuid4()), }, json={"pose": "front"}, ).json() print(r["status"]) # "synthesizing" ``` ## Limits * **Rate limit**: bucket `characters_synthesize` — 15 requests/min sustained, 30 burst per team. Shared with synthesize-flow create and resynthesize. * **Idempotency**: pass `Idempotency-Key` (any opaque value, 1–256 chars). Same key + same body within 24h replays the cached response. Same key + different body returns `409 idempotency_key_in_use`. See [Idempotency](/idempotency). Always pass `Idempotency-Key` — this endpoint dispatches a paid generation, so a network retry without a key can double-charge. ## Errors | Code | HTTP | When | | ------------------------ | ---- | ------------------------------------------------------------------------------------------- | | `invalid_format` | 400 | `pose` is not one of the canonical 5. | | `invalid_request` | 400 | Character is in a non-regen state. | | `balance_too_low` | 402 | Team credits \< the regen cost. | | `resource_not_found` | 404 | Unknown ID, soft-deleted character, or cross-team. | | `idempotency_key_in_use` | 409 | Same `Idempotency-Key` used with a different body, or previously used on a different route. | | `too_many_requests` | 429 | Burst > 30 or sustained > 15/min. | ## Common pitfalls * The pose enum is locked at v1.0 to `portrait`, `front`, `side`, `back`, `other`. Anything else is `400 invalid_format`. * After regen, the character is `synthesizing` again. Polling resumes the same way as for fresh synthesis. * Regenerating from `ready` is allowed and moves the character back to `synthesizing` — existing generations that already reference the old ref are unaffected; only future requests pick up the new pose. # Resynthesize a character Source: https://docs.aurous-labs.com/api-reference/characters/resynthesize-character POST /v1/characters/{id}/resynthesize Re-run the synthesize pipeline to produce a fresh set of refs. `POST /v1/characters/{id}/resynthesize` re-runs the synthesize pipeline against the character's existing `attributes` to produce a fresh set of ref poses. The endpoint accepts characters in `reviewing` or `failed` state. The call is synchronous: the response returns the updated character with `status: reviewing` and its fresh refs. Review the refs, then call [`POST /v1/characters/{id}/save`](/api-reference/characters/save-character) to move the character to `ready`. The endpoint returns **200** with the updated character resource (Stripe pattern — the resource exists and this is a state transition). This endpoint **costs credits** — same cost as the original synthesize-flow create (4 generations). Estimate via `POST /v1/images/estimate` on the equivalent prompt if you need a budget guardrail. ## Body | Field | Required | Description | | ----------- | -------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `force_all` | no | When `true`, regenerate every pose even if some are already populated. Default `false` — the behaviour is to replace the full ref set anyway, so this flag is mainly forward-compat for partial-resync semantics. | ## Examples ```bash cURL theme={null} curl -X POST https://api.aurous-labs.com/v1/characters/char_01HXMQ7Z3K8Y2VNABCDEFGHJKM/resynthesize \ -H "X-Api-Key: $AUROUS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{}' ``` ```typescript Node.js theme={null} const character = await fetch( `https://api.aurous-labs.com/v1/characters/${id}/resynthesize`, { method: "POST", headers: { "X-Api-Key": process.env.AUROUS_API_KEY!, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID(), }, body: JSON.stringify({}), }, ).then((r) => r.json()); console.log(character.status); // "synthesizing" ``` ```python Python theme={null} import os, uuid, requests r = requests.post( f"https://api.aurous-labs.com/v1/characters/{character_id}/resynthesize", headers={ "X-Api-Key": os.environ["AUROUS_API_KEY"], "Idempotency-Key": str(uuid.uuid4()), }, json={}, ).json() print(r["status"]) # "synthesizing" ``` ## Limits * **Rate limit**: bucket `characters_synthesize` — 15 requests/min sustained, 30 burst per team. Shared with synthesize-flow create and `regenerate-ref`. * **Idempotency**: pass `Idempotency-Key` (any opaque value, 1–256 chars). Same key + same body within 24h replays the cached response. Same key + different body returns `409 idempotency_key_in_use`. See [Idempotency](/idempotency). Always pass `Idempotency-Key` — this endpoint dispatches 4 paid generations, so a network retry without a key can multiply your bill. ## Errors | Code | HTTP | When | | ------------------------ | ---- | ---------------------------------------------------------------------------------------------------- | | `invalid_request` | 400 | Character is not in `reviewing` or `failed` state. | | `balance_too_low` | 402 | Team credits \< the resynthesize cost. | | `resource_not_found` | 404 | Unknown ID, soft-deleted character, or cross-team. | | `idempotency_key_in_use` | 409 | Same `Idempotency-Key` used with a different body, or previously used on a different route. | | `uploads_expired` | 422 | Character was created from uploaded photos and the originals have since expired. No credits charged. | | `too_many_requests` | 429 | Burst > 30 or sustained > 15/min. | ## Common pitfalls * `resynthesize` is the most expensive character endpoint — always pass `Idempotency-Key` to avoid double-charges on retry. * Resynthesize works on both `reviewing` **and** `failed` characters — it's the primary way to retry a failed synthesis, reusing the character's stored `attributes`. If the character's original uploaded photos have since expired, the call returns `422 uploads_expired` instead (this endpoint doesn't accept new photos on resynthesize); in that case, delete via `DELETE /v1/characters/{id}` and recreate via `POST /v1/characters` with fresh `upload_ids`. * Resynthesize uses the **stored** `attributes`. If you want to change identity (e.g. different `hair_color`), call [`PATCH /v1/characters/{id}`](/api-reference/characters/update-character) with the new attributes first, then resynthesize. # Retrieve a character Source: https://docs.aurous-labs.com/api-reference/characters/retrieve-character GET /v1/characters/{id} Fetch a character by ID. Use this to poll the synthesize flow. `GET /v1/characters/{id}` returns a single character. Use it to: * Poll a synthesize-flow character until `status` transitions from `synthesizing` to `reviewing` (or `failed`). * Read the `refs[]` array for proxy URLs you can render in your UI. * Inspect `attributes` for the typed identity fields the platform stored. ## Status lifecycle | Status | Meaning | Customer action | | -------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `synthesizing` | Synthesize-flow generation running. | Poll. Typical completion 30–90 seconds. | | `reviewing` | Synthesize completed; refs ready for human approval. | Inspect `refs`. Call `POST /v1/characters/{id}/save` to publish, or `POST /v1/characters/{id}/resynthesize` to retry. | | `ready` | Usable as `character_id` on `POST /v1/images` or `POST /v1/videos`. | Reference on generation. | | `failed` | Synthesize dispatch failed. `error_message` populated. | Call `POST /v1/characters/{id}/resynthesize` to retry — it reuses the stored `attributes`. Returns `422 uploads_expired` if the original uploaded photos are gone; delete via `DELETE /v1/characters/{id}` and recreate with fresh `upload_ids` in that case. | Soft-deleted characters return `404 resource_not_found` on this endpoint — the V1 surface treats them as gone for IDOR safety. If you need to confirm a soft-delete landed, the 404 itself is the confirmation. ## Examples ```bash cURL theme={null} curl https://api.aurous-labs.com/v1/characters/char_01HXMQ7Z3K8Y2VNABCDEFGHJKM \ -H "X-Api-Key: $AUROUS_API_KEY" ``` ```typescript Node.js theme={null} async function pollUntilReady(id: string): Promise { for (let i = 0; i < 60; i++) { const c = await fetch( `https://api.aurous-labs.com/v1/characters/${id}`, { headers: { "X-Api-Key": process.env.AUROUS_API_KEY! } }, ).then((r) => r.json()); if (c.status === "reviewing" || c.status === "ready") return c; if (c.status === "failed") throw new Error(c.error_message); await new Promise((r) => setTimeout(r, 2000)); } throw new Error("timed out waiting for character"); } ``` ```python Python theme={null} import os, time, requests def poll_until_ready(id: str): for _ in range(60): c = requests.get( f"https://api.aurous-labs.com/v1/characters/{id}", headers={"X-Api-Key": os.environ["AUROUS_API_KEY"]}, ).json() if c["status"] in ("reviewing", "ready"): return c if c["status"] == "failed": raise RuntimeError(c["error_message"]) time.sleep(2) raise TimeoutError("character did not finish in time") ``` ## Limits * **Rate limit**: bucket `characters_get` — 120 requests/min sustained, 240 burst per team. ## Errors | Code | HTTP | When | | -------------------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `resource_not_found` | 404 | Unknown ID, soft-deleted character, or character belongs to a different team. The 404 is intentional — cross-team existence is never leaked. | ## Common pitfalls * `refs[].url` is a **proxy URL** — do not assume it points at any specific storage host. Treat it as opaque and re-fetch as needed; URLs are valid for \~24h. * Polling at 2-second intervals is plenty for synthesize. Tighter polling only burns rate-limit budget. * Soft-deleted characters return 404 on this endpoint — if you want to keep an audit trail of past characters, mirror them in your own database before calling DELETE. # Save a reviewing character Source: https://docs.aurous-labs.com/api-reference/characters/save-character POST /v1/characters/{id}/save Promote a synthesize-flow character from reviewing to ready. `POST /v1/characters/{id}/save` is the publication step of the synthesize flow. It moves a `reviewing` character to `ready`, cleans up temporary upload bytes from the synthesis pipeline, and makes the character referenceable via `character_id` on [`POST /v1/images`](/api-reference/images/create-image) or [`POST /v1/videos`](/api-reference/videos/create-video). This endpoint costs **no** credits and dispatches no inference — it is purely a state transition. ## When to use * The synthesize flow finished (`status: reviewing`) and you've inspected `refs[]` — the four poses look right and you're ready to use the character. * You want to convert an idempotent intent ("create the character") into a usable resource without doing extra work. If the refs **don't** look right, call [`POST /v1/characters/{id}/refs/regenerate`](/api-reference/characters/regenerate-ref) (one pose) or [`POST /v1/characters/{id}/resynthesize`](/api-reference/characters/resynthesize-character) (all four) instead. To discard, call [`DELETE /v1/characters/{id}`](/api-reference/characters/delete-character). ## Examples ```bash cURL theme={null} curl -X POST https://api.aurous-labs.com/v1/characters/char_01HXMQ7Z3K8Y2VNABCDEFGHJKM/save \ -H "X-Api-Key: $AUROUS_API_KEY" ``` ```typescript Node.js theme={null} const character = await fetch( `https://api.aurous-labs.com/v1/characters/${id}/save`, { method: "POST", headers: { "X-Api-Key": process.env.AUROUS_API_KEY! } }, ).then((r) => r.json()); console.log(character.status); // "ready" ``` ```python Python theme={null} import os, requests r = requests.post( f"https://api.aurous-labs.com/v1/characters/{character_id}/save", headers={"X-Api-Key": os.environ["AUROUS_API_KEY"]}, ).json() print(r["status"]) # "ready" ``` ## Limits * **Rate limit**: bucket `characters_post` — 30 requests/min sustained, 60 burst per team. ## Errors | Code | HTTP | When | | -------------------- | ---- | -------------------------------------------------------------------- | | `invalid_request` | 400 | Character is in a non-savable state (e.g. `synthesizing`, `failed`). | | `resource_not_found` | 404 | Unknown ID, soft-deleted character, or cross-team. | ## Common pitfalls * Save is idempotent on already-`ready` characters — calling it twice returns 200 both times with `status: ready` unchanged. Use this property to retry confidently. * A `synthesizing` character returns `400` — don't call save until you've polled and seen `status: reviewing`. * After save, the character cannot be returned to `reviewing`. To retry refs, call `regenerate-ref` (single pose) or `resynthesize` (all four). # Update a character Source: https://docs.aurous-labs.com/api-reference/characters/update-character PATCH /v1/characters/{id} Edit a character's name and attributes. Refs are immutable. `PATCH /v1/characters/{id}` updates the **mutable** fields of a character: `name` and `attributes`. Send only the fields you want to change. Omitted fields are preserved. The reference images themselves (`refs[]`) are **immutable** by design — the cover image is the first ref in synthesize order or the first uploaded ref, and is not customer-controllable in v1.0. To change the visual identity, regenerate a single pose via [`POST /v1/characters/{id}/refs/regenerate`](/api-reference/characters/regenerate-ref) or rerun the whole synthesis via [`POST /v1/characters/{id}/resynthesize`](/api-reference/characters/resynthesize-character). ## When to use * Renaming a character. * Updating `attributes.additional_details` to add notes you want to surface in your UI. This endpoint does **not** trigger any inference or charge credits — it is purely metadata. ## Examples ```bash cURL theme={null} curl -X PATCH https://api.aurous-labs.com/v1/characters/char_01HXMQ7Z3K8Y2VNABCDEFGHJKM \ -H "X-Api-Key: $AUROUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Aurora (final)"}' ``` ```typescript Node.js theme={null} const updated = await fetch( `https://api.aurous-labs.com/v1/characters/${id}`, { method: "PATCH", headers: { "X-Api-Key": process.env.AUROUS_API_KEY!, "Content-Type": "application/json", }, body: JSON.stringify({ name: "Aurora (final)" }), }, ).then((r) => r.json()); ``` ```python Python theme={null} import os, requests r = requests.patch( f"https://api.aurous-labs.com/v1/characters/{character_id}", headers={"X-Api-Key": os.environ["AUROUS_API_KEY"]}, json={"name": "Aurora (final)"}, ).json() ``` ## Limits * **Rate limit**: bucket `characters_post` — 30 requests/min sustained, 60 burst per team. ## Errors | Code | HTTP | When | | -------------------- | ---- | --------------------------------------------------------------------------------------------------- | | `resource_not_found` | 404 | Unknown ID, soft-deleted character, or cross-team. `error.param` is `"character_id"`. | | `invalid_request` | 400 | Body validation failure (e.g. `name` over the length cap). `error.param` names the offending field. | ## Common pitfalls * The platform applies a **strict whitelist** server-side. Sending fields outside the v1.0 mutable set (`name`, `attributes`) — for example `refs`, `id`, `team_id`, `status`, or `aurous_version` — returns `400 invalid_request` with `error.param` naming the first rejected field and a message listing every offending key. This is intentional: a silent-ignore policy would let a buggy client believe a non-existent field had taken effect. If you need a field to be mutable, send a feature request — new mutable fields land in v1.1 schema bumps. * PATCH on a `deleted` character returns 404 `resource_not_found` — revive flows are not part of v1.0. * `attributes` is not deep-merged; sending `attributes: { eye_color: "blue" }` replaces the whole object. Re-send all fields you want preserved. # Classify an uploaded ref Source: https://docs.aurous-labs.com/api-reference/characters/upload-classify POST /v1/characters/uploads/classify Detect the pose of an image you uploaded via /v1/characters/uploads/init. `POST /v1/characters/uploads/classify` runs pose detection on an uploaded reference image and returns a label (`portrait`, `front`, `side`, `back`, or `other`) plus a confidence score. Call it after the PUT to `upload_url` finishes and before [`POST /v1/characters`](/api-reference/characters/create-character) if you want to label or de-duplicate refs in your own UI. This endpoint is **optional**. The platform does not require classification before consuming an `upload_id` — refs without a labeled pose default to `other`. ## When to use * You're building an upload UI where customers can review which pose was detected before committing. * You want to reject a duplicate `front` upload before the user submits the form. * You want to display the detected pose alongside the uploaded thumbnail. If you don't need any of that, skip classify and call `POST /v1/characters` directly with `upload_ids`. ## Example ```bash cURL theme={null} curl -X POST https://api.aurous-labs.com/v1/characters/uploads/classify \ -H "X-Api-Key: $AUROUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"upload_id": "upl_01HXMQ7Z3K8Y2VNABCDEFGHJKM"}' ``` ```typescript Node.js theme={null} const result = await fetch( "https://api.aurous-labs.com/v1/characters/uploads/classify", { method: "POST", headers: { "X-Api-Key": process.env.AUROUS_API_KEY!, "Content-Type": "application/json", }, body: JSON.stringify({ upload_id: "upl_01HXMQ7Z3K8Y2VNABCDEFGHJKM" }), }, ).then((r) => r.json()); console.log(result.pose, result.confidence); // e.g. "front", 0.94 ``` ```python Python theme={null} import os, requests r = requests.post( "https://api.aurous-labs.com/v1/characters/uploads/classify", headers={"X-Api-Key": os.environ["AUROUS_API_KEY"]}, json={"upload_id": "upl_01HXMQ7Z3K8Y2VNABCDEFGHJKM"}, ).json() print(r["pose"], r["confidence"]) ``` ## Limits * **Rate limit**: bucket `characters_post` — 30 requests/min sustained, 60 burst per team. * **Idempotent**: yes — classify is a pure read of the uploaded bytes; calling twice returns the same answer (no duplicate cost). ## Errors | Code | HTTP | When | | -------------------- | ---- | ------------------------------------------------------------------------------ | | `upload_invalid` | 400 | `upload_id` is unknown, expired, or already consumed by `POST /v1/characters`. | | `resource_not_found` | 404 | `upload_id` belongs to a different team. | ## Common pitfalls * Classify returns `other` for poses that don't match the canon — don't surface "unknown" as an error in your UI; treat it as an acceptable label. * The confidence score is informative, not a gate. A low-confidence `front` is still a valid `front` for the synthesize layer. * Once `POST /v1/characters` consumes the upload, calling classify on the same `upload_id` returns 400 `upload_invalid`. Cache the classify result client-side if you need it later. # Mint a character upload URL Source: https://docs.aurous-labs.com/api-reference/characters/upload-init POST /v1/characters/uploads/init Get a signed PUT URL for one character reference image. `POST /v1/characters/uploads/init` is the first step of the **upload flow** for creating a character. It returns a short-lived signed PUT URL plus an opaque `upload_id`. Upload your image bytes to that URL, then pass the `upload_id` to [`POST /v1/characters`](/api-reference/characters/create-character) (or call [`POST /v1/characters/uploads/classify`](/api-reference/characters/upload-classify) first to detect the pose). You typically call this endpoint **once per reference image**. A character supports 1–6 refs. ## When to use * You already have ref images on disk or in your own storage and want to attach them to a new character without round-tripping bytes through your API. * You want to capture pose metadata before committing the character (use `upload-classify`). If instead you want the platform to generate refs from a text description, skip the upload flow and pass `generate: true` + `attributes` to `POST /v1/characters` (the **synthesize flow**). ## Lifecycle 1. `POST /v1/characters/uploads/init` → `{ upload_id, upload_url, expires_at }`. 2. `PUT ` with the image bytes (any well-formed PUT, no extra headers required). 3. Optional: `POST /v1/characters/uploads/classify` with the same `upload_id` to detect the pose. 4. `POST /v1/characters` with `{ upload_ids: [upl_..., ...] }` to consume the uploads and create the character. ## Example ```bash cURL theme={null} # 1. Mint the URL curl -X POST https://api.aurous-labs.com/v1/characters/uploads/init \ -H "X-Api-Key: $AUROUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "filename": "front-portrait.jpg", "extension": "jpg", "content_type": "image/jpeg" }' # Response: { # "upload_id": "upl_01H...", # "upload_url": "https://...", # "upload_headers": { "Content-Type": "image/jpeg" }, # "expires_at": "..." # } # 2. Upload the bytes (set every header from `upload_headers` on the PUT) curl -X PUT "$UPLOAD_URL" \ -H "Content-Type: image/jpeg" \ --data-binary @ref-portrait.jpg ``` ```typescript Node.js theme={null} const init = await fetch("https://api.aurous-labs.com/v1/characters/uploads/init", { method: "POST", headers: { "X-Api-Key": process.env.AUROUS_API_KEY!, "Content-Type": "application/json", }, body: JSON.stringify({ filename: "front-portrait.jpg", extension: "jpg", content_type: "image/jpeg", }), }).then((r) => r.json()); // Echo every header from `upload_headers` on the PUT so the storage // layer accepts the upload without rewriting the Content-Type. await fetch(init.upload_url, { method: "PUT", headers: init.upload_headers, body: fs.readFileSync("./ref-portrait.jpg"), }); console.log(init.upload_id); // pass to POST /v1/characters ``` ```python Python theme={null} import os, requests init = requests.post( "https://api.aurous-labs.com/v1/characters/uploads/init", headers={"X-Api-Key": os.environ["AUROUS_API_KEY"]}, json={ "filename": "front-portrait.jpg", "extension": "jpg", "content_type": "image/jpeg", }, ).json() # Echo every header from `upload_headers` on the PUT so the storage # layer accepts the upload without rewriting the Content-Type. with open("ref-portrait.jpg", "rb") as f: requests.put(init["upload_url"], data=f.read(), headers=init["upload_headers"]) print(init["upload_id"]) ``` ## Limits * **Rate limit**: bucket `characters_post` — 30 requests/min sustained, 60 burst per team. * **Upload URL TTL**: 15 minutes. Mint a fresh URL if the PUT lands later. * **Upload ticket TTL**: 24 hours. After that the bytes are evicted and `upload_id` is no longer accepted on `POST /v1/characters`. ## Common pitfalls * The `upload_url` is **not** authenticated — the signature is in the query string. Do not add `X-Api-Key` to the PUT. * One ticket = one image. Mint multiple tickets in parallel for multi-ref characters. * Browsers may need a CORS-aware proxy in front of the PUT — the V1 surface targets server-side integrators and does not advertise CORS headers on the upload host. # Cancel an in-flight chat completion Source: https://docs.aurous-labs.com/api-reference/chat/cancel-an-in-flight-chat-completion /api-reference/openapi.json post /v1/chat/completions/{id}/cancel Aborts a streaming chat completion that is still in progress. Commits actuals up to the abort point against the credit hold and releases the remainder. Returns the final-state `ChatCompletionResponse`. State machine: - `pending` — hold placed, dispatch not yet started → released, status `cancelled_by_request`. - `processing` AND in-flight on this instance → upstream aborted, partial actuals committed, `cancelled_by_request`. - `processing` on a previous deploy → 409 `chat_cancel_target_not_cancellable` (no cross-instance signalling in v1.0; poll `GET /v1/chat/completions/:id` for the final state). - already terminal → 409 `chat_cancel_target_already_terminal`. - id unknown or cross-team → 404 `chat_cancel_target_not_found`. # Chat cancellation Source: https://docs.aurous-labs.com/api-reference/chat/cancellation Cancel an in-flight chat completion mid-stream via POST /v1/chat/completions/{id}/cancel. Partial-token billing semantics. `POST /v1/chat/completions/{id}/cancel` requests cancellation of an in-flight chat completion. Useful when the user navigates away from a generating answer, when an upstream timeout fires, or when a downstream system signals "stop." This is a request, not a guarantee — if the completion has already reached a terminal state (`completed`, `failed`, `cancelled`), the call returns `409 chat_cancel_target_already_terminal`. If the completion is past the point of no return (the model has already emitted the full response, but the platform hasn't recorded the final state yet), best-effort cancellation may still bill the full output. ## Request ```bash theme={null} curl -X POST https://api.aurous-labs.com/v1/chat/completions/cmp_01HXMQ7Z3K8Y2VNABCDEFGHJKM/cancel \ -H "Authorization: Bearer $AUROUS_API_KEY" ``` No body. Returns `200` with the updated completion row (now in `status: cancelled`) on success. ```json theme={null} { "object": "chat.completion", "id": "cmp_01HXMQ7Z3K8Y2VNABCDEFGHJKM", "status": "cancelled", "model": "aurous-grow-2.0-pro", "created_at": "2026-05-20T10:00:00Z", "cancelled_at": "2026-05-20T10:00:08Z", "choices": [ { "index": 0, "finish_reason": "cancelled", "message": { "role": "assistant", "content": "The first part of the response that was generated before cancel..." } } ], "usage": { "prompt_tokens": 47, "completion_tokens": 23, "total_tokens": 70, "credits_charged": 0.0167, "breakdown": { "model": "aurous-grow-2.0-pro", "input": { "text": 47, "cached_input_tokens": 0 }, "output": { "text": 23, "reasoning": 0 } } } } ``` ## Billing semantics A cancelled completion bills **only the tokens already committed** to output at the moment cancellation took effect. The credit hold reserved for the maximum potential cost is committed for `credits_charged` and the rest is **released back to your available balance**. The math is the same as a `succeeded` completion — just with fewer `completion_tokens`. Cancellation never bills zero (the prompt was already processed and is billed at the input rate); it never bills the full `max_tokens` worth of output either. For streamed completions, the platform commits whichever tokens were already sent to the SSE client before the cancel signal reached the worker. Race conditions can cause the final committed-token count to differ from what the client received by 1-2 tokens; we err on the side of NOT over-billing. ## Effect on streamed clients If you call `cancel` on an active streamed completion: * The SSE stream emits any in-flight frames already queued * A final `data: { "object": "chat.completion.chunk", "choices": [{ "finish_reason": "cancelled" }] }` frame is emitted * The `data: [DONE]\n\n` terminator follows * The TCP connection closes The streamed client should treat `finish_reason: "cancelled"` the same as any other terminal `finish_reason` — pop the partial assistant message into the conversation, no error toast required. ## Error modes | Condition | Status | Code | | ----------------------------------------------------------- | ------ | -------------------------------------------------------------------------- | | Target id doesn't exist | 404 | `chat_cancel_target_not_found` | | Target exists but in `completed` / `failed` / `cancelled` | 409 | `chat_cancel_target_already_terminal` | | Target exists but in `pending` (model hasn't started) | 409 | `chat_cancel_target_not_cancellable` | | Target belongs to a different team than the calling API key | 404 | `chat_cancel_target_not_found` (disclosure-safe — we don't leak existence) | The `not_cancellable` case is narrow: a completion is in `pending` for \~50ms while the platform queues it to the provider. Once the model starts generating (`processing`), cancel is supported. If you hit `not_cancellable`, retry the cancel after a short backoff (\~200ms). ## Use cases ### User navigates away mid-generation ```typescript theme={null} useEffect(() => { return () => { // Cleanup on unmount — cancel the in-flight completion if (currentCompletionId) { fetch(`https://api.aurous-labs.com/v1/chat/completions/${currentCompletionId}/cancel`, { method: "POST", headers: { "X-Api-Key": process.env.AUROUS_API_KEY! }, }); } }; }, [currentCompletionId]); ``` ### Upstream timeout fires before the stream completes ```python theme={null} import time from openai import OpenAI client = OpenAI(base_url="https://api.aurous-labs.com/v1", api_key="al_live_...") stream = client.chat.completions.create( model="aurous-grow-2.0-pro", messages=[{"role": "user", "content": "Write a 10,000-word essay on..."}], max_tokens=8192, stream=True, ) start = time.time() cmp_id = None for chunk in stream: if cmp_id is None: cmp_id = chunk.id # cmp_ if time.time() - start > 30: # 30s budget # cancel — partial tokens billed, rest of the hold released client.post(f"/chat/completions/{cmp_id}/cancel", body=None, cast_to=dict) break ``` ### Server-Sent Events client disconnect (automatic) If the SSE client closes the TCP connection (browser tab closes, network dies, etc.), the platform detects the disconnect and **automatically cancels** the in-flight completion as if `POST /cancel` had been called. The same partial-billing semantics apply; the completion row ends in `status: cancelled` with `cancelled_reason: client_disconnect` in the metadata. ## Where to next? * [Chat overview](/api-reference/chat/overview) — the full chat surface * [Chat streaming](/api-reference/chat/streaming) — SSE behavior * [`POST /v1/chat/completions/{id}/cancel`](/api-reference/openapi#tag/chat) — endpoint reference # Create a chat completion Source: https://docs.aurous-labs.com/api-reference/chat/create-a-chat-completion /api-reference/openapi.json post /v1/chat/completions OpenAI-compatible chat completion. Supports streaming (`stream: true` returns `text/event-stream` SSE), function calling (`tools` / `tool_choice`), multimodal input (image_url / video_url content parts), reasoning_effort, and structured output (`response_format`). Pricing: credits are debited from team balance at completion, snapshotted to the per-model pricing version that was active when the credit hold was placed. `Idempotency-Key` is honored on non-streamed requests only (24h replay window). Streamed requests echo `Aurous-Idempotency-Status: ignored_streaming` and emit a warning frame as the first SSE data line; use `stream=false` for at-most-once semantics. Chat, embedding, image, and video rates are all NOT frozen per `Aurous-Version` — they track the most recently published rate version for each model, because provider economics and per-model markup are tuned more often than the API contract changes. The receipt echoed on every chat / embedding response (and the cost breakdown on every image / video generation) carries the exact rate that applied at dispatch or hold time, so audit trails remain stable even as the underlying rate card changes. `Aurous-Version` still governs request/response shapes — pin it for stable shapes, but call the relevant estimate or models endpoint for a current price immediately before you generate. # Chat idempotency Source: https://docs.aurous-labs.com/api-reference/chat/idempotency Safe retries on POST /v1/chat/completions — replay semantics, the streaming exception, and cross-route conflict handling. `POST /v1/chat/completions` accepts an `Idempotency-Key` header (or `Aurous-Idempotency-Key`) on non-streamed requests. A successful key + body combination is cached server-side for 24 hours; any subsequent identical request returns the cached response with `Aurous-Idempotent-Replayed: true`. Mismatched bodies return `409 idempotency_key_in_use`. This is the same idempotency contract used across `/v1/embeddings`, `/v1/images`, and `/v1/videos`. See the global [Idempotency](/idempotency) page for the broad pattern; this page covers the chat-specific specifics. ## When to use it Use an idempotency key whenever a network-level retry could otherwise cause a duplicate charge: * Your client library auto-retries on `5xx` or network timeouts * You're calling from a background job that resumes after a worker restart * You're processing user-submitted content where the user might click "send" twice * You're inside a database transaction that might roll back and replay The pattern: mint a UUID (or any opaque value, max 64 chars) per **logical operation**, pass it on every retry, get the same response back without re-billing. ```bash theme={null} KEY=$(uuidgen) curl -X POST https://api.aurous-labs.com/v1/chat/completions \ -H "Authorization: Bearer $AUROUS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $KEY" \ -d '{ "model": "aurous-grow-2.0-pro", "messages": [{ "role": "user", "content": "say hi" }], "max_tokens": 10 }' # Retry — returns the same response + Aurous-Idempotent-Replayed: true curl -X POST https://api.aurous-labs.com/v1/chat/completions \ -H "Authorization: Bearer $AUROUS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $KEY" \ -d '{ "model": "aurous-grow-2.0-pro", "messages": [{ "role": "user", "content": "say hi" }], "max_tokens": 10 }' ``` The replay returns instantly — no model call, no charge, no row mutation. ## Response headers The canonical signal that a response is a replay is `Aurous-Idempotent-Replayed: true`. On any FIRST successful idempotent call, the header is absent; on a replay of the same key + same body, the header is present and set to `true`. `Aurous-Idempotency-Status` is informational and currently emitted only on the streaming path: * `Aurous-Idempotency-Status: ignored_streaming` — on `stream: true` + `Idempotency-Key`, the key is ignored (see [Streaming exception](#streaming-exception)). * `Aurous-Idempotency-Status: not_set` — on `stream: true` with no key. * *(non-streaming responses do not currently emit this header — the presence/absence of `Aurous-Idempotent-Replayed` is the canonical signal. Surfacing `accepted` on non-streamed responses is on the v1.0.x roadmap.)* ## Conflict semantics If you send the SAME idempotency key with a DIFFERENT body within the 24-hour window, the platform returns `409 idempotency_key_in_use`: ```json theme={null} { "error": { "type": "invalid_request", "code": "idempotency_key_in_use", "message": "Idempotency-Key 'xyz...' was used 12 minutes ago with a different request body. Pass a unique key per logical operation, or pass the same body to replay the cached response.", "doc_url": "https://docs.aurous-labs.com/errors#idempotency_key_in_use", "request_id": "req_..." } } ``` The body fingerprint covers the entire `POST` body — model, messages, tools, response\_format, temperature, max\_tokens, etc. A single-character change in the prompt is enough to mismatch. ## Cross-route conflicts Idempotency keys are scoped to your **team**, not to a single route. Using the same key on `POST /v1/chat/completions` and then on `POST /v1/embeddings` is treated as a body mismatch (the routes have different bodies) and returns the same `409 idempotency_key_in_use` error. Mint a fresh key per logical operation, OR scope your keys with a route prefix (`chat-`, `embed-`) if you're concerned about collisions in your own code. ## Key format * 1-256 printable-ASCII characters; we recommend UUIDs (v4 or v7) or other opaque identifiers * Empty / whitespace-only keys are rejected with `400 invalid_request` * Keys longer than 256 chars are rejected with `400 invalid_request` We don't enforce a specific format — `Idempotency-Key: my-job-2026-05-20-001` works fine. We just need it to be unique per logical operation. ## Header aliases Two header names are accepted (case-insensitive): * `Idempotency-Key: ` — Stripe-style, recommended * `Aurous-Idempotent-Key: ` — vendor-prefixed alias Send either one; we treat them as equivalent. If you send both, the platform uses the first one in the request order. ## Streaming exception `POST /v1/chat/completions` with `stream: true` and an `Idempotency-Key` header is allowed, but the **idempotency does not apply** — the platform emits `Aurous-Idempotency-Status: ignored_streaming` and a warning frame as the first SSE data line: ``` data: { "warning": { "code": "idempotency_key_ignored_on_streaming", "message": "Idempotency-Key was provided but ignored on a streamed request. Use stream: false for at-most-once semantics." } } data: { "object": "chat.completion.chunk", ... } ... data: [DONE] ``` The reason: a streamed response is a multi-frame transport that can be partially consumed, partially discarded by the client, or interrupted mid-flight. Replaying a partial stream from cache would either re-emit frames the client already saw (incorrect playback) or restart from frame 1 (different semantics from a fresh call). Neither is sound. The two valid patterns for at-most-once streaming: 1. **`stream: false`** for the chat completion you need to be at-most-once (the dominant integrators of this pattern are background jobs where the streaming UX is irrelevant — the worker just needs the final assistant message) 2. **Client-side dedupe** for streamed UI — track which `cmp_` you've already shown the user; if the same logical operation retries, suppress the second stream We may add a `replay_on_idempotency` option for streamed responses in v1.1 — it would cache the full final response and replay it as a single non-streamed frame on the retry. If you have a concrete use case, email **[support@aurous-labs.com](mailto:support@aurous-labs.com)**. ## Window + storage * Idempotency keys are cached for **24 hours** after first use. After 24 hours the key is forgotten — sending the same key + same body at hour 25 mints a NEW completion and bills it. * Caches are scoped per-team. Two different teams can use the same opaque key value without conflict. * The cache stores the full response body — including `cmp_`, the chat content, and the usage block — so the replay is **semantically equivalent** to the original. Field ordering may differ between the original and the replay (the replay reconstructs the JSON from the stored cache, not from the original serialization), but every value is byte-identical and clients that parse JSON (which is everyone) are unaffected. ## Combining idempotency with retries The recommended retry pattern for chat completions: 1. Mint a UUID per logical operation 2. Use it on every retry of that operation 3. Retry on `5xx` and network errors, but NOT on `4xx` (a 4xx means the request is malformed — retrying won't help) 4. Exponential backoff with jitter — start at 1s, double up to 32s 5. Cap retries at 5 (chat completions take \~1-15s; 5 retries over \~60s is generous) The OpenAI SDK's built-in retry logic (in both Node and Python) honors any header you pass, so setting `idempotencyKey` once and letting the SDK retry is the simplest pattern. ## Where to next? * [Idempotency (concept)](/idempotency) — the global idempotency pattern across all writes * [Chat overview](/api-reference/chat/overview) — the full chat surface * [Chat streaming](/api-reference/chat/streaming) — SSE details and the streaming exception * [`POST /v1/chat/completions`](/api-reference/openapi#tag/chat) — the endpoint reference # Multimodal input Source: https://docs.aurous-labs.com/api-reference/chat/multimodal Pass images and video alongside text in a chat completion. `messages[*].content` accepts either a plain string (text-only) or an **array of content parts** when you want to mix text with images or video. The model `aurous-grow-2.0-pro` is multimodal — it accepts text + image and text + video parts in the same request. ## Content-part shape ```jsonc theme={null} { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://example.com/cat.jpg", "detail": "high" } } ] } ``` Supported part types: | `type` | Payload | Notes | | ----------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------ | | `text` | `{ "text": "..." }` | Plain text fragment. | | `image_url` | `{ "image_url": { "url": "...", "detail": "low" \| "high" \| "auto" } }` | URL must be HTTPS-fetchable, or a `data:image/...;base64,...` URI. | | `video_url` | `{ "video_url": { "url": "..." } }` | URL must point to a video supported by `aurous-grow-2.0-pro`. | The `detail` hint on `image_url` corresponds to the vision-quality tier: `low` is the cheapest (\~512 tokens per image), `high` is the standard (\~1024 tokens per image), and the platform-specific `xhigh` (\~2048 tokens) is exposed as `vision_quality: "xhigh"` at the top level of the request body for finer-grained control. ## Token cost for images Image input is charged per token alongside text input. A rough heuristic at default quality: | Quality | Tokens per image | | ---------------- | ---------------- | | `low` | \~512 | | `high` (default) | \~1024 | | `xhigh` | \~2048 | The exact count is returned in the `usage.prompt_tokens` field of the response. Estimates are held conservatively; if actuals exceed the hold, the difference is committed up to your team's negative-balance floor (default 0 — see [Pricing](/api-reference/chat/pricing)). ## Image input example ```bash cURL theme={null} curl -X POST https://api.aurous-labs.com/v1/chat/completions \ -H "Authorization: Bearer $AUROUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "aurous-grow-2.0-pro", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "What breed is this cat?" }, { "type": "image_url", "image_url": { "url": "https://example.com/cat.jpg", "detail": "high" } } ] } ], "max_tokens": 256 }' ``` ```typescript Node.js (OpenAI SDK) theme={null} import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.aurous-labs.com/v1", apiKey: process.env.AUROUS_API_KEY!, }); const res = await client.chat.completions.create({ model: "aurous-grow-2.0-pro", messages: [ { role: "user", content: [ { type: "text", text: "What breed is this cat?" }, { type: "image_url", image_url: { url: "https://example.com/cat.jpg", detail: "high" }, }, ], }, ], max_tokens: 256, }); console.log(res.choices[0].message.content); ``` ```python Python (OpenAI SDK) theme={null} from openai import OpenAI client = OpenAI( base_url="https://api.aurous-labs.com/v1", api_key="al_live_xxxxxxxxxxxxxxxx", ) res = client.chat.completions.create( model="aurous-grow-2.0-pro", messages=[ { "role": "user", "content": [ {"type": "text", "text": "What breed is this cat?"}, { "type": "image_url", "image_url": { "url": "https://example.com/cat.jpg", "detail": "high", }, }, ], } ], max_tokens=256, ) print(res.choices[0].message.content) ``` ## Base64-encoded images For images that aren't reachable over HTTPS (test fixtures, local-only files), inline them via a data URI: ```typescript Node.js theme={null} import { readFileSync } from "node:fs"; const buf = readFileSync("./cat.jpg"); const base64 = buf.toString("base64"); const dataUrl = `data:image/jpeg;base64,${base64}`; const res = await client.chat.completions.create({ model: "aurous-grow-2.0-pro", messages: [ { role: "user", content: [ { type: "text", text: "What breed?" }, { type: "image_url", image_url: { url: dataUrl } }, ], }, ], }); ``` ```python Python theme={null} import base64 from openai import OpenAI with open("cat.jpg", "rb") as f: b64 = base64.b64encode(f.read()).decode() res = client.chat.completions.create( model="aurous-grow-2.0-pro", messages=[ { "role": "user", "content": [ {"type": "text", "text": "What breed?"}, { "type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}, }, ], } ], ) ``` Watch the request body size — base64 inflates payloads by \~33%. For images larger than a few hundred KB, host them on your own CDN and pass the URL. ## Video input Video parts work the same way as images. The model decodes the first N seconds of the video (provider-specific cap) at sampled frames and treats them as the visual context: ```jsonc theme={null} { "role": "user", "content": [ { "type": "text", "text": "Summarize what happens in this clip." }, { "type": "video_url", "video_url": { "url": "https://example.com/clip.mp4" } } ] } ``` Video token costs are higher than images (typically 3–5x) and depend on clip length. The exact charge is in `usage.prompt_tokens` on the response. ## Mixing modalities in a single message You can interleave text and vision parts freely: ```jsonc theme={null} { "role": "user", "content": [ { "type": "text", "text": "Compare these two product photos:" }, { "type": "image_url", "image_url": { "url": "https://example.com/a.jpg" } }, { "type": "text", "text": "versus" }, { "type": "image_url", "image_url": { "url": "https://example.com/b.jpg" } }, { "type": "text", "text": "Which is the better photograph?" } ] } ``` The model treats the array as one ordered input — text fragments and visual parts share semantic context. ## Limits * The total prompt-token count (text + image + video) must be at or below the model's `aurous_metadata.context_window`. Over the cap returns `400 max_input_tokens_exceeded`. * Image and video URLs must resolve in under 10 seconds; longer fetches will be treated as a failed request. * Provider-side moderation may reject inputs; rejection is surfaced via `finish_reason: "content_filter"`. ## Errors * `max_input_tokens_exceeded` (400) — prompt is over the model's context window after image/video tokenization. Drop content parts or lower `detail`. * `chat_provider_request_invalid` (500) — an inline asset failed to parse. This is treated as our bug; the request is logged for follow-up. # Chat completions Source: https://docs.aurous-labs.com/api-reference/chat/overview OpenAI-compatible chat over the Aurous Labs API. Drop-in for any OpenAI SDK. `POST /v1/chat/completions` is the entry point for conversational and tool-driven LLM workloads. The surface is intentionally OpenAI-compatible — if your code already talks to `chat.completions.create`, you can point the SDK at Aurous Labs by changing two lines: the `baseURL` and the API key. ```text theme={null} baseURL: https://api.aurous-labs.com/v1 apiKey: al_live_ ``` Pass the key either as `Authorization: Bearer al_live_...` (what OpenAI SDKs send by default) or as `X-Api-Key: al_live_...`. Both are accepted. ## What's supported * **Streaming** via `stream: true` (Server-Sent Events). See [Streaming](/api-reference/chat/streaming). * **Function / tool calling** via `tools` and `tool_choice`. See [Tools](/api-reference/chat/tools). * **Multimodal input** — image and video parts inside `messages[*].content`. See [Multimodal](/api-reference/chat/multimodal). * **Reasoning effort** for reasoning-capable models. See [Reasoning](/api-reference/chat/reasoning). * **Structured output** via `response_format: { type: "json_schema", ... }` or `{ type: "json_object" }`. * **Idempotency** via the `Idempotency-Key` header — on non-streamed requests only. Streamed requests echo `Aurous-Idempotency-Status: ignored_streaming` and emit a warning frame as the first SSE data line. Use `stream: false` for at-most-once semantics. ## Models List available models at [`GET /v1/models`](/api-reference/openapi#tag/models). The day-1 chat model is **`aurous-grow-2.0-pro`** — a multimodal, tool-capable, reasoning-capable model with a 256K context window. Capabilities, context window, default and hard-cap `max_output_tokens`, and credit rates are returned on the `aurous_metadata` extension of each model row. ## Pricing Chat is billed per token at credit rates surfaced on each model row at [`GET /v1/models`](/api-reference/openapi#tag/models) (the `chat_pricing` block) and in the `usage` block of every chat completion response (including the final chunk of a streamed response). The rate on `/v1/models` is the caller's **effective rate** — any per-team override is already applied. See [Pricing](/api-reference/chat/pricing) for the credit math, version pinning, and the rate mutability rules. ## Quick start ```bash cURL theme={null} curl -X POST https://api.aurous-labs.com/v1/chat/completions \ -H "Authorization: Bearer $AUROUS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "model": "aurous-grow-2.0-pro", "messages": [ { "role": "system", "content": "You are a concise assistant." }, { "role": "user", "content": "Summarize the Linear product update in 3 bullets." } ], "max_tokens": 512 }' ``` ```typescript Node.js (OpenAI SDK) theme={null} import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.aurous-labs.com/v1", apiKey: process.env.AUROUS_API_KEY!, // al_live_xxxxxxxxxxxxxxxx }); const completion = await client.chat.completions.create({ model: "aurous-grow-2.0-pro", messages: [ { role: "system", content: "You are a concise assistant." }, { role: "user", content: "Summarize the Linear product update in 3 bullets." }, ], max_tokens: 512, }); console.log(completion.choices[0].message.content); console.log(completion.usage); // includes credits_charged ``` ```python Python (OpenAI SDK) theme={null} from openai import OpenAI client = OpenAI( base_url="https://api.aurous-labs.com/v1", api_key="al_live_xxxxxxxxxxxxxxxx", # or read from env ) completion = client.chat.completions.create( model="aurous-grow-2.0-pro", messages=[ {"role": "system", "content": "You are a concise assistant."}, {"role": "user", "content": "Summarize the Linear product update in 3 bullets."}, ], max_tokens=512, ) print(completion.choices[0].message.content) print(completion.usage) # includes credits_charged ``` ## Response shape Non-streamed responses are standard OpenAI shape with an Aurous extension on `usage`: ```jsonc theme={null} { "id": "cmp_01HXMQ7Z3K8Y2VNABCDEFGHJKM", "object": "chat.completion", "created": 1731948000, "model": "aurous-grow-2.0-pro", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "...", "tool_calls": null }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 200, "completion_tokens": 600, "total_tokens": 800, "credits_charged": 0.285, "breakdown": { "input_credits": 0.015, "output_credits": 0.270, "model": "aurous-grow-2.0-pro", "pricing_version": 7 } } } ``` The `usage.credits_charged` value is the exact amount deducted from your team's balance. The `breakdown.pricing_version` is the rate-card version snapshot applied to this inference — pinned at request time even if admin updates rates afterward. ## Idempotency Pass `Idempotency-Key` (any opaque value, 1–256 chars; UUID v4 recommended) to make non-streamed `POST /v1/chat/completions` safe to retry. Same key + same body within 24h replays the cached response with `Aurous-Idempotent-Replayed: true`. Same key + different body returns `409 idempotency_key_in_use`. Streamed requests ignore the header — see [Streaming](/api-reference/chat/streaming). ## Cancellation In-flight streamed requests can be aborted via [`POST /v1/chat/completions/{id}/cancel`](/api-reference/openapi#tag/chat). Partial actuals (tokens delivered before the abort) are committed; the remainder of the held credits is released. See the cancel pattern in [Streaming](/api-reference/chat/streaming#cancel). ## Errors Every non-2xx response uses the standard Aurous error envelope — see [Errors](/errors) for the full taxonomy. Chat-specific codes you might see: * `model_not_found` (404) — unknown `model` slug. Check the `/v1/models` listing. * `model_disabled` (403) — model exists but admin has deactivated it. * `model_wrong_kind` (400) — you sent an embedding model to the chat endpoint (or vice versa). * `max_tokens_exceeds_hard_cap` (400) — requested `max_tokens` is over the model's hard cap. The model's `max_output_tokens_hard_cap` is on the `/v1/models` row. * `missing_max_tokens_no_model_default` (400) — the model has no platform-side default, so you must pass `max_tokens` explicitly. * `max_input_tokens_exceeded` (400) — your prompt is over the model's context window. Trim input or pick a larger model. * `tpm_rate_limit_exceeded` (429) — tokens-per-minute bucket is dry. Sleep `Retry-After` and retry. * `provider_rate_limited` (503) — upstream throttled. Retry-After echoed. * `chat_provider_unavailable` (502) — upstream transient failure. Retry with backoff. # Pricing Source: https://docs.aurous-labs.com/api-reference/chat/pricing Credit math for chat completions, with the rate-mutability asymmetry. Chat completions are billed in **credits** at per-token rates surfaced on each model row at [`GET /v1/models`](/api-reference/openapi#tag/models) under the `chat_pricing` block. The rate is the caller's **effective rate** — any per-team override is already applied. See the dedicated [LLM pricing guide](/pricing/llm-pricing) for the per-1K math and worked examples. ## What you pay for Each chat completion is billed across up to three buckets, depending on the model: | Bucket | Counts | Rate field on `/v1/models` | | --------- | --------------------------------------------------------------------------- | ---------------------------------------------------- | | Input | `usage.prompt_tokens` (text + image + video parts) | `chat_pricing.input.credits_per_M` | | Output | `usage.completion_tokens` (assistant message content + tool-call arguments) | `chat_pricing.output.credits_per_M` | | Reasoning | `usage.reasoning_tokens` (hidden deliberation on reasoning-capable models) | `chat_pricing.output.credits_per_M` (same as output) | Customers can compute credits-per-1K by dividing `credits_per_M` by 1000. The response `usage` block reports each bucket and the rolled-up `credits_charged`: ```jsonc theme={null} { "usage": { "prompt_tokens": 200, "completion_tokens": 600, "reasoning_tokens": 50, "total_tokens": 850, "credits_charged": 0.2856, "breakdown": { "input_credits": 0.0150, "output_credits": 0.2700, "reasoning_credits": 0.0006, "model": "aurous-grow-2.0-pro", "pricing_version": 7 } } } ``` `credits_charged` is the exact amount deducted from your team's balance. Check it on every response — it's the source of truth for the billing line. ## Hold-and-commit (credits are reserved before dispatch) Submitting a chat completion places a **hold** on your team balance sized at `(estimated_input_tokens × 1.10) + (max_tokens × output_rate)`. The hold ensures the request can't run if you can't afford the worst case. When the response completes: * Actuals come back in `usage`. * The hold is replaced by the actual `credits_charged`. * Any unused hold is released back to your balance. If actuals exceed the hold (rare — the 10% input margin and `max_tokens` upper bound usually cover it), the overage is committed up to your team's `balance_negative_floor`. The default floor is `0` — overages beyond that are absorbed by the platform and logged for follow-up. Enterprise contracts can set a per-team negative floor; contact support if your workload needs one. ## The pricing-version pin Every chat completion snapshots the rate-card version at request time. `breakdown.pricing_version` on the response is the version that was applied. Admin can update chat-model rates between published `Aurous-Version` releases, but **the price actually charged is whatever was in force when the request was created** — not whatever the latest rates are at the moment you read the response. ## Mutability asymmetry vs. images and videos Chat, embedding, image, and video rates are all mutable without an `Aurous-Version` bump. Admin can update any of them at any time; image rates are per-tier (`size_tier`) and DB-driven, video rates are per-model and DB-driven. The per-request charge is still deterministic in every case — each request snapshots the rate in force when it was created, and that snapshot is what you're charged: | Surface | How to read the current rate | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Chat, embeddings, video | [`GET /v1/models`](/api-reference/openapi#tag/models) — `chat_pricing` / `embedding_pricing` / `video_pricing` | | Images | `POST /v1/images/estimate` — mirrors the request shape of `POST /v1/images` and returns the exact credit cost without dispatching; see [Image pricing](/api-reference/images/models-and-pricing) for the published rate card | Why: rates track provider tuning, new models, and per-model markup changes that don't justify a new version pin every time. If you want the rate snapshot at the moment your client made a request, call `GET /v1/models` (chat, embedding, video) or `POST /v1/images/estimate` (images) immediately beforehand and store the relevant pricing block next to your request. The `inferences.llm_pricing_version` field on each completion row is the audit-trail proof that the rate you were quoted is the rate you paid. ## Estimating cost before dispatch Two ways to budget: 1. **Hand math** — multiply expected token counts by the per-million rates on `/v1/models`. The per-1K rate is `chat_pricing.input.credits_per_M / 1000` (and likewise for output). See [LLM pricing](/pricing/llm-pricing) for the full formula. 2. **Run a dry call** — submit the request with `max_tokens: 1` and read `usage.prompt_tokens` from the response. The input cost is then `(prompt_tokens / 1_000_000) × chat_pricing.input.credits_per_M`. The 1-token output charge is negligible. A typical short conversational turn on `aurous-grow-2.0-pro` lands around **0.2–0.4 credits**. Long-context analytical turns with reasoning can push past 1 credit. The live `/v1/models` response carries the current per-model rates. ## Refunds and partial charges | Outcome | Charged for | | -------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | Successful completion | Actuals as reported in `usage`. | | Streamed completion cancelled mid-stream | Actuals up to the abort point (tokens already delivered). The remainder of the hold is released. | | Provider returned an empty `content_filter` block | 0. The hold is released. | | Provider returned a partial `content_filter` block | Partial actuals (tokens delivered before the filter trip). | | Provider returned a 5xx | 0. The hold is released. Retry with backoff. | | Bad input (400 from the platform) | 0. No row written. | The policy is: **you pay for what was delivered.** ## Idempotency and billing For non-streamed requests, an `Idempotency-Key` replays the cached response — including the original `credits_charged` — for 24 hours. You will NOT be double-charged for a retried key. See [Idempotency](/idempotency) for the full semantics. ## Where to read rates * [`GET /v1/models`](/api-reference/openapi#tag/models) — per-model `chat_pricing` / `embedding_pricing` (the caller's effective rate, including any per-team override) plus capability metadata. * [LLM pricing guide](/pricing/llm-pricing) — per-1K math, examples, and the mutability story in long form. ## Common questions **Are cached prompts cheaper?** Some providers cache stable prefixes and discount the input rate for cache hits. When a hit happens, the response `usage` will reflect the reduced charge on `breakdown.input_credits`; the platform handles cache accounting transparently. **Can I see usage trends?** Yes — `GET /v1/usage` aggregates credits by day/key/model. The dashboard's usage tab visualizes the same data. **What if I'm rate-limited?** Token throughput is limited per team (TPM bucket). Hitting it returns `429 tpm_rate_limit_exceeded` with `Retry-After`. The TPM is in addition to the per-minute request bucket (RPM); both apply. # Reasoning effort Source: https://docs.aurous-labs.com/api-reference/chat/reasoning Tune deliberate reasoning depth for reasoning-capable chat models. Models whose `aurous_metadata.capabilities` array contains `"reasoning_effort"` support an OpenAI-compatible `reasoning_effort` parameter. Setting it lets the model spend more or fewer hidden tokens on deliberation before producing the visible answer. **`aurous-grow-2.0-pro`** is reasoning-capable. ## The parameter ```jsonc theme={null} { "model": "aurous-grow-2.0-pro", "messages": [...], "reasoning_effort": "medium", "max_tokens": 2048 } ``` Accepted values: `"minimal"`, `"low"`, `"medium"` (default), `"high"`. | Effort | Behavior | When to use | | --------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | `minimal` | Reasoning suppressed entirely (model emits only visible output). Fastest, cheapest. | Stateless single-turn answers, classification, no multi-step thinking needed. | | `low` | Minimal hidden reasoning. Fast, cheap. | Snappy chat replies, summarization, lightweight extraction. | | `medium` | Default. Balanced. | Day-to-day prompts where you want quality without paying full freight. | | `high` | Maximum hidden reasoning. Slower, more reasoning tokens billed. | Hard math, multi-step planning, tricky tool-use chains, code generation that has to compile first try. | ## Billing for reasoning tokens Reasoning tokens are visible in the response `usage` block as `reasoning_tokens` — a SEPARATE count from `completion_tokens` (which covers visible output text only). They contribute to `credits_charged` at the model's output rate; the credit subtotal is broken out as `breakdown.reasoning_credits` so the four credit lines reconcile cleanly with `credits_charged`: ```jsonc theme={null} { "usage": { "prompt_tokens": 180, "completion_tokens": 420, "reasoning_tokens": 350, "total_tokens": 950, "credits_charged": 0.3645, "breakdown": { "input_credits": 0.0135, "output_credits": 0.1890, "reasoning_credits": 0.1620, "model": "aurous-grow-2.0-pro", "pricing_version": 1 } } } ``` A `reasoning_effort: "high"` turn on a hard problem can spend 1500–3000 reasoning tokens. Budget for it, or set `low` when you don't need the depth. ## Example ```bash cURL theme={null} curl -X POST https://api.aurous-labs.com/v1/chat/completions \ -H "Authorization: Bearer $AUROUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "aurous-grow-2.0-pro", "messages": [ { "role": "user", "content": "An array contains [3, -1, 4, -2, 5, -3, 6]. Find the contiguous subarray with the largest sum. Show your work." } ], "reasoning_effort": "high", "max_tokens": 2048 }' ``` ```typescript Node.js (OpenAI SDK) theme={null} import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.aurous-labs.com/v1", apiKey: process.env.AUROUS_API_KEY!, }); const res = await client.chat.completions.create({ model: "aurous-grow-2.0-pro", messages: [ { role: "user", content: "An array contains [3, -1, 4, -2, 5, -3, 6]. Find the contiguous subarray with the largest sum. Show your work.", }, ], // @ts-expect-error reasoning_effort lands on the openai TS types alongside o1; passes through. reasoning_effort: "high", max_tokens: 2048, }); console.log(res.choices[0].message.content); console.log("reasoning_tokens:", res.usage?.reasoning_tokens); ``` ```python Python (OpenAI SDK) theme={null} from openai import OpenAI client = OpenAI( base_url="https://api.aurous-labs.com/v1", api_key="al_live_xxxxxxxxxxxxxxxx", ) res = client.chat.completions.create( model="aurous-grow-2.0-pro", messages=[ { "role": "user", "content": ( "An array contains [3, -1, 4, -2, 5, -3, 6]. Find the " "contiguous subarray with the largest sum. Show your work." ), } ], reasoning_effort="high", max_tokens=2048, ) print(res.choices[0].message.content) print("reasoning_tokens:", res.usage.reasoning_tokens) ``` ## When the model doesn't support reasoning effort If you set `reasoning_effort` on a model without the capability, the parameter is silently ignored — the request still runs at the model's default behavior. To verify capability before sending, check `aurous_metadata.capabilities` on the `/v1/models` listing: ```bash theme={null} curl https://api.aurous-labs.com/v1/models \ -H "Authorization: Bearer $AUROUS_API_KEY" \ | jq '.data[] | select(.id == "aurous-grow-2.0-pro") | .aurous_metadata.capabilities' # → ["streaming","tools","multimodal_input","reasoning_effort","structured_output"] ``` ## Streaming with reasoning Reasoning models support streaming. Reasoning tokens are NOT streamed back as content deltas — they appear only in the final chunk's `usage.reasoning_tokens` count. From the client's perspective, the first content delta arrives after the model finishes its hidden reasoning pass. This means streamed reasoning responses have a longer "time to first token" than non-reasoning streams. If your UX shows a typing indicator, keep it visible during the silence; the keep-alive comment frame (`: keep-alive`) confirms the connection is healthy. ## Practical guidance * Default to `medium`. Tune up or down based on task category. * For agentic tool-use loops with `high`, watch `reasoning_tokens` carefully — the cost can dwarf input + visible-output tokens combined. * For latency-sensitive UX (typeahead, completions inside a form), prefer `low` or omit the param entirely. # Retrieve a stored chat completion Source: https://docs.aurous-labs.com/api-reference/chat/retrieve-a-stored-chat-completion /api-reference/openapi.json get /v1/chat/completions/{id} Look up a previously created chat completion by its opaque id (`cmp_`). Returns the reconstructed `ChatCompletionResponse` with the actuals committed at completion time. Cross-team lookups return 404 (no enumeration leak). # Streaming Source: https://docs.aurous-labs.com/api-reference/chat/streaming Server-Sent Events for chat completions, with token usage in the final chunk. Set `stream: true` on `POST /v1/chat/completions` and the response switches from `application/json` to `text/event-stream`. Tokens are emitted as they're generated; the final chunk carries the `usage` block (including `credits_charged`) before the standard `data: [DONE]` terminator. ## Frame format Each frame is a single `data:` line followed by a blank line: ```text theme={null} data: {"id":"cmp_...","object":"chat.completion.chunk","created":1731948000,"model":"aurous-grow-2.0-pro","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]} data: {"id":"cmp_...","object":"chat.completion.chunk","created":1731948000,"model":"aurous-grow-2.0-pro","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]} data: {"id":"cmp_...","object":"chat.completion.chunk","created":1731948000,"model":"aurous-grow-2.0-pro","choices":[{"index":0,"delta":{"content":" world"},"finish_reason":null}]} data: {"id":"cmp_...","object":"chat.completion.chunk","created":1731948000,"model":"aurous-grow-2.0-pro","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":12,"completion_tokens":24,"total_tokens":36,"credits_charged":0.0117,"breakdown":{"input_credits":0.0009,"output_credits":0.0108,"model":"aurous-grow-2.0-pro","pricing_version":7}}} data: [DONE] ``` A keep-alive comment frame `: keep-alive` is sent every 15 seconds if no content chunk has been emitted — SSE clients ignore comment lines, so you can rely on it to keep the connection warm without affecting parsing. ## Headers on a streamed response * `Content-Type: text/event-stream` * `Cache-Control: no-cache, no-transform` * `Aurous-Request-Id: req_` — quote in support tickets. * `Aurous-Version: YYYY-MM-DD` — the contract version applied. * `Aurous-Idempotency-Status: ignored_streaming` — if you sent `Idempotency-Key`. The header is recorded but the key is NOT stored; the first SSE frame will be a warning (see [Idempotency on streamed requests](#idempotency-on-streamed-requests)). * `X-RateLimit-Limit` / `X-RateLimit-Remaining` / `X-RateLimit-Reset` — RPM bucket. * `X-RateLimit-TPM-Limit` / `X-RateLimit-TPM-Remaining` / `X-RateLimit-TPM-Reset` — TPM (tokens-per-minute) bucket. ## Example: streamed completion ```bash cURL theme={null} curl -N -X POST https://api.aurous-labs.com/v1/chat/completions \ -H "Authorization: Bearer $AUROUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "aurous-grow-2.0-pro", "messages": [{ "role": "user", "content": "Write a haiku about latency." }], "stream": true, "max_tokens": 256 }' ``` ```typescript Node.js (OpenAI SDK) theme={null} import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.aurous-labs.com/v1", apiKey: process.env.AUROUS_API_KEY!, }); const stream = await client.chat.completions.create({ model: "aurous-grow-2.0-pro", messages: [{ role: "user", content: "Write a haiku about latency." }], stream: true, max_tokens: 256, }); for await (const chunk of stream) { const delta = chunk.choices[0]?.delta?.content; if (delta) process.stdout.write(delta); // The final chunk carries the usage block. if (chunk.usage) { console.log("\n---"); console.log("credits_charged:", chunk.usage.credits_charged); } } ``` ```python Python (OpenAI SDK) theme={null} from openai import OpenAI client = OpenAI( base_url="https://api.aurous-labs.com/v1", api_key="al_live_xxxxxxxxxxxxxxxx", ) stream = client.chat.completions.create( model="aurous-grow-2.0-pro", messages=[{"role": "user", "content": "Write a haiku about latency."}], stream=True, max_tokens=256, ) for chunk in stream: delta = chunk.choices[0].delta.content if delta: print(delta, end="", flush=True) if chunk.usage: print("\n---") print("credits_charged:", chunk.usage.credits_charged) ``` ## Cancel an in-flight stream Two ways to stop a stream: 1. **Close the connection** — tear down the TCP/HTTP connection on the client. The server detects the disconnect, waits up to 5 seconds for the upstream provider's final usage chunk, commits actuals up to the abort point, releases the remainder of the hold, and flips the row to `cancelled_client_disconnect`. 2. **Call the cancel endpoint** — `POST /v1/chat/completions/{id}/cancel`. Aborts the upstream connection, commits actuals from chunks already delivered, releases the remainder, flips the row to `cancelled_by_request`, and returns the final-state record: ```jsonc theme={null} { "id": "cmp_01HXMQ7Z3K8Y2VNABCDEFGHJKM", "object": "chat.completion", "status": "cancelled_by_request", "usage": { "prompt_tokens": 12, "completion_tokens": 8, "total_tokens": 20, "credits_charged": 0.0069 } } ``` The cancel endpoint distinguishes three terminal cases — useful for retry decisions: | Code | HTTP | When | | ------------------------------------- | ---- | ---------------------------------------------------------------------------------------------------------- | | `chat_cancel_target_not_found` | 404 | `id` doesn't exist for your team. | | `chat_cancel_target_already_terminal` | 409 | Already completed, failed, or cancelled. Idempotency hint, not a bug. | | `chat_cancel_target_not_cancellable` | 409 | Record exists and isn't terminal but the stream can't be aborted (e.g. a sync call that already returned). | ### `AbortController` pattern (Node) The OpenAI SDK accepts an `AbortSignal`, so you can wire the same controller into both the request and a "Stop" button in your UI: ```typescript theme={null} import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.aurous-labs.com/v1", apiKey: process.env.AUROUS_API_KEY!, }); const controller = new AbortController(); // Stop button handler: controller.abort(); try { const stream = await client.chat.completions.create( { model: "aurous-grow-2.0-pro", messages: [{ role: "user", content: "Long answer please" }], stream: true, max_tokens: 4096, }, { signal: controller.signal }, ); let completionId: string | undefined; for await (const chunk of stream) { completionId ??= chunk.id; const delta = chunk.choices[0]?.delta?.content; if (delta) process.stdout.write(delta); } } catch (err) { if (controller.signal.aborted && completionId) { // Optional: confirm cancellation server-side and commit actuals. await fetch( `https://api.aurous-labs.com/v1/chat/completions/${completionId}/cancel`, { method: "POST", headers: { Authorization: `Bearer ${process.env.AUROUS_API_KEY}` }, }, ); } else { throw err; } } ``` Aborting via the controller alone tears down the connection, which is enough to commit partial actuals. Calling `/cancel` afterward is optional — useful when you want to confirm the final-state record before clearing it from your UI. ### `AbortController` pattern (Python) ```python theme={null} import os import threading from openai import OpenAI client = OpenAI( base_url="https://api.aurous-labs.com/v1", api_key=os.environ["AUROUS_API_KEY"], ) # A simple "stop" mechanism shared between threads. stop_event = threading.Event() stream = client.chat.completions.create( model="aurous-grow-2.0-pro", messages=[{"role": "user", "content": "Long answer please"}], stream=True, max_tokens=4096, ) completion_id = None for chunk in stream: completion_id = completion_id or chunk.id if stop_event.is_set(): stream.close() # closes the underlying HTTP connection break delta = chunk.choices[0].delta.content if delta: print(delta, end="", flush=True) ``` ## Idempotency on streamed requests Streamed requests cannot be replayed deterministically, so `Idempotency-Key` is intentionally not stored when `stream: true`. The server still echoes the header value back via `Aurous-Idempotency-Status: ignored_streaming`, and emits a warning frame as the FIRST data line so SDK callers see it before any token chunks: ```text theme={null} data: {"warning":{"code":"idempotency_key_ignored_on_streaming","message":"Idempotency-Key headers are ignored on streamed chat requests. Use stream=false for at-most-once semantics."}} ``` OpenAI-compatible clients won't recognize the `warning` shape, but they won't crash on it either — the field is ignored. If you need at-most-once delivery (typical for billing-sensitive integrations), set `stream: false` and pass `Idempotency-Key` as usual. ## Client disconnects If the client connection drops mid-stream (network blip, browser tab closed, server-side timeout): * The server keeps the upstream connection open for up to 5 seconds in case the provider's final usage chunk arrives. * If usage arrives within the grace window, actuals are committed exactly. * If not, the server commits a chunk-count fallback estimate, logs the incident, and flips the row to `cancelled_client_disconnect`. The row remains retrievable via `GET /v1/chat/completions/{id}` with its final committed `usage`. ## Throughput & rate limits Streamed chat requests draw from two buckets: * **RPM** (requests/minute) — `X-RateLimit-Remaining` headers. * **TPM** (tokens/minute) — `X-RateLimit-TPM-Remaining` headers. The bucket counts estimated tokens at request time; actuals adjust the bucket on commit. Hitting either bucket returns `429` with `Retry-After` — see [Rate limits](/rate-limits). # Structured output Source: https://docs.aurous-labs.com/api-reference/chat/structured-output Schema-enforced JSON output from POST /v1/chat/completions via response_format. Two modes: json_schema (strict) and json_object (loose). `POST /v1/chat/completions` accepts a `response_format` parameter that forces the model's output to be valid JSON. Two modes are supported: * **`{ "type": "json_object" }`** — loose. Just guarantees the output parses as JSON. No schema enforcement. * **`{ "type": "json_schema", "json_schema": {...} }`** — strict. The output must conform to the supplied JSON Schema, validated by the model itself. Both modes follow OpenAI's shape, so the OpenAI SDK works without modification. ## `json_object` — loose JSON output The simplest mode. Useful when the schema is implicit (the prompt tells the model what to produce) and you just need parseable JSON back. ```typescript Node.js theme={null} const completion = await client.chat.completions.create({ model: "aurous-grow-2.0-pro", messages: [ { role: "system", content: "Respond with JSON only. Use the structure { 'city': string, 'capital': string }." }, { role: "user", content: "France" }, ], response_format: { type: "json_object" }, max_tokens: 100, }); const parsed = JSON.parse(completion.choices[0].message.content!); // { city: "France", capital: "Paris" } ``` ```python Python theme={null} res = client.chat.completions.create( model="aurous-grow-2.0-pro", messages=[ {"role": "system", "content": "Respond with JSON only. Use the structure { 'city': string, 'capital': string }."}, {"role": "user", "content": "France"}, ], response_format={"type": "json_object"}, max_tokens=100, ) import json parsed = json.loads(res.choices[0].message.content) # { 'city': 'France', 'capital': 'Paris' } ``` The model is constrained to emit syntactically-valid JSON, but the schema is up to you to communicate in the prompt. Use `json_object` mode when the schema is loose, dynamic, or you want the model to choose its own keys. ## `json_schema` — strict schema-enforced output The model is constrained at decoding time to emit JSON that **validates** against your supplied schema. Useful when downstream code expects an exact shape — extracting fields from a document, classification, structured agent calls. ```typescript Node.js theme={null} const completion = await client.chat.completions.create({ model: "aurous-grow-2.0-pro", messages: [ { role: "user", content: "Extract: 'Jane Doe lives in Berlin and is 34 years old.'" }, ], response_format: { type: "json_schema", json_schema: { name: "person", strict: true, schema: { type: "object", properties: { name: { type: "string" }, city: { type: "string" }, age: { type: "integer" }, }, required: ["name", "city", "age"], additionalProperties: false, }, }, }, max_tokens: 200, }); const person = JSON.parse(completion.choices[0].message.content!); // { name: "Jane Doe", city: "Berlin", age: 34 } ``` ```python Python theme={null} res = client.chat.completions.create( model="aurous-grow-2.0-pro", messages=[ {"role": "user", "content": "Extract: 'Jane Doe lives in Berlin and is 34 years old.'"}, ], response_format={ "type": "json_schema", "json_schema": { "name": "person", "strict": True, "schema": { "type": "object", "properties": { "name": {"type": "string"}, "city": {"type": "string"}, "age": {"type": "integer"}, }, "required": ["name", "city", "age"], "additionalProperties": False, }, }, }, max_tokens=200, ) import json person = json.loads(res.choices[0].message.content) # { 'name': 'Jane Doe', 'city': 'Berlin', 'age': 34 } ``` The `name` field is required (it's the schema's identifier in your code; the platform doesn't use it but the OpenAI-compat surface demands it). `strict: true` enables the model's strict-mode decoder; `strict: false` (or omitting `strict`) reverts to best-effort schema adherence — the model will TRY to follow the schema but isn't guaranteed. ## Schema constraints The supplied schema must obey a subset of JSON Schema: * **`type`**: `object`, `array`, `string`, `integer`, `number`, `boolean`, `null` (any combination) * **`properties`**: nested objects allowed * **`required`**: required field names * **`additionalProperties: false`**: recommended; omit to allow extra keys * **`enum`**: allowed on `string`, `integer`, `number` * **`items`**: required on `array` types; supports nested schemas * **`anyOf`** / **`oneOf`**: supported on object properties * **`$ref`**: supported within the same `json_schema` object * **`description`**: optional; the model uses it as guidance **Constraints not supported**: * `pattern` (regex matching) — falls back to best-effort even in strict mode * `format` (date-time, email, etc.) — same; best-effort * `minimum` / `maximum` / `minLength` / `maxLength` — same; best-effort * External `$ref` to remote schema URLs If your schema exceeds the platform's depth or size guard, you'll get a `400 response_format_too_deep` or `400 response_format_too_large` error. ## Schema size + depth caps * Maximum schema depth: **8 levels of nesting** (root object = depth 1) * Maximum total schema size: **128KB of JSON-encoded text** * Maximum properties per object: **128** * Maximum enum values per field: **256** These caps exist to prevent the validator from doing pathologically slow work on adversarial input. Real-world schemas don't come close. ## Streaming + structured output `response_format` works with `stream: true`. The model emits valid-JSON-shaped partial frames; the FULL JSON validates against the schema only after the final non-`[DONE]` chunk. If you want to parse incrementally as bytes arrive, use a streaming JSON parser (e.g., `clarinet` / `oboe.js` in Node, `ijson` in Python) — the partial frame text is sound enough for prefix parsing. ## Tool calls vs structured output Both `tools` and `response_format` can be used in the same request. The semantics: * If `tool_choice` resolves to a tool call, the model emits the tool call (not the structured response). * If the model produces an assistant message instead of a tool call, that message is constrained to the `response_format`. In practice, use `tools` when the model should choose between calling a function or producing a structured answer, and `response_format` alone when the model must always produce JSON. ## Error modes | Condition | Status | Code | | ---------------------------------------------------- | ------ | ----------------------------- | | Schema exceeds depth cap | 400 | `response_format_too_deep` | | Schema exceeds size cap | 400 | `response_format_too_large` | | Malformed `json_schema` block | 400 | DTO `invalid_request` | | Strict mode requested but model emitted invalid JSON | 502 | `chat_provider_unknown_error` | The last case is rare in practice with `aurous-grow-2.0-pro` + `strict: true` — the decoder is constrained at token-emission time, not validated post-hoc. ## Where to next? * [Chat overview](/api-reference/chat/overview) — the full chat surface * [Chat tools](/api-reference/chat/tools) — function/tool calling * [`POST /v1/chat/completions`](/api-reference/openapi#tag/chat) — endpoint reference # Tools and function calling Source: https://docs.aurous-labs.com/api-reference/chat/tools Define callable tools and let the model decide when to invoke them. `POST /v1/chat/completions` accepts the standard OpenAI `tools` array. The model emits structured `tool_calls` in its response when it decides a tool is the right move; your code runs the tool and round-trips the result back as a `role: "tool"` message. ## Tool schema Each tool is a function declaration with a JSON-schema parameter object: ```jsonc theme={null} { "type": "function", "function": { "name": "get_weather", "description": "Return the current weather for a city.", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "City name, e.g. 'Berlin'." }, "unit": { "type": "string", "enum": ["c", "f"], "default": "c" } }, "required": ["city"] } } } ``` The `tool_choice` field controls how the model picks a tool: | Value | Behavior | | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `"auto"` (default if `tools` is set) | Model decides per turn whether to call a tool. | | `"none"` | Disable tool calls for this turn. | | `"required"` | Force the model to call at least one tool. Returns `400 tool_choice_required_unsupported` if the model lacks the capability. | | `{ "type": "function", "function": { "name": "..." } }` | Force a specific tool. | ## Single-turn example ```bash cURL theme={null} curl -X POST https://api.aurous-labs.com/v1/chat/completions \ -H "Authorization: Bearer $AUROUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "aurous-grow-2.0-pro", "messages": [ { "role": "user", "content": "What is the weather in Berlin?" } ], "tools": [ { "type": "function", "function": { "name": "get_weather", "description": "Return the current weather for a city.", "parameters": { "type": "object", "properties": { "city": { "type": "string" }, "unit": { "type": "string", "enum": ["c", "f"] } }, "required": ["city"] } } } ], "tool_choice": "auto", "max_tokens": 512 }' ``` ```typescript Node.js (OpenAI SDK) theme={null} import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.aurous-labs.com/v1", apiKey: process.env.AUROUS_API_KEY!, }); const tools = [ { type: "function" as const, function: { name: "get_weather", description: "Return the current weather for a city.", parameters: { type: "object", properties: { city: { type: "string" }, unit: { type: "string", enum: ["c", "f"] }, }, required: ["city"], }, }, }, ]; const first = await client.chat.completions.create({ model: "aurous-grow-2.0-pro", messages: [{ role: "user", content: "What is the weather in Berlin?" }], tools, tool_choice: "auto", max_tokens: 512, }); const call = first.choices[0].message.tool_calls?.[0]; console.log(call?.function.name, call?.function.arguments); // → "get_weather" '{"city":"Berlin","unit":"c"}' ``` ```python Python (OpenAI SDK) theme={null} from openai import OpenAI client = OpenAI( base_url="https://api.aurous-labs.com/v1", api_key="al_live_xxxxxxxxxxxxxxxx", ) tools = [ { "type": "function", "function": { "name": "get_weather", "description": "Return the current weather for a city.", "parameters": { "type": "object", "properties": { "city": {"type": "string"}, "unit": {"type": "string", "enum": ["c", "f"]}, }, "required": ["city"], }, }, } ] first = client.chat.completions.create( model="aurous-grow-2.0-pro", messages=[{"role": "user", "content": "What is the weather in Berlin?"}], tools=tools, tool_choice="auto", max_tokens=512, ) call = first.choices[0].message.tool_calls[0] print(call.function.name, call.function.arguments) # → "get_weather" '{"city":"Berlin","unit":"c"}' ``` ## Round-tripping the tool result After the model emits a tool call, run the tool locally, then re-call `chat.completions.create` with the original assistant message AND a new `role: "tool"` message carrying the result: ```typescript Node.js (OpenAI SDK) theme={null} // Step 1: model returned a tool_call. const assistantMessage = first.choices[0].message; const call = assistantMessage.tool_calls![0]; const args = JSON.parse(call.function.arguments); // Step 2: run the tool (your code). const weather = await fetchWeather(args.city, args.unit); // → { tempC: 14, condition: "cloudy" } // Step 3: send the tool result back as a follow-up turn. const followup = await client.chat.completions.create({ model: "aurous-grow-2.0-pro", messages: [ { role: "user", content: "What is the weather in Berlin?" }, assistantMessage, // contains tool_calls { role: "tool", tool_call_id: call.id, content: JSON.stringify(weather), }, ], max_tokens: 512, }); console.log(followup.choices[0].message.content); // → "It's 14°C and cloudy in Berlin right now." ``` The `tool_call_id` on the `role: "tool"` message must match the `id` field on the assistant's `tool_calls` entry. The model may emit multiple tool calls in a single turn — round-trip them all in one follow-up. ## Parallel tool calls If the model emits two or more tool calls in a single turn (`message.tool_calls.length > 1`), run them in parallel and send all results back in the next request: ```typescript theme={null} const calls = assistantMessage.tool_calls!; const results = await Promise.all( calls.map(async (c) => ({ role: "tool" as const, tool_call_id: c.id, content: JSON.stringify(await dispatchTool(c.function.name, c.function.arguments)), })), ); const final = await client.chat.completions.create({ model: "aurous-grow-2.0-pro", messages: [ { role: "user", content: "Compare Berlin and Tokyo weather." }, assistantMessage, ...results, // one tool message per call ], }); ``` ## Forcing a specific tool Pin the tool the model must call by name: ```jsonc theme={null} { "tools": [...], "tool_choice": { "type": "function", "function": { "name": "get_weather" } } } ``` Combined with `response_format`, this is the building block for "extract structured data from text" flows: define a tool whose parameters match the schema you want and force it. ## Streaming and tools When `stream: true`, tool-call arguments arrive as `delta.tool_calls[*].function.arguments` fragments that you must concatenate per `id` until you see `finish_reason: "tool_calls"`. The OpenAI Node and Python SDKs both expose `tool_calls` accumulators on their streaming helpers — prefer those over hand-parsing deltas. ## Errors * `tool_choice_required_unsupported` (400) — you passed `tool_choice: "required"` to a model whose `aurous_metadata.capabilities` doesn't include `tool_choice_required`. List models via [`GET /v1/models`](/api-reference/openapi#tag/models) to check capability flags before setting this. * `response_format_too_large` (400) — the JSON schema in `response_format` exceeds the platform's payload cap. Trim the schema (fewer properties, shorter descriptions) and retry. * `response_format_too_deep` (400) — the JSON schema nests deeper than the parser's cap. Flatten nested definitions or pull them into `$defs` references. # Create embeddings Source: https://docs.aurous-labs.com/api-reference/embeddings/create-embeddings /api-reference/openapi.json post /v1/embeddings Create embeddings from text and/or visual content (images, video). Multimodal input is combined into a SINGLE embedding (the underlying model concatenates parts into one document representation). For OpenAI-style N→N batch embedding, loop client-side: send one request per item. Returns the OpenAI-compatible envelope (`object: "list"`, `data: [{ embedding, index, object }]`, `model`, `usage`) plus the Aurous `usage` extension carrying `credits_charged` and a per-modality `breakdown` (`input: { text, visual, video }`) so you can correlate charge to input. `credits_charged` is authoritative. `Idempotency-Key` is always honored. # Output Dimensions Source: https://docs.aurous-labs.com/api-reference/embeddings/dimensions When the `dimensions` parameter is supported, when it's rejected, and the storage vs recall tradeoff. The OpenAI embeddings API accepts an optional `dimensions` parameter to truncate the returned vector. Aurous Labs accepts the parameter on the wire and validates it against each model's supported set (publishing `400 embeddings_unsupported_dimensions` for out-of-set values), but on v1.0 the parameter is **validated only** — the upstream model is dispatched without it, and the response vector is always the model's native dimension. True truncation will land in a future model version. Plan around the native dimension for now; we publish the supported set on each model row so your client can adapt without a redeploy when truncation ships. ## Check which dimensions a model supports Call [`GET /v1/models`](/api-reference/openapi#tag/models) and read `aurous_metadata.dimensions` for the model row. The field is an array of supported integers (e.g. `[1024, 2048]`) or `null` when the model returns a fixed shape with no truncation support. ```jsonc theme={null} // excerpt from GET /v1/models for an embedding model { "id": "aurous-embed-vision-1.0", "object": "model", "kind": "embedding", "aurous_metadata": { "context_window": 128000, "dimensions": [1024, 2048], // ← supported output dimensions "max_input_items": 100, "capabilities": ["multimodal_input"] } } ``` When `aurous_metadata.dimensions` is `null`, do not send the `dimensions` parameter — the response vector will be the model's native dimension. When the array is populated, you may send `dimensions` set to one of those values. On v1.0 the request will succeed (the value passes the whitelist gate) but the response vector will still be the model's native dimension — the parameter is reserved for the upcoming truncation rollout. Check `data[0].embedding.length` after the first successful call to see the actual shape. Picking a value NOT in the supported list returns `400 embeddings_unsupported_dimensions`. ## Why lower dimensions can be useful When a model supports multiple dimensions, the tradeoff is **storage cost vs retrieval quality**: * **Lower dimensions** — smaller vectors. Cheaper to store at index scale, faster cosine similarity (every comparison touches fewer floats), and a smaller transfer footprint between your service and your vector database. Slight recall loss vs the native dimension — usually a few percent on a clean evaluation set, sometimes negligible in practice. * **Higher dimensions** — richer semantic representation, best recall. The native dimension is what the model was trained to output, so it's the default for accuracy-sensitive workloads. The platform doesn't take a position on which to choose — that's a benchmark call against your own data. The recommendation is to start with the model's native dimension and only truncate after measuring the recall delta on your evaluation set. ## Example 1 — recommended call for `aurous-embed-vision-1.0` (no `dimensions`) The day-1 embedding model returns its native dimension whether or not you pass `dimensions`. Omit the parameter and read the shape off the response: ```bash cURL theme={null} curl -X POST https://api.aurous-labs.com/v1/embeddings \ -H "Authorization: Bearer $AUROUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "aurous-embed-vision-1.0", "input": "The quick brown fox jumps over the lazy dog." }' ``` ```typescript Node.js (OpenAI SDK) theme={null} import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.aurous-labs.com/v1", apiKey: process.env.AUROUS_API_KEY!, }); const res = await client.embeddings.create({ model: "aurous-embed-vision-1.0", input: "The quick brown fox jumps over the lazy dog.", }); console.log("native dim:", res.data[0].embedding.length); ``` ```python Python (OpenAI SDK) theme={null} from openai import OpenAI client = OpenAI( base_url="https://api.aurous-labs.com/v1", api_key="al_live_xxxxxxxxxxxxxxxx", ) res = client.embeddings.create( model="aurous-embed-vision-1.0", input="The quick brown fox jumps over the lazy dog.", ) print("native dim:", len(res.data[0].embedding)) ``` ## Example 2 — what happens when you pass an unsupported value Sending `dimensions: 512` to `aurous-embed-vision-1.0` (whose supported set is `[1024, 2048]`): ```bash theme={null} curl -X POST https://api.aurous-labs.com/v1/embeddings \ -H "Authorization: Bearer $AUROUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "aurous-embed-vision-1.0", "input": "Hello world.", "dimensions": 512 }' ``` ```http theme={null} HTTP/1.1 400 Bad Request Aurous-Request-Id: req_01HXMQ7Z3K8Y2VNABCDEFGHJKM Content-Type: application/json { "error": { "type": "invalid_request", "code": "embeddings_unsupported_dimensions", "message": "Model aurous-embed-vision-1.0 only accepts 1024, 2048 for dimensions; received 512.", "doc_url": "https://docs.aurous-labs.com/errors#embeddings_unsupported_dimensions", "request_id": "req_01HXMQ7Z3K8Y2VNABCDEFGHJKM" } } ``` No credits are charged for a request rejected at the DTO boundary — the `400` happens before the model is called, so there's nothing to refund. Fix the request and retry. ## Forward compatibility `aurous_metadata.dimensions` is the source of truth. When a future model with a different supported set ships, its row will list the supported values and the validator picks them up automatically. The error code stays the same; only the supported set changes. Read the metadata at request time (or cache it with a short TTL) so your client adapts without a redeploy. ## Related * [Overview](/api-reference/embeddings/overview) — embeddings surface and quick start. * [Multimodal](/api-reference/embeddings/multimodal) — text + image + video parts in one request. * [Pricing](/api-reference/embeddings/pricing) — credit math and worked examples. * [Estimate](/api-reference/embeddings/estimate) — preview cost without charging. # Estimate Cost Before Embedding Source: https://docs.aurous-labs.com/api-reference/embeddings/estimate Preview the credit charge for an embedding request without dispatching the model or deducting credits. `POST /v1/embeddings/estimate` takes the **same payload** as `POST /v1/embeddings` and returns the projected token counts and credit charge — without calling the model and without billing your team. Use it to: * Show a per-item cost in your UI before a customer clicks "Embed". * Project the total cost of a batch indexing job before kicking it off. * Budget-gate a workload: skip items the estimate exceeds your per-call cap. * Sanity-check rate-card changes against your own corpus. ## Request shape The body is identical to `POST /v1/embeddings` minus the two fields that are billing-related and meaningless when no charge is made: | Field | Status on `/embeddings` | Status on `/embeddings/estimate` | | ----------------- | ---------------------------------------- | ------------------------------------------- | | `model` | required | required | | `input` | required (string or content-parts array) | required | | `dimensions` | optional | optional | | `encoding_format` | optional | **omitted** — not relevant for an estimate | | `user` | optional | **omitted** — not stored on an estimate row | All the input validation that runs on `POST /v1/embeddings` runs here too: same DTO, same caps (16 parts max, 8 image\_url max, 1M chars per text part, 128K-token context window). `video_url` parts are rejected with [`embeddings_video_unsupported`](/api-reference/errors/embeddings_video_unsupported) on both endpoints as of 2026-05-24. A malformed payload returns the same typed error code an estimate would have generated on the live endpoint. ## Quick start ```bash cURL theme={null} curl -X POST https://api.aurous-labs.com/v1/embeddings/estimate \ -H "Authorization: Bearer $AUROUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "aurous-embed-vision-1.0", "input": "A 500-token product description for a leather messenger bag." }' ``` ```typescript Node.js (fetch) theme={null} const res = await fetch("https://api.aurous-labs.com/v1/embeddings/estimate", { method: "POST", headers: { Authorization: `Bearer ${process.env.AUROUS_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: "aurous-embed-vision-1.0", input: "A 500-token product description for a leather messenger bag.", }), }); const estimate = await res.json(); console.log("would charge:", estimate.credits_estimated, "credits"); console.log("breakdown:", estimate.breakdown.input); ``` ```python Python (requests) theme={null} import os, requests res = requests.post( "https://api.aurous-labs.com/v1/embeddings/estimate", headers={ "Authorization": f"Bearer {os.environ['AUROUS_API_KEY']}", "Content-Type": "application/json", }, json={ "model": "aurous-embed-vision-1.0", "input": "A 500-token product description for a leather messenger bag.", }, ) estimate = res.json() print("would charge:", estimate["credits_estimated"], "credits") print("breakdown:", estimate["breakdown"]["input"]) ``` The OpenAI SDKs don't ship an estimate helper, so call the endpoint directly with `fetch` / `requests` / `axios`. ## Response shape ```jsonc theme={null} { "estimated": true, "tokens": { "text": 500, "image": 0, "video": 0, "total": 500 }, "credits_estimated": 0.009375, "breakdown": { "input": { "text": 0.009375, "visual": 0, "video": 0 }, "model": "aurous-embed-vision-1.0" } } ``` Fields: | Field | Notes | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `estimated` | Literal `true`. Distinguishes the estimate envelope from a real embedding response (which uses `object: "list"`). | | `tokens.text` | Estimated text-token count after tokenization. | | `tokens.image` | Estimated visual-token contribution from `image_url` parts. | | `tokens.video` | Deprecated 2026-05-24 — always `0`. Retained on the response shape for one release cycle so existing SDKs that read it don't break; `video_url` parts now return `embeddings_video_unsupported`. | | `tokens.total` | Sum of the three modalities — what would surface as `usage.prompt_tokens` on the live call. | | `credits_estimated` | Projected total credits. Matches the per-modality math walked in [Pricing](/api-reference/embeddings/pricing#how-cost-is-computed). | | `breakdown.input.{text,visual,video}` | Per-modality credit decomposition, identical shape to `usage.breakdown.input` on the live call. | | `breakdown.model` | Echoes the requested model slug, inside the breakdown block. | ## Multimodal estimate The estimate endpoint accepts content-parts arrays the same way `/v1/embeddings` does: ```bash theme={null} curl -X POST https://api.aurous-labs.com/v1/embeddings/estimate \ -H "Authorization: Bearer $AUROUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "aurous-embed-vision-1.0", "input": [ { "type": "text", "text": "Product photo of a vintage leather messenger bag with brass buckles." }, { "type": "image_url", "image_url": { "url": "https://assets.aurous-labs.com/example-images/messenger-bag.jpg" } } ] }' ``` The response carries the same envelope; the `tokens.image` and `breakdown.input.visual` values are populated, and `credits_estimated` reflects the multimodal total. ## Estimates are an upper bound Estimates are computed from a pre-fetch tokenization pass. The real charge on `POST /v1/embeddings` may differ by a few percent for **URL-fetched media** — the platform fetches and re-tokenizes images and videos at request time, and the provider's actual token count can come back slightly above or below the pre-fetch estimate. Plain-string text input is exact. In practice: * Budgeting a batch — treat the sum of estimates as your worst case. * Per-request cost UI — render the estimate; show the actual `credits_charged` on success. * Reconciliation — compare your accumulated estimates vs the same period's `GET /v1/usage`; small drift is expected and benign. ## No row, no hold, no billing event A successful estimate call: * Does **not** create an embedding row. * Does **not** place a hold on your balance. * Does **not** emit a billing event. * Counts only against your **RPM** rate-limit bucket. It does **not** consume from your **TPM** bucket. A 4xx from the estimate endpoint behaves like any other validation 4xx — same typed error envelope, no row, no charge. ## Errors Estimates run all the same input checks as the live endpoint, so the same codes apply: * `embeddings_batch_not_supported` (400) — `input` is a `string[]` array. See [Multimodal](/api-reference/embeddings/multimodal#batch-rejection). * `embeddings_input_too_many_items` (400) — over the 16-part or 8-image cap. Split into multiple requests. * `embeddings_video_unsupported` (400) — any `video_url` part is rejected. Extract a representative frame in your pipeline and submit it as `image_url` (bills at the visual rate). * `embeddings_input_too_large` (400) — pre-fetch token estimate is over the context window. Trim input. * `embeddings_unsupported_dimensions` (400) — the requested `dimensions` is not supported. See [Dimensions](/api-reference/embeddings/dimensions). * `model_not_found` (404) — unknown `model` slug. List models with [`GET /v1/models`](/api-reference/openapi#tag/models). * `model_disabled` (403) — model exists but admin has deactivated it. * `model_wrong_kind` (400) — you sent a chat model to the embeddings estimate endpoint. Server errors (5xx) on the estimate endpoint are platform issues — retry with backoff. They do not consume rate-limit budget. ## Related * [Overview](/api-reference/embeddings/overview) — the live `POST /v1/embeddings` endpoint. * [Pricing](/api-reference/embeddings/pricing) — the credit math the estimate is based on. * [Multimodal](/api-reference/embeddings/multimodal) — input shape for mixed text + image + video. * [Errors](/errors) — full error taxonomy. # Estimate embedding credits Source: https://docs.aurous-labs.com/api-reference/embeddings/estimate-embedding-credits /api-reference/openapi.json post /v1/embeddings/estimate Estimate credits + per-modality breakdown WITHOUT dispatching. Use this BEFORE a real POST /v1/embeddings to preview cost. Same DTO shape as POST /v1/embeddings minus `encoding_format` and `user` (irrelevant when no charge is made). Estimates are upper bounds based on pre-fetch input; actual `credits_charged` from POST /v1/embeddings may differ slightly for URL-fetched media (image / video bytes whose server-side tokenization can be more or less aggressive than the local estimator). # Embedding limits Source: https://docs.aurous-labs.com/api-reference/embeddings/limits Caps on parts, characters, URLs, and request size for POST /v1/embeddings. `POST /v1/embeddings` enforces a small set of input caps to keep latency predictable and prevent abuse. Hitting a cap returns a `400 invalid_request` with a specific error code; this page enumerates each cap, its trigger, and the error code you'll see. ## Caps at a glance | Limit | Cap | Error code | | ---------------------------------------------- | ------------------ | -------------------------------------------- | | Total content parts per request | **16** | `embeddings_input_too_many_items` | | Image parts per request | **8** | `embeddings_input_too_many_items` | | Video parts per request | **0** (rejected) | `embeddings_video_unsupported` | | Text characters per text part | **1,000,000** (1M) | DTO `invalid_request` (max-length validator) | | URL length (image\_url) | **2,048 chars** | DTO `invalid_request` (max-length validator) | | Aggregate input tokens (text + visual + video) | **128K** | `embeddings_input_too_large` | | `string[]` batch input | Not accepted | `embeddings_batch_not_supported` | The model context window is 128K input tokens. The character caps above are guardrails on individual parts; the aggregate token count is the real constraint. ## Total content parts (≤ 16) `input` accepts either a string (`input: "..."`) or an array of content parts (`input: [{type:"text", text:"..."}, {type:"image_url", image_url:{url:"..."}}, ...]`). The array form is capped at **16 total parts** across all types. ```bash theme={null} # 16 text parts — accepted curl -X POST https://api.aurous-labs.com/v1/embeddings \ -H "X-Api-Key: $AUROUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "aurous-embed-vision-1.0", "input": [ {"type": "text", "text": "part 1"}, {"type": "text", "text": "part 2"}, ... (14 more) ... ] }' # 17+ parts — rejected { "error": { "type": "invalid_request", "code": "embeddings_input_too_many_items", "message": "Embedding request has 17 content parts; the cap is 16 total parts per request. See https://docs.aurous-labs.com/api-reference/embeddings/limits.", "doc_url": "https://docs.aurous-labs.com/errors#embeddings_input_too_many_items" } } ``` ## Image parts (≤ 8) Within the 16-part total, **image parts** are capped at 8. The cap exists because each image contributes \~1,000-1,500 visual tokens to the input — packing more than 8 images in one request risks blowing the 128K context window mid-request, which surfaces as a noisy `embeddings_input_too_large` rather than a clean image-cap error. ```bash theme={null} # 8 images — accepted { "model": "aurous-embed-vision-1.0", "input": [ {"type": "image_url", "image_url": {"url": "https://example.com/1.jpg"}}, ... (7 more) ... ] } # 9 images — rejected with embeddings_input_too_many_items ``` If you need to embed 16 images, send two requests of 8 each (loop pattern). See [OpenAI batch incompat](/api-reference/embeddings/openai-batch-incompat) for the loop guidance. ## Video parts (rejected, 2026-05-24) `video_url` parts are no longer accepted on the v1 embeddings surface. Submitting one returns [`embeddings_video_unsupported`](/api-reference/errors/embeddings_video_unsupported). The provider folds video frames into the visual billing bucket — the previously published video rate never actually fired — so we removed the input shape entirely. Extract a representative frame in your pipeline and submit it as `image_url`; it bills at the visual rate. ```json theme={null} // Any video_url part — rejected { "error": { "code": "embeddings_video_unsupported", "message": "Video input is not supported on v1 embeddings. Submit text or image_url parts; the visual rate applies to image inputs." } } ``` ## Per-part text length (≤ 1,000,000 chars) Each text part is capped at 1,000,000 characters. This is a guardrail against runaway input; in practice the 128K-token aggregate kicks in first for English text (\~4 chars/token average → \~512K chars max), but the per-part cap exists to bound a single malformed part. Exceeding 1,000,000 characters returns a DTO-level `400 invalid_request` from the validation layer (not a specific `embeddings_*` code) — the request never reaches the embedding service. ## URL length (≤ 2,048 chars) `image_url.url` and `video_url.url` are capped at 2,048 characters. URLs longer than that are rejected at the DTO layer with `400 invalid_request`. The error message currently surfaces the generic content-parts validator hint rather than naming the URL-length cap specifically; a more pointed error message is tracked for v1.0.x. This is a sane guard against signed URLs with arbitrarily-long query strings; production CDN URLs are typically \<1,000 chars and rarely come close. ## Aggregate input tokens (≤ 128K) The hard limit is the model's context window. Sum the tokens across all parts: * Text: count via the provider tokenizer (see [how-we-count-tokens](/guides/how-we-count-tokens)) * Image: \~1,000-1,500 visual tokens per typical image * Video: \~100-200 visual tokens per second of video If the sum exceeds 128K, the call returns `embeddings_input_too_large`: ```json theme={null} { "error": { "type": "invalid_request", "code": "embeddings_input_too_large", "message": "Embedding input is approximately 147,392 tokens, exceeding the 128,000-token context window of aurous-embed-vision-1.0. Trim text parts, reduce image count, or use a shorter video clip. See https://docs.aurous-labs.com/api-reference/embeddings/limits.", "doc_url": "https://docs.aurous-labs.com/errors#embeddings_input_too_large" } } ``` To preview the count without billing, use [`POST /v1/embeddings/estimate`](/api-reference/embeddings/estimate) — same body, returns the token breakdown + the credit charge. ## `string[]` batch input OpenAI accepts `input: ["a", "b", "c"]` as a batched N-vector return. **Aurous does NOT.** See [OpenAI batch incompat](/api-reference/embeddings/openai-batch-incompat) for the rationale and workaround. ## Server-side URL fetching constraints `image_url.url` and `video_url.url` are fetched server-side at request time. Several schemes / hosts are blocked: * **HTTPS-only** — `http://`, `ftp://`, `data:`, `file://`, `gopher://` all rejected * **RFC1918** addresses (10.x, 172.16-31.x, 192.168.x) blocked * **Loopback** (127.0.0.1, ::1) blocked * **Link-local** (169.254.x, fe80::/10) blocked * **Cloud metadata** (169.254.169.254, metadata.google.internal) blocked See [URL fetching](/api-reference/embeddings/url-fetching) for the full set of guards and what happens when a URL 404s or times out (10-second fetch timeout). ## Where to next? * [Multimodal embeddings](/api-reference/embeddings/multimodal) — the full content-parts surface * [URL fetching](/api-reference/embeddings/url-fetching) — what happens server-side when we fetch an image/video URL * [`POST /v1/embeddings/estimate`](/api-reference/embeddings/estimate) — preview cost + token counts * [How we count tokens](/guides/how-we-count-tokens) — per-modality tokenization details * [Error codes](/errors) — full taxonomy # Multimodal embeddings Source: https://docs.aurous-labs.com/api-reference/embeddings/multimodal Combine text and images into a single embedding that represents the whole document. `aurous-embed-vision-1.0` is a multimodal embedding model: a single request that mixes text and image parts produces **one** embedding representing both together. This is the distinguishing feature of the embeddings surface — you get a single vector that captures the semantic relationship between text and visual content in the same document, not separate vectors per modality. **Video input is no longer accepted as of 2026-05-24.** `video_url` parts return [`embeddings_video_unsupported`](/api-reference/errors/embeddings_video_unsupported). The provider folded video frames into the visual billing bucket — the published video rate never actually fired. Extract a representative frame in your pipeline and submit it as `image_url`; it bills at the visual rate. ## Input shape `input` accepts two shapes: 1. **A plain string** — text-only embedding. The simplest form. 2. **An array of content parts** — multimodal embedding. Mix `text` and `image_url` parts in one request; the model concatenates them into a single combined document and returns one embedding for the whole thing. ```jsonc theme={null} { "model": "aurous-embed-vision-1.0", "input": [ { "type": "text", "text": "Product photo of a leather messenger bag." }, { "type": "image_url", "image_url": { "url": "https://assets.aurous-labs.com/example-images/messenger-bag.jpg" } } ] } ``` Supported part types: | `type` | Payload | Notes | | ----------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `text` | `{ "text": "..." }` | UTF-8 text. NULL bytes are rejected. Max 1,000,000 characters per part. | | `image_url` | `{ "image_url": { "url": "https://..." } }` | HTTPS URL, ≤ 2048 chars. Must be fetchable in under 10s. | | `video_url` | — | **Rejected as of 2026-05-24.** Returns [`embeddings_video_unsupported`](/api-reference/errors/embeddings_video_unsupported). | ## One request → one embedding The fundamental difference from OpenAI's embedding API is this: **the v1 surface returns exactly one embedding per request, regardless of how many parts the input array contains**. When you pass a content-parts array, the model treats the parts as one ordered document and produces a single vector representing the whole thing. This is intentional — it lets you embed text + an image together so the resulting vector captures their joint meaning (a product description fused with the photo, a chart caption fused with the chart image). ```jsonc theme={null} // Response is always a single-element data array on v1 { "object": "list", "data": [ { "index": 0, "object": "embedding", "embedding": [/* 2048 floats */] } ], "model": "aurous-embed-vision-1.0", "usage": { /* ... */ } } ``` ## Batch rejection — the `string[]` shape is NOT accepted on v1 OpenAI's API accepts `input: string[]` and returns one embedding per string (N→N). Aurous Labs **rejects** that shape on v1 because the underlying model would concatenate the strings into a single document and return one combined vector — the opposite of what an OpenAI-trained customer would expect. Silently swapping semantics would cause subtle bugs in production code (your "100 documents embedded" call would return 1 unusable embedding). The platform returns `400 embeddings_batch_not_supported` whenever `input` is an array of pure strings. Two workarounds: ### Option 1 — loop client-side Send one request per item. This is the equivalent of OpenAI's N→N batch semantics. Use `Promise.all` (Node) or `asyncio.gather` (Python) to parallelize. ```typescript Node.js theme={null} const documents = [ "The quick brown fox jumps over the lazy dog.", "Pack my box with five dozen liquor jugs.", "Sphinx of black quartz, judge my vow.", ]; const results = await Promise.all( documents.map((text) => client.embeddings.create({ model: "aurous-embed-vision-1.0", input: text, }), ), ); const vectors = results.map((r) => r.data[0].embedding); ``` ```python Python theme={null} import asyncio from openai import AsyncOpenAI client = AsyncOpenAI( base_url="https://api.aurous-labs.com/v1", api_key="al_live_xxxxxxxxxxxxxxxx", ) documents = [ "The quick brown fox jumps over the lazy dog.", "Pack my box with five dozen liquor jugs.", "Sphinx of black quartz, judge my vow.", ] async def embed_all() -> list[list[float]]: results = await asyncio.gather(*[ client.embeddings.create(model="aurous-embed-vision-1.0", input=text) for text in documents ]) return [r.data[0].embedding for r in results] vectors = asyncio.run(embed_all()) ``` ### Option 2 — pass content parts for a deliberately combined embedding If you actually want one embedding representing several text fragments fused together (e.g., a title + description + tags as one document), pass them as content parts: ```jsonc theme={null} { "model": "aurous-embed-vision-1.0", "input": [ { "type": "text", "text": "Title: Leather Messenger Bag" }, { "type": "text", "text": "Description: Hand-stitched full-grain leather." }, { "type": "text", "text": "Tags: bag, leather, messenger, full-grain" } ] } ``` This is intentional, semantically meaningful, and returns one embedding for the combined document. It's NOT the same as embedding the three strings independently — the combined vector is a single point in vector space representing all three together. ## Worked example — text + image A typical RAG-for-images use case: embed a product photo with its description, store the vector, search later with a user's natural-language query. ```bash cURL theme={null} curl -X POST https://api.aurous-labs.com/v1/embeddings \ -H "Authorization: Bearer $AUROUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "aurous-embed-vision-1.0", "input": [ { "type": "text", "text": "Product photo of a vintage leather messenger bag with brass buckles." }, { "type": "image_url", "image_url": { "url": "https://assets.aurous-labs.com/example-images/messenger-bag.jpg" } } ] }' ``` ```typescript Node.js (OpenAI SDK) theme={null} import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.aurous-labs.com/v1", apiKey: process.env.AUROUS_API_KEY!, }); const res = await client.embeddings.create({ model: "aurous-embed-vision-1.0", input: [ { type: "text", text: "Product photo of a vintage leather messenger bag with brass buckles.", }, { type: "image_url", image_url: { url: "https://assets.aurous-labs.com/example-images/messenger-bag.jpg", }, }, ] as never, // OpenAI's typings predate multimodal embeddings; the wire shape is forwarded as-is. }); console.log("vector dim:", res.data[0].embedding.length); console.log("credits_charged:", res.usage.credits_charged); console.log("breakdown:", res.usage.breakdown.input); // → { text: 0.000487, visual: 0.04992, video: 0 } ``` ```python Python (OpenAI SDK) theme={null} from openai import OpenAI client = OpenAI( base_url="https://api.aurous-labs.com/v1", api_key="al_live_xxxxxxxxxxxxxxxx", ) res = client.embeddings.create( model="aurous-embed-vision-1.0", input=[ { "type": "text", "text": "Product photo of a vintage leather messenger bag with brass buckles.", }, { "type": "image_url", "image_url": { "url": "https://assets.aurous-labs.com/example-images/messenger-bag.jpg", }, }, ], ) print("vector dim:", len(res.data[0].embedding)) print("credits_charged:", res.usage.credits_charged) print("breakdown:", res.usage.breakdown.input) ``` Example response: ```jsonc theme={null} { "object": "list", "data": [ { "index": 0, "object": "embedding", "embedding": [/* 2048 floats */] } ], "model": "aurous-embed-vision-1.0", "usage": { "prompt_tokens": 1050, "total_tokens": 1050, "credits_charged": 0.050407, "breakdown": { "input": { "text": 0.000487, "visual": 0.049920, "video": 0 }, "model": "aurous-embed-vision-1.0" } } } ``` The `breakdown.input.text` and `breakdown.input.visual` fields decompose the charge across modalities so you can attribute cost to inputs. See [Pricing](/api-reference/embeddings/pricing) for the per-1K credit math. ## Image-URL requirements * **HTTPS only.** Plain HTTP and `data:` URIs are rejected. * **≤ 2048 characters** per URL string. * **Fetchable in under 10 seconds.** Long-running fetches are treated as failed requests. * **Public reachability.** The platform fetches the URL from a server-side IP, so private hosts (localhost, RFC 1918 ranges, internal VPC) are not accessible. If you have local images that aren't on a public CDN, host them on your own (S3, Cloudflare R2, etc.) and pass the URL. The embeddings surface does not currently accept inline base64 data URIs on v1. ## Limits | Limit | Cap | Code on violation | | --------------------------------------- | ---------------------- | ---------------------------------- | | Total content parts per request | 16 | `embeddings_input_too_many_items` | | `image_url` parts per request | 8 | `embeddings_input_too_many_items` | | `video_url` parts per request | 0 (any video → reject) | `embeddings_video_unsupported` | | Total input tokens (after tokenization) | 128,000 | `embeddings_input_too_large` | | URL string length | 2048 chars | `invalid_request` (DTO validation) | | Text part character length | 1,000,000 chars | `invalid_request` (DTO validation) | If you need to embed more than 8 images or more than 1 video, split the work into multiple requests — the vectors will land in your index independently. There is no "fan-out" mode that combines more images into a single embedding on v1. ## Errors * `embeddings_batch_not_supported` (400) — `input` was an array of pure strings. Loop client-side or pass content parts. See [batch rejection](#batch-rejection). * `embeddings_input_too_many_items` (400) — over the 16-part or 8-image cap. Split into multiple requests. * `embeddings_video_unsupported` (400) — any `video_url` part is rejected. Extract a representative frame in your pipeline and submit it as `image_url` (bills at the visual rate). * `embeddings_input_too_large` (400) — pre-fetch tokenization estimates over the context window. Trim input or skip the largest part. * `embeddings_provider_unknown_error` (502) — upstream returned an error the platform's mapping table doesn't yet recognize. Retry with backoff; quote the `request_id`. See [Errors](/errors) for the full taxonomy and recovery guidance. # OpenAI batch input is not supported Source: https://docs.aurous-labs.com/api-reference/embeddings/openai-batch-incompat Why POST /v1/embeddings rejects input: ["a","b","c"] and the recommended workaround for callers coming from OpenAI. OpenAI's [`embeddings.create`](https://platform.openai.com/docs/api-reference/embeddings/create) accepts `input` as a string array, and the response returns one vector per input string (N → N). Many tutorials assume this shape: ```python theme={null} # OpenAI shape — N strings → N vectors res = openai.embeddings.create( model="text-embedding-3-large", input=["alpha", "beta", "gamma"], ) print(len(res.data)) # 3 ``` Pointing that exact call at Aurous Labs returns: ```json theme={null} { "error": { "type": "invalid_request", "code": "embeddings_batch_not_supported", "message": "POST /v1/embeddings does not accept string[] batch input. The underlying multimodal model concatenates batched text into a single combined vector — opposite of OpenAI's N→N semantics. Loop client-side or use content-parts input. See https://docs.aurous-labs.com/api-reference/embeddings/openai-batch-incompat.", "doc_url": "https://docs.aurous-labs.com/errors#embeddings_batch_not_supported", "request_id": "req_..." } } ``` This page exists because the API behavior is intentional and the workaround is one-liner short. ## Why we can't just accept it The Aurous embedding model is **multimodal** (text + image + video → one combined vector) and is published as a single-output model. If we accepted `input: ["a", "b", "c"]` and silently returned the OpenAI-shaped N=3 vector response, we'd be running the multimodal model in a mode that concatenates the three strings into a single combined vector and returning that one vector three times — wrong answer, hard to detect at debug time. The two valid customer intents behind `input: string[]` map to different APIs: | Intent | Aurous shape | | ----------------------------------------------------------- | --------------------------------------------------------------------------- | | "Embed each string as a separate vector, return a list." | Loop client-side, one request per string. | | "Embed all three pieces together, return one combined vec." | Send the three pieces as `content-parts` — text+text+text — in one request. | The 400 error makes the disambiguation explicit so we don't silently choose the wrong one for you. ## Workaround #1 — loop client-side (one vector per string) This matches OpenAI's N→N semantics. Most code wants this. ```python Python theme={null} from openai import OpenAI client = OpenAI( base_url="https://api.aurous-labs.com/v1", api_key="al_live_xxxxxxxxxxxxxxxx", ) inputs = ["alpha", "beta", "gamma"] vectors = [] for text in inputs: res = client.embeddings.create(model="aurous-embed-vision-1.0", input=text) vectors.append(res.data[0].embedding) print(len(vectors), len(vectors[0])) # 3, 2048 ``` ```typescript Node.js theme={null} import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.aurous-labs.com/v1", apiKey: process.env.AUROUS_API_KEY!, }); const inputs = ["alpha", "beta", "gamma"]; const vectors: number[][] = []; for (const text of inputs) { const res = await client.embeddings.create({ model: "aurous-embed-vision-1.0", input: text, }); vectors.push(res.data[0].embedding); } console.log(vectors.length, vectors[0].length); // 3 2048 ``` ```python Python — parallel (faster) theme={null} import asyncio from openai import AsyncOpenAI client = AsyncOpenAI( base_url="https://api.aurous-labs.com/v1", api_key="al_live_xxxxxxxxxxxxxxxx", ) async def main(): inputs = ["alpha", "beta", "gamma"] tasks = [ client.embeddings.create(model="aurous-embed-vision-1.0", input=text) for text in inputs ] results = await asyncio.gather(*tasks) return [r.data[0].embedding for r in results] vectors = asyncio.run(main()) print(len(vectors), len(vectors[0])) # 3, 2048 ``` Throughput tip: parallelize the loop with `asyncio.gather` (Python) or `Promise.all` (Node). The per-team rate-limit bucket is shared across all your concurrent embedding requests — see [Rate limits](/rate-limits) — so the wall-clock cost of N parallel embeddings is roughly `max(N×per-request-latency / concurrency, total-tokens / TPM)`. ## Workaround #2 — content-parts (one combined vector) If your intent is to embed multiple pieces of related context (a document chunk plus a caption, say) and end up with a single semantic vector, use the content-parts array form. This stays within Aurous's native single-vector contract. ```bash cURL theme={null} curl -X POST https://api.aurous-labs.com/v1/embeddings \ -H "X-Api-Key: $AUROUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "aurous-embed-vision-1.0", "input": [ { "type": "text", "text": "Photo of a golden retriever in a park" }, { "type": "text", "text": "Setting: sunset, soft directional light" } ] }' ``` The OpenAI SDK's typed `embeddings.create` does not accept the content-parts array. Use the lower-level `client.post()` escape hatch: ```python Python theme={null} res = client.post( "/embeddings", body={ "model": "aurous-embed-vision-1.0", "input": [ {"type": "text", "text": "Photo of a golden retriever in a park"}, {"type": "text", "text": "Setting: sunset, soft directional light"}, ], }, cast_to=dict, ) print(len(res["data"][0]["embedding"])) # 2048 ``` ```typescript Node.js theme={null} const res = (await client.post("/embeddings", { body: { model: "aurous-embed-vision-1.0", input: [ { type: "text", text: "Photo of a golden retriever in a park" }, { type: "text", text: "Setting: sunset, soft directional light" }, ], }, })) as { data: { embedding: number[] }[] }; console.log(res.data[0].embedding.length); // 2048 ``` See [Multimodal embeddings](/api-reference/embeddings/multimodal) for the full content-parts taxonomy (text + image + video) and the per-modality rate math. ## Why not a server-side auto-promote shim? We considered transparently converting `input: ["a", "b", "c"]` to one of the workarounds server-side. We chose not to: * **Auto-loop on the server** — would silently translate OpenAI's N→N intent into N separate billed requests under the hood, defeating the cost-transparency story (`credits_charged` would report a per-request amount that doesn't match the single-request the customer thinks they sent). * **Auto-content-parts** — would silently translate into one combined vector, which is the opposite of what most callers want when they paste an OpenAI tutorial. Both options would do something different from what the caller intended \~half the time. The current 400 with a pointed `doc_url` is the least-surprising path. We may revisit this with an explicit opt-in (e.g. `encoding_format: "openai_batch"`) once we have telemetry on real customer patterns. ## Where to next? * [Multimodal embeddings](/api-reference/embeddings/multimodal) — the full content-parts surface * [Embedding limits](/api-reference/embeddings/limits) — caps on parts, characters, URLs * [Embedding pricing](/api-reference/embeddings/pricing) — per-modality credit rates * [Embedding estimate](/api-reference/embeddings/estimate) — preview cost without charging # Embeddings Source: https://docs.aurous-labs.com/api-reference/embeddings/overview Vector embeddings for text and multimodal content over the Aurous Labs API. OpenAI-compatible, multimodal-native. `POST /v1/embeddings` produces a vector representation of your input that you can store, index, and use for semantic search, retrieval-augmented generation (RAG), classification, and similarity ranking. The surface is OpenAI-shaped — if your code already talks to `embeddings.create`, point the SDK at Aurous Labs by changing two lines: the `baseURL` and the API key. ```text theme={null} baseURL: https://api.aurous-labs.com/v1 apiKey: al_live_ ``` Pass the key either as `Authorization: Bearer al_live_...` (what OpenAI SDKs send by default) or as `X-Api-Key: al_live_...`. Both are accepted. ## When to use embeddings Embeddings convert input into a fixed-length float vector. Use them when you need to compare meaning rather than generate text: * **Semantic search** — embed documents at index time, embed the user's query at search time, return the top-K nearest documents by cosine similarity. * **Retrieval-augmented generation (RAG)** — pull semantically-relevant chunks from your knowledge base, then pass them into a chat completion as context. * **Classification** — embed labeled examples once, then embed new inputs and route based on nearest-neighbor label. * **Similarity ranking** — deduplicate, cluster, or surface "more like this" recommendations. ## When NOT to use embeddings * **Generating text** — embeddings don't produce text; use [chat completions](/api-reference/chat/overview) (`POST /v1/chat/completions`). * **Generating images** — use [image generation](/api-reference/openapi#tag/images) (`POST /v1/images`). * **N→N batch embedding** — v1 does not accept `string[]` batch input (the underlying model concatenates batched text into a single combined vector, opposite of OpenAI's N→N semantics). Loop client-side to embed multiple items independently. See [Multimodal](/api-reference/embeddings/multimodal) for the rationale and the workaround. ## Models List available models at [`GET /v1/models`](/api-reference/openapi#tag/models). The day-1 embedding model is **`aurous-embed-vision-1.0`** — a multimodal embedding model with a 128K context window that accepts text, image, and video parts and returns a single combined vector. Each model row has a top-level `kind` field that is either `chat` or `embedding`; sending an embedding model to `/v1/chat/completions` (or a chat model here) returns `400 model_wrong_kind`. Context window, supported output dimensions, capabilities, and per-modality credit rates are returned on the `aurous_metadata` extension of each model row. ## Pricing Embeddings are billed per token at credit rates surfaced on each model row at [`GET /v1/models`](/api-reference/openapi#tag/models) (the `embedding_pricing` block) and in the `usage` block of every embedding response. The rate has three axes: **text-token input**, **visual-token input** (images), and **video-token input**. The rate on `/v1/models` is the caller's **effective rate** — any per-team override is already applied. See [Pricing](/api-reference/embeddings/pricing) for the credit math, worked examples, and the rate-mutability rules. To preview cost without charging, send the same payload to [`POST /v1/embeddings/estimate`](/api-reference/embeddings/estimate). Estimates are an upper bound — the real `credits_charged` may differ a few percent for URL-fetched media. ## Quick start ```bash cURL theme={null} curl -X POST https://api.aurous-labs.com/v1/embeddings \ -H "Authorization: Bearer $AUROUS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "model": "aurous-embed-vision-1.0", "input": "The quick brown fox jumps over the lazy dog." }' ``` ```typescript Node.js (OpenAI SDK) theme={null} import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.aurous-labs.com/v1", apiKey: process.env.AUROUS_API_KEY!, // al_live_xxxxxxxxxxxxxxxx }); const res = await client.embeddings.create({ model: "aurous-embed-vision-1.0", input: "The quick brown fox jumps over the lazy dog.", }); console.log(res.data[0].embedding.length); // → e.g. 2048 // usage carries credits_charged + per-modality breakdown console.log(res.usage); ``` ```python Python (OpenAI SDK) theme={null} from openai import OpenAI client = OpenAI( base_url="https://api.aurous-labs.com/v1", api_key="al_live_xxxxxxxxxxxxxxxx", # or read from env ) res = client.embeddings.create( model="aurous-embed-vision-1.0", input="The quick brown fox jumps over the lazy dog.", ) print(len(res.data[0].embedding)) # → e.g. 2048 print(res.usage) # includes credits_charged ``` ## Response shape Responses are standard OpenAI shape with an Aurous extension on `usage`: ```jsonc theme={null} { "object": "list", "data": [ { "index": 0, "object": "embedding", "embedding": [0.0123, -0.0456, 0.0789, /* ... */] } ], "model": "aurous-embed-vision-1.0", "usage": { "prompt_tokens": 12, "total_tokens": 12, "credits_charged": 0.000225, "breakdown": { "input": { "text": 0.000225, "visual": 0, "video": 0 }, "model": "aurous-embed-vision-1.0" } } } ``` The `usage.credits_charged` value is the exact amount deducted from your team's balance. The `usage.breakdown.input` block decomposes the charge across the three input modalities so you can correlate cost to input shape — `text` and `visual` and `video` are always present, with `0` for any modality not used in the request. `data` is always a single-element array on v1 (one combined embedding per request). See [Multimodal](/api-reference/embeddings/multimodal) for the rationale. ## Idempotency Pass `Idempotency-Key` (any opaque value, 1–256 chars; UUID v4 recommended) to make `POST /v1/embeddings` safe to retry. Same key + same body within 24h replays the cached response with `Aurous-Idempotent-Replayed: true`. Same key + different body returns `409 idempotency_key_in_use`. See [Idempotency](/idempotency) for the full semantics. ## Limits * **16 content parts** maximum per request (across all modalities combined). * **8 image\_url parts** maximum per request. * **1 video\_url part** maximum per request. * **128K tokens** total input across text + image + video (after tokenization). Over the cap returns `400 embeddings_input_too_large`. * Image and video URLs must be HTTPS-reachable in under 10 seconds. URL strings are capped at 2048 characters. See [Multimodal](/api-reference/embeddings/multimodal) for the input shape and the worked multimodal example. ## Errors Every non-2xx response uses the standard Aurous error envelope — see [Errors](/errors) for the full taxonomy. Embedding-specific codes you might see: * `model_not_found` (404) — unknown `model` slug. Check the [`/v1/models`](/api-reference/openapi#tag/models) listing. * `model_disabled` (403) — model exists but admin has deactivated it. * `model_wrong_kind` (400) — you sent a chat model to the embeddings endpoint (or vice versa). * `embeddings_batch_not_supported` (400) — `input` is a `string[]` array. v1 doesn't accept batch input; loop client-side. See [Multimodal](/api-reference/embeddings/multimodal#batch-rejection). * `embeddings_input_too_many_items` (400) — over the 16-part or 8-image cap. Split into multiple requests. * `embeddings_video_unsupported` (400) — any `video_url` part is rejected (changed 2026-05-24). Extract a representative frame in your pipeline and submit it as `image_url` (bills at the visual rate). * `embeddings_input_too_large` (400) — pre-fetch token estimate is over the model's context window. Trim input. * `embeddings_unsupported_dimensions` (400) — the requested `dimensions` is not supported by this model. Check `aurous_metadata.dimensions` on the model row. See [Dimensions](/api-reference/embeddings/dimensions). * `tpm_rate_limit_exceeded` (429) — tokens-per-minute bucket is dry. Sleep `Retry-After` and retry. * `embeddings_provider_unknown_error` (502) — upstream returned an unmapped error. Retry with backoff. ## Related * [Multimodal](/api-reference/embeddings/multimodal) — the input-shape details and the multimodal worked example. * [Dimensions](/api-reference/embeddings/dimensions) — when the `dimensions` parameter is accepted. * [Pricing](/api-reference/embeddings/pricing) — per-token credit math and worked examples. * [Estimate](/api-reference/embeddings/estimate) — preview cost without charging. # Pricing Source: https://docs.aurous-labs.com/api-reference/embeddings/pricing Credit math for embeddings, with worked examples and the per-modality breakdown. Embeddings are billed in **credits** at per-token rates surfaced on each model row at [`GET /v1/models`](/api-reference/openapi#tag/models) under the `embedding_pricing` block. The rate is the caller's **effective rate** — any per-team override is already applied. Each response carries a `usage` block with the exact charge and a per-modality breakdown so you can attribute cost to inputs. **Changed 2026-05-24.** The `embedding_pricing.video` rate was removed. Video input on `POST /v1/embeddings` is rejected with [`embeddings_video_unsupported`](/api-reference/errors/embeddings_video_unsupported). The provider folds video frames into the visual billing bucket, so the published video rate never actually fired — to keep the receipt honest we removed the shape. Embed visual content from videos by extracting frames in your pipeline and submitting them as `image_url` parts at the visual rate. See the [changelog](/changelog) for migration notes. ## What you pay for Each embedding request is billed across two input buckets, depending on the modalities you sent: | Bucket | Counts | Rate field on `/v1/models` | | -------------- | --------------------------------------------------------------- | ---------------------------------------- | | Input — text | `prompt_tokens` from `text` parts (or the plain-string `input`) | `embedding_pricing.text.credits_per_M` | | Input — visual | `prompt_tokens` attributed to `image_url` parts | `embedding_pricing.visual.credits_per_M` | Customers can compute credits-per-1K by dividing `credits_per_M` by 1000. There is no separate "output" bucket — embeddings return a vector, not generated tokens. ## Day-1 rates for `aurous-embed-vision-1.0` From the seeded pricing config (rates are mutable; the live values are always at [`/v1/models`](/api-reference/openapi#tag/models) on the per-model `embedding_pricing` block): | Modality | Raw rate (USD per 1M input tokens) | Credit rate (credits per 1M input tokens) | | -------------- | ---------------------------------- | ----------------------------------------- | | Text | \$0.125 / 1M | 18.75 credits / 1M | | Visual (image) | \$0.325 / 1M | 48.75 credits / 1M | The credit rate is computed at request time from `usd_per_M`, the platform anchor `usd_per_credit` (1 credit = \$0.01 by default), and a percentage markup. See [How cost is computed](#how-cost-is-computed) below for the formula. ## Worked examples The examples below use the day-1 rates and the default anchor (`usd_per_credit = $0.01`, `markup_pct = 50%`). Numbers are exact; round to your billing precision. ### Example 1 — text-only, 500-token document ```jsonc theme={null} { "model": "aurous-embed-vision-1.0", "input": "A 500-token product description..." } ``` ```text theme={null} text tokens = 500 visual tokens = 0 text credits = 500 / 1M × $0.125 ÷ $0.01 × 1.5 = 0.009375 credits visual credits = 0 credits_charged = 0.009375 credits ``` Response: ```jsonc theme={null} { "usage": { "prompt_tokens": 500, "total_tokens": 500, "credits_charged": 0.009375, "breakdown": { "input": { "text": 0.009375, "visual": 0, "video": 0 }, "model": "aurous-embed-vision-1.0" } } } ``` (`breakdown.input.video` is retained on the response shape at `0` for one release cycle so existing SDKs that read it don't break — it will be dropped in a follow-up release.) ### Example 2 — multimodal, text + 1 image (\~1000 visual tokens) ```jsonc theme={null} { "model": "aurous-embed-vision-1.0", "input": [ { "type": "text", "text": "A 1000-token product description..." }, { "type": "image_url", "image_url": { "url": "https://assets.aurous-labs.com/example-images/product.jpg" } } ] } ``` ```text theme={null} text tokens = 1000 visual tokens = 1000 text credits = 1000 / 1M × $0.125 ÷ $0.01 × 1.5 = 0.01875 credits visual credits = 1000 / 1M × $0.325 ÷ $0.01 × 1.5 = 0.04875 credits credits_charged = 0.06750 credits ``` Response: ```jsonc theme={null} { "usage": { "prompt_tokens": 2000, "total_tokens": 2000, "credits_charged": 0.067500, "breakdown": { "input": { "text": 0.018750, "visual": 0.048750, "video": 0 }, "model": "aurous-embed-vision-1.0" } } } ``` ### Example 3 — multimodal, text + 2 images (\~2000 visual tokens) ```jsonc theme={null} { "model": "aurous-embed-vision-1.0", "input": [ { "type": "text", "text": "A 2000-token combined catalog entry..." }, { "type": "image_url", "image_url": { "url": "https://assets.aurous-labs.com/example-images/front.jpg" } }, { "type": "image_url", "image_url": { "url": "https://assets.aurous-labs.com/example-images/back.jpg" } } ] } ``` ```text theme={null} text tokens = 2000 visual tokens = 2000 text credits = 2000 / 1M × $0.125 ÷ $0.01 × 1.5 = 0.03750 credits visual credits = 2000 / 1M × $0.325 ÷ $0.01 × 1.5 = 0.09750 credits credits_charged = 0.13500 credits ``` Response: ```jsonc theme={null} { "usage": { "prompt_tokens": 4000, "total_tokens": 4000, "credits_charged": 0.135000, "breakdown": { "input": { "text": 0.037500, "visual": 0.097500, "video": 0 }, "model": "aurous-embed-vision-1.0" } } } ``` A typical RAG-for-images workload — one product image plus a short description — lands somewhere between Example 1 and Example 2 (a few hundredths of a credit per item). At the default 1 credit = $0.01 anchor, indexing 100K product images with descriptions sits around **$67\*\* before any volume discount. ## The `usage.breakdown.input` block Every embedding response carries this block: ```jsonc theme={null} "breakdown": { "input": { "text": , "visual": , "video": 0 }, "model": "" } ``` * `text` — credits attributed to the text portion of the input (always present, `0` when no text). * `visual` — credits attributed to image parts (always present, `0` when no images). * `video` — deprecated 2026-05-24, retained at `0` for one release cycle so existing SDKs that read it don't break. Will be dropped in a follow-up release. * `model` — the model slug, echoing back the request to make audit trails self-contained. The sum of the live modality keys (`text` + `visual`) equals `credits_charged` in the common case (see the next section for the rare overdraft path). ## How cost is computed The platform applies this formula per modality, then sums: ```text theme={null} modality_credits = (tokens × USD_per_M) ÷ USD_per_credit × (1 + markup_pct / 100) ``` Walking Example 2's text bucket end-to-end: ```text theme={null} tokens = 1000 USD_per_M = 0.125 ← raw upstream rate (the platform converts to credits via the formula above; the resulting credits_per_M is what's surfaced on /v1/models.embedding_pricing.text) USD_per_credit = 0.01 ← platform anchor (or your team override if set) markup_pct = 50 raw_credits = (1000 × 0.125) / 1,000,000 / 0.01 = 0.0125 marked_credits = 0.0125 × (1 + 50 / 100) = 0.01875 credits ``` The same formula runs for `visual` with its own rate. The final `credits_charged` is the sum across `text` + `visual`. `credits_charged` is the **authoritative** value — deduct it from your team balance and store it next to the row in your ledger. On rare overdraft-fallback paths (where the upstream model returned more tokens than the held credits could cover), the modality components in `breakdown.input` are scaled to reconcile back to `credits_charged`. Raw, un-scaled values are preserved in the platform's audit trail and surfaced through [`GET /v1/usage`](/api-reference/openapi#tag/usage) for FinOps reconciliation. ## Rate updates — the mutability story Embedding rates are mutable: admin can update them at any time without a fresh `Aurous-Version` release — LLM rates are explicitly mutable in the contract. To detect a rate change, poll [`GET /v1/models`](/api-reference/openapi#tag/models) and compare the per-model `embedding_pricing.{text,visual}.credits_per_M` against the value you last cached. The per-request charge is always deterministic against the rate card in force when the request was created — not against whatever the latest rates are at the moment you read the response. ## Estimating cost before dispatch Send the same payload to [`POST /v1/embeddings/estimate`](/api-reference/embeddings/estimate) to preview the charge without billing. See [Estimate](/api-reference/embeddings/estimate) for the endpoint shape, the upper-bound caveat, and SDK examples. ## Refunds | Outcome | Charged for | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | | Successful embedding | Actuals as reported in `usage`. | | Bad input (400 from the platform) | 0. No row written, no credits deducted. | | Provider returned a 5xx | 0. The hold is released. Retry with backoff. | | `embeddings_input_too_large` (pre-fetch token estimate exceeds context window) | 0. The DTO rejects before the provider is called. | | `embeddings_video_unsupported` (any `video_url` part) | 0. Rejected at the DTO boundary; embed extracted frames as `image_url` instead. | The policy is: **you pay for what was delivered.** Embeddings are atomic — there is no partial-completion concept the way streamed chat has — so the row either lands with a charge or doesn't land at all. ## Idempotency and billing `POST /v1/embeddings` accepts `Idempotency-Key`. Same key + same body within 24h replays the cached response — **including the original `credits_charged`**. You will NOT be double-charged for a retried key. Same key + different body returns `409 idempotency_key_in_use`. See [Idempotency](/idempotency) for the full semantics. ## Where to read rates * [`GET /v1/models`](/api-reference/openapi#tag/models) — per-model `embedding_pricing.{text,visual}.credits_per_M` rows (the caller's effective rate, including any per-team override). * [Estimate](/api-reference/embeddings/estimate) — preview cost for a specific payload. ## Common questions **Are the per-modality rates the same?** No. On `aurous-embed-vision-1.0` the visual rate is higher than text per token, reflecting the cost difference. Plain-string input pays only the text rate. **Can I see usage trends?** Yes — [`GET /v1/usage`](/api-reference/openapi#tag/usage) aggregates credits by day/key/model with the same per-modality split. **What happened to the video rate?** Removed 2026-05-24. The provider folds video frames into the visual billing bucket — the published video rate never actually fired. Submitting a `video_url` part now returns [`embeddings_video_unsupported`](/api-reference/errors/embeddings_video_unsupported). Extract a representative frame in your pipeline and embed it as an `image_url` part at the visual rate. ## Related * [Overview](/api-reference/embeddings/overview) — embeddings surface and quick start. * [Multimodal](/api-reference/embeddings/multimodal) — the input shapes that drive the visual bucket. * [Estimate](/api-reference/embeddings/estimate) — preview cost without charging. * [Errors](/errors) — the full error taxonomy. # URL fetching for image_url Source: https://docs.aurous-labs.com/api-reference/embeddings/url-fetching How the platform fetches image URLs server-side, what gets blocked, and what happens on timeouts or 404s. When you pass `image_url: { url: "..." }` to `POST /v1/embeddings`, the platform fetches the image bytes server-side at request time and feeds them to the embedding model. This page covers the fetch guards (what we block to prevent SSRF and abuse) and the failure modes (what error you get when the URL is unreachable). **As of 2026-05-24, `video_url` parts are rejected** with [`embeddings_video_unsupported`](/api-reference/errors/embeddings_video_unsupported) before any fetch is attempted. Extract a representative frame in your pipeline and submit it as `image_url`; it bills at the visual rate. ## The fetch is server-side Aurous fetches the URL from our infrastructure, NOT your browser/server. Implications: * The URL must be **publicly reachable** from our cloud, not from your private network * Our IP appears in your CDN's logs as the requester, not your end user's IP * Authentication via header (Bearer token, signed URL params, etc.) on the URL is preserved — we forward the URL as-is to the GET request We do not store the fetched bytes after the embedding completes (no cache; bytes are streamed into the model and discarded). ## Allowed schemes * **`https://`** — accepted Blocked schemes: * `http://` — rejected (security; we require TLS) * `data:` — rejected (use direct base64 inside the request body via `image_url.b64_json` if you have inline bytes; that's a separate ingest path) * `ftp://`, `file://`, `gopher://`, etc. — rejected * Bare hostnames without a scheme — rejected Attempting any blocked scheme returns `400 invalid_request` from the DTO validator before the request reaches the fetch layer. ## Blocked address ranges Even with a valid `https://` URL, the resolved IP must NOT fall into one of these ranges: * **Loopback**: `127.0.0.0/8`, `::1` * **Link-local**: `169.254.0.0/16` (incl. cloud metadata `169.254.169.254`), `fe80::/10` * **RFC1918 private**: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16` * **Unique local IPv6**: `fc00::/7` * **Multicast / reserved**: `224.0.0.0/4`, `240.0.0.0/4` * **Cloud metadata endpoints**: hostnames like `metadata.google.internal`, `metadata.aws.com`, `169.254.170.2`, etc. Resolving to a blocked address returns `400 invalid_request` with a `url host ... is a private / loopback / link-local IPv4 address` message and the `param` set to the offending content-parts path (`input[*].image_url.url` for embeddings, `messages[*].content[*].image_url.url` for chat). The error code is shared across both surfaces. The resolution check happens AFTER we look up DNS — a URL that resolves to a public address now but to a private address tomorrow (DNS rebinding) is still blocked on second resolution. We do not pin the resolved IP across the request lifecycle, but we do re-check the IP at TLS-connect time. ## Timeout Each URL fetch has a **10-second timeout**. URLs that take longer to first-byte return `502 chat_provider_request_invalid` with detail `url_fetch_timeout`. Common causes: * The host is geo-distant and TLS handshake is slow * The host is rate-limiting our IP * The asset is huge (>500MB) and slow to deliver For consistent latency, host your media on a CDN with global PoPs. Cloudflare R2 + the Cloudflare CDN, S3 + CloudFront, GCS + Cloud CDN, etc. are all fine. ## Size limits * **Image fetch cap**: 50 MB per image * **Video fetch cap**: 500 MB per video Exceeding the cap returns `400 invalid_request` with detail `url_size_exceeded`. The cap is enforced by streaming the body and aborting when the size is exceeded — we do not download then check. ## Content-type validation We require the response `Content-Type` header to match the media kind: * Image URLs: `Content-Type` must start with `image/` (e.g. `image/jpeg`, `image/png`, `image/webp`) * Video URLs: `Content-Type` must start with `video/` (e.g. `video/mp4`, `video/quicktime`) A mismatch (e.g. an `image_url` pointing at an `application/octet-stream`) returns `400 invalid_request` with detail `url_content_type_mismatch`. Set the `Content-Type` on your CDN or origin correctly — most CDNs do this automatically based on the file extension. ## 4xx / 5xx upstream If the URL returns a non-2xx status, the upstream fetch failure surfaces as a `502` with the provider-error envelope. The code name still reads `chat_provider_unknown_error` even on the embeddings surface today — that's tracked for a v1.1 rename to a surface-agnostic `provider_request_invalid`. For now, both surfaces share the same code: ```json theme={null} { "error": { "type": "server_error", "code": "chat_provider_unknown_error", "message": "Failed to fetch image at https://example.com/missing.jpg: upstream returned 404 Not Found", "doc_url": "https://docs.aurous-labs.com/errors#chat_provider_unknown_error", "request_id": "req_..." } } ``` This catches: * 404 (asset moved / not yet uploaded) * 403 (access control denied us) * 5xx (origin down) * TLS errors (expired cert, hostname mismatch) * DNS resolution failure In all cases, the platform does NOT bill the request — no hold is committed. ## Recommended URL hygiene For reliable embedding pipelines: * **Upload to a CDN with stable URLs.** R2 + Cloudflare, S3 + CloudFront, Bunny CDN. Avoid direct origin hosting on a single VM. * **Use signed URLs with a short TTL.** A 1-hour expiry is fine — we fetch immediately on request. * **Set `Cache-Control: public, max-age=3600`** on the CDN response — lets the CDN edge-cache the asset, which makes our fetch fast on the second call. * **Use `image/webp` or `video/mp4`** — those are universally supported. * **Trim images before upload.** A 4096×4096 PNG resized to 1024×1024 cuts our per-image visual token count by \~16× with negligible semantic loss. ## Where to next? * [Multimodal embeddings](/api-reference/embeddings/multimodal) — the full content-parts surface * [Embedding limits](/api-reference/embeddings/limits) — caps on parts, characters, URLs * [Error codes](/errors) — full taxonomy * [`POST /v1/embeddings`](/api-reference/openapi#tag/embeddings) — endpoint reference # Get a single generated image Source: https://docs.aurous-labs.com/api-reference/images-proxy/get-a-single-generated-image /api-reference/openapi.json get /v1/images/{id}/output/{n} Returns the generated image. Image outputs are retained for ~7 days after generation; after that the endpoint returns `410 Gone` with `code: output_expired`. If the generation never produced an output (status `failed`, `cancelled`, `moderation_rejected`, or polling-timeout `expired`), this endpoint returns `422 Unprocessable Entity` with `code: output_not_available` — check `GET /v1/images/{id}` for the failure reason. Save what you want to keep — long-term storage is intentionally not part of the platform. This route is anonymous-read: knowing the URL is sufficient to fetch the bytes. Browser `` tags work directly without auth headers. API consumers can still send `Authorization: Bearer …` or `X-Api-Key: …` — these are accepted but not required. # Create an image Source: https://docs.aurous-labs.com/api-reference/images/create-image POST /v1/images Submit an image generation. Optionally anchor identity with a character. `POST /v1/images` submits an image generation request. Admitting the generation places a hold for the full price and reduces your available balance immediately — see [Image pricing](/api-reference/images/models-and-pricing) for the rate card and how the hold settles; the generation is processed asynchronously. Poll [`GET /v1/images/{id}`](/api-reference/images/retrieve-image) for status, or register a [webhook endpoint](/webhooks) for a push callback when the generation completes or fails. For a step-by-step walkthrough, see the [Quickstart](/quickstart). The full request shape, including all generation parameters, is in the playground below. ## Using a style Styles come from `GET /v1/loras`; pass a style's `id` (opaque `lora_*` or slug) as `lora_id`. The field is **tri-state**: | You send | What happens | | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | *(omitted)* | Style matching runs automatically: when your prompt clearly names a look (e.g. "golden-hour film photo"), a matching style is applied. Matching is conservative — most prompts resolve to no style. | | `"lora_…"` or a slug | That style is pinned and applied. | | `null` | Style matching is disabled for this request — the image generates without a style. | Whatever happens, the outcome is observable: every generation response echoes `style: { id, name }` (or `null`), and `style.id` round-trips — pass it back as `lora_id` to reuse the style. Styles **compose** with [composition acts](/guides/actions) and `subjects` — pin a style and an `action_id` together and both apply. One special case: some catalog entries are composition acts. Sending an act's id as `lora_id` pins the act itself, so combining it with a *different* `action_id` returns `400 parameter_invalid_combination`. The same applies to `subjects`: the few styles that pick their own model still return `400 parameter_invalid_combination` when combined with `subjects`. `lora_id` remains incompatible with `context_images`. ### Retired styles Retired style ids keep working — how depends on the style: * **Aliased** — the id applies its designated successor style; the response `style` echoes the successor. Update your stored id at your convenience. * **Plain** — the request succeeds but generates **without** a style, and the response carries a [`warnings[]`](#warnings) entry with code `style_retired_plain`. * **Discontinued** — a small set of styles no longer generate at all: `400` with code [`style_retired`](/errors#style_retired). Pick a current style from `GET /v1/loras`. ## Batching with `count` `count` (1–4, whole number) generates that many images in parallel and bills per image. If some images in the batch fail, you receive the ones that succeeded and the difference is refunded automatically — the response `image_count` reflects the number actually delivered, and `output_urls` contains one URL per delivered image. ## Warnings The 201 body (and the [estimate](/api-reference/openapi) response) may carry `warnings[]` — non-fatal adjustments the platform made to your request: ```json theme={null} { "warnings": [ { "code": "style_retired_plain", "param": "lora_id", "message": "Style lora_06AAAAAAAAAAAAAAAAAAAAAAAA is retired and no longer applies a style — this request generates without one." } ] } ``` Current codes are `style_retired_plain` (see above) and `parameter_ignored` (a parameter you sent has no effect on the generation path your request selected — for example `seed` on a styled generation). The key is omitted when there is nothing to report, appears **only** on the create and estimate responses (never on GETs, lists, or webhooks), and the code set is open — ignore codes you don't recognize. Idempotent replays return the original warnings verbatim. Response fields echo the request parameters as sent, not as used: an ignored parameter (flagged in `warnings[]`) is echoed back with exactly the value you sent, not the value that was actually applied. ## Using a character When `character_id` is set, the platform attaches the character's saved reference images to the generation as visual anchors for identity consistency. The dispatch path is image-to-image, so `denoise_strength` becomes effective and influences how closely the output follows the refs vs the prompt. The character must be in `status: ready`. Use a `synthesizing` / `reviewing` / `failed` character, or a soft-deleted one, and the request returns `400 character_not_ready`. `character_id` and `reference_image_urls` are **mutually exclusive**. Sending both returns `400 mutually_exclusive_input`. Pick one path per generation. ```bash cURL theme={null} curl -X POST https://api.aurous-labs.com/v1/images \ -H "X-Api-Key: $AUROUS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "prompt": "Aurora at golden hour on a windswept cliff, cinematic", "character_id": "char_01HXMQ7Z3K8Y2VNABCDEFGHJKM", "size": "1_5k_2_3" }' ``` If the character has multiple ref poses, the dispatcher consumes all of them as anchors. There is no current way to limit attachment to a subset of poses — the whole ref set goes in. ## Size Specify image dimensions one of two ways — never both: **Named preset** via `size`: | Tier | Bills at | Available aspect ratios | | ------ | ---------- | --------------------------------------------------------- | | `1_5k` | `standard` | `1:1`, `3:2`, `2:3`, `4:3`, `3:4`, `16:9`, `9:16`, `21:9` | | `2k` | `large` | `1:1`, `3:2`, `2:3`, `4:3`, `3:4`, `16:9`, `9:16`, `21:9` | | `4k` | `large` | `1:1`, `3:2`, `2:3`, `4:3`, `3:4`, `16:9`, `9:16`, `21:9` | Combine into a preset string in `_` form, e.g. `1_5k_16_9`, `2k_1_1`, `4k_2_3`. **Omitting `size`** (and not passing `width`/`height` either) defaults to `1_5k_1_1` — the cheapest, `standard`-tier preset. See [Image pricing](/api-reference/images/models-and-pricing) for the credit rate per tier and exactly how the tier is decided. **Custom dimensions** via `width` and `height`: * Both required when used. * Range `[1024, 4096]` per side. * Snapped server-side to the nearest multiple of 32. Sending both `size` and `width`/`height` returns `400 parameter_invalid_combination`. Sending only one of `width`/`height` returns `400 missing_field`. The response's `width`/`height` (and `size_preset`, for the named-preset path) echo your *resolved request* — the post-snap dimensions for a custom size, or the preset's fixed dimensions. For most requests that's also the delivered file's actual pixel size, but not always: a very large request can render at a smaller size than requested, and when that happens the response still reports the dimensions you requested, not the delivered file's. ## Idempotency Pass `Idempotency-Key` (any opaque value, 1–256 chars; UUID v4 recommended). Same key + same body within 24h replays the cached response with `Aurous-Idempotent-Replayed: true`. Same key + different body returns `409 idempotency_key_in_use`. The 24h window and 1–256 char bound are documented in [Idempotency](/idempotency). ## Webhooks Register a [webhook endpoint](/webhooks) subscribed to `image.completed` / `image.failed` (`POST /v1/webhook_endpoints`) to receive a POST callback when the generation reaches a terminal state. The event payload is `{ event: "image.completed" | "image.failed", data: {...} }` where `data` matches the `GET /v1/images/{id}` response. See [Webhooks](/webhooks) for signature verification. # Models & pricing Source: https://docs.aurous-labs.com/api-reference/images/models-and-pricing The Aurous Image rate card, how the pixel tier is decided, and the exact-price guarantee. Billed in Aurous credits. Aurous Image generations are billed in **Aurous credits** at **\$0.01 per credit** (so credits = dollars × 100). The price is fixed by the output size you asked for at request time — there is no provider-side metering to reconcile afterward. This page gives the day-one rate card, how the tier is decided, and the guarantee that keeps the quote and the charge identical. ## Models | Model | What it does | | ------------------ | ------------------------------------------------------------------------ | | `aurous-image-pro` | Flagship image generation. Pixel-tiered pricing: `standard` and `large`. | Live rates and capabilities are on [`GET /v1/models`](/api-reference/openapi#tag/models), under this model's `image_pricing` block. ## Rate card | Tier | Output size | Credits per image | | ---------- | ------------------------ | ----------------- | | `standard` | ≤ 2,360,000 px (2.36 MP) | **6.5** | | `large` | > 2,360,000 px | **13** | Multiply by `count` for a multi-image request — every image in a request shares the same output size, so the same tier and rate apply to each one. ## How the tier is decided The tier is decided by your **output pixels** — `width × height`, after any server-side snapping — not by which size preset name you sent: * Pass a `size` preset (e.g. `2k_1_1`) or explicit `width`/`height`, and the platform resolves the output pixel count and matches it against the boundary above. * **Omit `size` entirely** and the platform defaults to `1_5k_1_1` (1472×1472 = 2,166,784 px) — **`standard` tier**, at 6.5 credits. * **Every `1_5k_*` preset is `standard` tier.** The largest, `1_5k_3_2` (or its `1_5k_2_3` mirror) at 1824×1216 = 2,217,984 px, is still under the boundary — every ratio at `1_5k_*` follows. * **Every `2k_*` and `4k_*` preset is above the boundary by pixel count.** `2k_1_1` alone is 2048×2048 = 4,194,304 px, well past it — every ratio at `2k_*` and `4k_*` follows. * **During the current migration, a small number of request shapes still settle on the previous flat per-image rate rather than the tier above.** Where that happens the charge is *lower* than the rate card, never higher, and it is disappearing as the migration completes — so treat the card as the ceiling. If you need the exact figure for a specific request, call [`POST /v1/images/estimate`](/api-reference/images/create-image) and read `estimated_cost.amount` and `breakdown.size_tier` back; the estimate is authoritative for that request and always matches what the generation bills. * **Custom `width`/`height` snap to the nearest multiple of 32 *before* the tier check runs** — the boundary compares against the *snapped* pixel count, not the number your own `width × height` arithmetic gives you. Both sides always snap to a multiple of 32, so every reachable output is a multiple of 1024 — the boundary itself (2,360,000) is never landed on exactly. The largest reachable `standard` output is 2,359,296 px; the smallest reachable `large` output is 2,365,440 px. * **This snap can flip your expected tier near the edge.** A `1552×1520` request computes to 2,359,040 px by hand (under the boundary — you'd budget `standard`), but snaps to 1568×1536 = 2,408,448 px and bills `large`. A `1537×1537` request computes to 2,362,369 px by hand (over the boundary), but snaps to 1536×1536 = 2,359,296 px and bills `standard`. If you're computing custom dimensions close to the boundary, snap each side to a multiple of 32 yourself before comparing against 2,360,000, or call `POST /v1/images/estimate` and read `size_tier` back rather than predicting it. * **Free:** reference images (`reference_image_urls`, `subjects[]`, `context_images`, `character_id`) and prompt enhancement (`enhance_prompt: true`). Neither changes which tier you land in, and neither adds a line to the price. ## The exact-price guarantee > `POST /v1/images/estimate` returns the exact price `POST /v1/images` will charge for the identical request body — there is no ceiling-vs-actual gap to reconcile for images. * **`estimated_cost.breakdown.size_tier`** (on the estimate) and **`cost.breakdown.size_tier`** (on the generation response) name the tier your request billed at — `"standard"` or `"large"`. It's a string, not a credit amount — don't add it to reproduce `amount`. When your team has a negotiated discount, the breakdown also carries `discount_factor`: a multiplier already folded into `base`, not a separate credit amount, so don't add that either — `base` alone equals `amount`. * Because the price depends only on your request — output size, `count`, and any team discount — and never on post-generation metering, the estimate is exact on every request shape: plain, `subjects[]`, and `context_images` alike. * Machine-readable per-tier rates are always available at [`GET /v1/models`](/api-reference/openapi#tag/models) under `image_pricing.tiers[]` — read them live rather than hard-coding the numbers above. ## How billing works Admitting the generation places a hold for the full price and reduces your available balance immediately. That hold is also the final charge — nothing about an image's price depends on what happens after dispatch, so there's no later reconciliation step the way there is for token-metered video. `cost.refunded: true` (with `amount: 0`) is set on a `failed` generation, meaning the full charge was refunded; if part of a `count > 1` batch fails, the difference for the images that didn't deliver is refunded automatically. A `cancelled` generation's hold is released the same way, but the response doesn't yet reflect that: `cost.amount` still shows the originally-held amount and `refunded` is not set — key off the generation's `status` field (`cancelled`), not `cost.refunded`, to detect a cancellation. ## Rates can change Image rates are **DB-driven and may change without an `Aurous-Version` bump** — the same mutability asymmetry documented for [LLM pricing](/api-reference/chat/pricing#mutability-asymmetry-vs-images-and-videos). Always read the current per-tier rate from [`GET /v1/models`](/api-reference/openapi#tag/models) rather than hard-coding the numbers above. Each charged image snapshots the rate in force when it was created — including on an idempotent replay — so the amount you were quoted is the amount you pay. # Retrieve an image Source: https://docs.aurous-labs.com/api-reference/images/retrieve-image GET /v1/images/{id} Fetch the live state of a generation by ID. Poll this for status. `GET /v1/images/{id}` returns the current state of a generation. Poll it to track async progress until `status` reaches a terminal value: `succeeded`, `failed`, `cancelled`, `expired`, or `moderation_rejected`. The same endpoint serves both image generations (`img_*`) and video generations (`vid_*`) — they share a status surface. See the [Videos overview](/api-reference/videos/create-video) for the polling pattern. ## Reading the `loras` array The `loras` field reports the styles applied to this generation. Each entry is a `{ id, name }` pair where `id` is the opaque LoRA ID resolved at dispatch time. The array is `null` for prompt-only generations (no LoRA picked) and for pure-reference generations. When you dispatch with a "bundle" style (a multi-style stack the dashboard exposes as a single pick), the `loras` array carries one entry — the bundle, surfaced as a LoRA. The V1 surface presents bundles and single LoRAs uniformly: one entry, one ID, one name. ## Output URLs `output_urls` is populated when `status === "succeeded"`. Each URL is a signed proxy URL on `api.aurous-labs.com`: * No `X-Api-Key` header needed — the signature is in the query string. * URLs expire **\~7 days after generation** (`410 Gone` with `code: output_expired` after that). If the generation reached a terminal status without producing output (`failed`, `cancelled`, `moderation_rejected`, polling-timeout `expired`), the proxy returns `422 Unprocessable Entity` with `code: output_not_available` instead. * Save what you want to keep — long-term storage is intentionally not part of the platform. For video generations, `video_url` is the polled URL, not `output_urls` — see [Create a video](/api-reference/videos/create-video). ## Polling cadence Most image generations finish in 10–30 seconds. Poll on a 2-second interval with light exponential backoff. Tighter polling burns rate-limit budget without finishing your generation any faster — see [Rate limits](/rate-limits). ```bash cURL theme={null} curl https://api.aurous-labs.com/v1/images/img_01HXMQ7Z3K8Y2VNABCDEFGHJKM \ -H "X-Api-Key: $AUROUS_API_KEY" ``` ## Cost on the response Once `status` is terminal, `cost.amount` reports the credits actually charged. For pending and processing generations, `cost.amount` reports the held amount — the upper bound that will be deducted on success. Cancelled generations refund any held credits. A `failed` generation settles at `cost.amount: 0` with `cost.refunded: true` — the reserved hold was released, never charged. `refunded` is only ever present on `failed` rows; it's omitted on `succeeded`, `pending`, and `processing` generations. # List chat, embedding, video, and image models Source: https://docs.aurous-labs.com/api-reference/models/list-chat-embedding-video-and-image-models /api-reference/openapi.json get /v1/models Returns the catalog of chat, embedding, video, and image models available to your team. Shape mirrors OpenAI's `GET /v1/models` envelope (`{ object: "list", data: [...] }`) so SDK clients that already speak OpenAI drop in unchanged. Platform-specific fields (capability tags, lifecycle pointers) live under `aurous_metadata` on each entry. Per-model pricing lives on top-level `chat_pricing` (chat models), `embedding_pricing` (embedding models), `video_pricing` (video models), or `image_pricing` (image models) — the unused siblings are null. Video rates are a matrix in credits per million VIDEO tokens, keyed by output resolution and whether the request includes video input; listed video rates use standard (non-team) pricing. Image rates are a small closed tier list in credits per generated image, keyed by output pixel count; listed image rates also use standard (non-team) pricing. Customers can compute credits-per-1K by dividing the published `credits_per_M` value by 1000. New model kinds may be added over time — clients should skip entries with an unrecognized `kind` rather than failing to parse the response. When the catalog is empty the response is `200` with `data: []` — never `404`. No query, path, or body parameters are accepted. Chat, embedding, image, and video rates are all NOT frozen per `Aurous-Version` — they track the most recently published rate version for each model, because provider economics and per-model markup are tuned more often than the API contract changes. The receipt echoed on every chat / embedding response (and the cost breakdown on every image / video generation) carries the exact rate that applied at dispatch or hold time, so audit trails remain stable even as the underlying rate card changes. `Aurous-Version` still governs request/response shapes — pin it for stable shapes, but call the relevant estimate or models endpoint for a current price immediately before you generate. # API Reference Source: https://docs.aurous-labs.com/api-reference/openapi Every Aurous Labs V1 endpoint, every parameter, every response shape. The interactive reference below is generated from the V1 OpenAPI specification. The current version pin is `2026-05-15`. Authenticate with your `X-Api-Key` header. Pass `Aurous-Version: 2026-05-15` to pin the contract. For runnable examples, walk the [Quickstart](/quickstart). For error envelope shapes, see [Errors](/errors). For the webhook signature cookbook, see [Webhooks](/webhooks). # Aggregated usage metrics Source: https://docs.aurous-labs.com/api-reference/public-api-v1/aggregated-usage-metrics /api-reference/openapi.json get /v1/usage Time-bucketed aggregates of every generation your team has dispatched. Buckets are returned newest-first. Pass `bucket_width` to control the bucket size; pass `group_by[]` to slice each bucket by status, key, user, or modality. Idempotent: GET is naturally safe to retry; no `Idempotency-Key` is required. Pagination is opaque-cursor (`page_token`); the cursor expires 24h after issue and is invalidated when query parameters change between pages. Headers `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`, and `Retry-After` (on 429) are returned on every response. # Cancel a pending or processing generation Source: https://docs.aurous-labs.com/api-reference/public-api-v1/cancel-a-pending-or-processing-generation /api-reference/openapi.json post /v1/images/{id}/cancel Cancels a generation that has not yet completed. Returns the cancelled generation resource. On a generation already in `cancelled` state this is a no-op (idempotent). On a generation already in `succeeded` or `failed` this returns `400 invalid_request / generation_not_cancellable` (terminal states cannot be undone). Hold-released or refund-applied credits are reflected on the next `GET /v1/balance`. # Delete a webhook endpoint Source: https://docs.aurous-labs.com/api-reference/public-api-v1/delete-a-webhook-endpoint /api-reference/openapi.json delete /v1/webhook_endpoints/{id} Idempotent — always returns 200, never 404. The response body reports whether anything was actually removed: `{ deleted: true, id: }` when the team owned the row, `{ deleted: false, id: null }` when nothing matched (already-gone, wrong team, typo). The `id` echoed back is always the SERVER-side id we deleted — never the user-supplied path-param — so integrator audit logs reflect what actually happened. # Estimate the credit cost of a video generation Source: https://docs.aurous-labs.com/api-reference/public-api-v1/estimate-the-credit-cost-of-a-video-generation /api-reference/openapi.json post /v1/videos/estimate For a fixed duration, `estimated_cost.amount` is the exact charge. For adaptive (`duration: -1`, the default), `estimated_cost.adaptive` is `true` and the price is a range: `amount_min`–`amount_max`, where `amount` equals `amount_max` (the credits held up front). You are charged for the delivered length and refunded the difference. Same validation as POST /v1/videos, but without side effects — no character synthesis, no reference materialization. A cast (`subjects[]`) is validated identically to POST /v1/videos — cast characters are existence/readiness-checked — and does not change the price. `reference_video_url`/`reference_audio_url` (a `file_` from POST /v1/files or an HTTPS URL) are validated for shape and parameter-combination compatibility, identically to `POST /v1/videos`. A `file_` additionally gets the same read-only checks the create call performs — it must exist, belong to your team (404 otherwise), and have been uploaded with the matching purpose (400 `reference_media_invalid` otherwise). The media **bytes** are never fetched at estimate time — the estimate never touches the network — so byte-level validation (format, duration, resolution) and HTTPS-URL fetching only happen on the actual create call. A reference-to-video request is never charged for `enhance_prompt`, even when `enhance_prompt: true` — the system composes the prompt around the reference and never invokes the enhancer, so the flat enhance adder never applies. Because the reference clip is never fetched, its real duration is unknown at estimate time, so a reference (`video_task: reference`) or extend request is priced at the 15-second input ceiling — an **upper bound**. The actual charge is typically lower: the create hold prices against the real clip length. As with the image estimate, this is a deliberate, conservative divergence on that path — a BYO-reference request is never charged more than this estimate. **Auto-matching is not run here, and the quote can miss in EITHER direction.** This endpoint prices exactly the request body you sent. Every *structural* validation rule is identical to POST /v1/videos, but model MATCHING is a create-time decision: if you omit `video_lora_id` and a video model then matches at create (from your prompt, or — when a `first_frame_url` is attached — from the image), the generation dispatches as a **video-input** generation and is priced accordingly. Which way the charge moves depends on what YOU pinned, because a matched model supplies `default_duration`, `default_resolution` AND `default_ratio` for whatever you left out, and all three are priced: - **You omitted `duration`/`resolution`** (the usual shape for a bare `first_frame_url` request): with no model, this endpoint has to price adaptive duration (the 15-second ceiling) at 1080p, and the matched model then brings a *shorter, smaller* default. The quote is an **upper bound that can substantially OVER-state** — measured on the first-frame path: `amount` 827.26 against an actual settled charge of 279.91. On these (adaptive) requests, `amount_min`/`amount_max` is the honest range to plan against; `amount` is only the ceiling that gets held. - **You sent `duration` and `resolution` explicitly**: your values win over the model defaults, so a match can only ADD the motion-reference input leg to the price. On that shape the charge genuinely **exceeds** this quote. **Pin `video_lora_id` whenever you need the quote to be exact** — that is the only value that removes matching from the create path entirely. `video_lora_id: null` opts out of IMAGE-based matching only, so on a request without a `first_frame_url` it does NOT make this quote exact; and on a request WITH one it also declines the model defaults, so an otherwise-bare request prices (and charges) the adaptive-15s/1080p fallback. This is inherent to auto-matching and is not specific to first frames. **Video and image rates are both DB-driven.** Video is priced from the live per-model rate table — see `GET /v1/models` for the current per-model credit rates — NOT frozen in the `Aurous-Version` rate card. Image pricing works the same way: the pixel-tiered rate card behind `POST /v1/images/estimate` is also read live from the registry and MAY change without an `Aurous-Version` bump — the same pricing asymmetry the LLM chat/embeddings surfaces use (spec §6.4). Pin an `Aurous-Version` for stable request/response **shapes**; call the matching estimate endpoint for an authoritative current-price quote immediately before you generate. # Estimate the credit cost of an image generation Source: https://docs.aurous-labs.com/api-reference/public-api-v1/estimate-the-credit-cost-of-an-image-generation /api-reference/openapi.json post /v1/images/estimate Same DTO as POST /v1/images. Returns the projected credit cost and a per-line-item breakdown (`base`, plus the `size_tier` this request would bill — `standard` or `large` — and `discount_factor` when your team has a negotiated discount) without enqueuing the work. `size_tier` is a STRING and `discount_factor` (when present) is a multiplier, not a credit amount — `base` alone equals the total, so skip both when reproducing `amount`. Reference images are free and do not affect price. Inputs are validated with the same rules as the create path — a private style from another team, a retired style that no longer generates (400 `style_retired`), a foreign or not-ready character subject, an over-budget subjects[] all return the same error a real POST /v1/images would — with one difference: the estimate never fetches inline https reference URLs, so a problem specific to a URL (unreachable host, non-image content) only surfaces at create. `count` multiplies the quote on every request shape — plain, `subjects[]`, and `context_images` alike: images generate in parallel and you are billed per image, with partial failures delivering fewer images and auto-refunding the difference, so estimate(count: 4) equals 4 × estimate(count: 1). The response carries the same `warnings[]` the real create would return — a retired style that generates without a style, or a parameter with no effect on the selected generation path — so you can surface them before spending credits. Isolated rate-limit bucket (`estimate_post`, 120/min) so pricing-check loops do not crowd out real generations. Reference images can be supplied via reference_image_urls (mutually exclusive with character_id) or as `reference` subjects. A `context_images` estimate fires the same 400 combination errors a real POST /v1/images would, and never fetches inline https entries (file IDs are still ownership-checked). When you pin an `action_id`, the estimate runs the identical composition-act gates as POST /v1/images — existence/visibility (an unknown or inaccessible act → 404 `resource_not_found`) and, when subjects are present, the subject-count check (an unsupported count → 400 `action_not_available`, whose message names the counts the act supports) — but never picks or signs a reference still. The same gates run when your `lora_id` is a composition-act id (it acts as the pin). Acts and styles do not change the price. # Fire a synthetic test delivery Source: https://docs.aurous-labs.com/api-reference/public-api-v1/fire-a-synthetic-test-delivery /api-reference/openapi.json post /v1/webhook_endpoints/{id}/test Enqueues a real, signed delivery with a fixture payload to exercise the receiver. The AurousEvent envelope carries `synthetic: true` so receivers can filter test fires from production traffic. Independent rate limit bucket (`webhooks_test`, 30/min) so it does not contend with normal webhook traffic. # Get a composition act by ID Source: https://docs.aurous-labs.com/api-reference/public-api-v1/get-a-composition-act-by-id /api-reference/openapi.json get /v1/actions/{id} Resolves an id from GET /v1/actions — a cheap liveness / support check for a persisted `action_id` (an act’s `supported_character_counts` changes as its stills are approved). Returns 404 for any id that is not a live act visible to your team. # Get a file (re-mints a fresh 1h signed URL) Source: https://docs.aurous-labs.com/api-reference/public-api-v1/get-a-file-re-mints-a-fresh-1h-signed-url /api-reference/openapi.json get /v1/files/{id} Returns the file metadata plus a freshly-minted 1h signed URL. Use this to re-mint a download URL whenever the previous one expires. # Get a LoRA style by ID or slug Source: https://docs.aurous-labs.com/api-reference/public-api-v1/get-a-lora-style-by-id-or-slug /api-reference/openapi.json get /v1/loras/{id} Resolves `lora_*` opaque IDs (canonical) or the mutable `slug`. Returns `404 not_found` if the LoRA is private to another team. # Get a video model by ID or slug Source: https://docs.aurous-labs.com/api-reference/public-api-v1/get-a-video-model-by-id-or-slug /api-reference/openapi.json get /v1/video_loras/{id} Resolves an id (or slug) from GET /v1/video_loras. Returns 404 for models that are not part of the catalog. # Get a webhook endpoint Source: https://docs.aurous-labs.com/api-reference/public-api-v1/get-a-webhook-endpoint /api-reference/openapi.json get /v1/webhook_endpoints/{id} Returns the endpoint metadata. `secret` is always null on reads. # Get credit balance Source: https://docs.aurous-labs.com/api-reference/public-api-v1/get-credit-balance /api-reference/openapi.json get /v1/balance Returns the team's credit balance broken down into total `credits`, `held_credits` (locked against pending generations on a charge-on-success basis), and `available_credits` = `credits` − `held_credits`. `POST /v1/images` and `POST /v1/videos` check against `available_credits` and return `402 invalid_request / balance_too_low` if insufficient. Holds are released back to the available balance when a generation fails or is cancelled; they commit (debiting `credits`) when a generation succeeds. The response also includes `billing_mode`: when it is `exempt`, do NOT gate on balance — generations are never blocked and the numeric fields never decrement. Treat `billing_mode` as an open enum (unknown ⇒ non-gating). # Get your team info Source: https://docs.aurous-labs.com/api-reference/public-api-v1/get-your-team-info /api-reference/openapi.json get /v1/team Returns basic information about the team associated with this API key, including the current credit balance and a `billing_mode` field. When `billing_mode` is `exempt`, do not gate on balance — generations are never blocked. Treat `billing_mode` as an open enum (unknown values ⇒ non-gating). # Immediately purge a file Source: https://docs.aurous-labs.com/api-reference/public-api-v1/immediately-purge-a-file /api-reference/openapi.json delete /v1/files/{id} Idempotent — returns 200 even when the file does not exist or has already been deleted. In-flight generations that already snapshotted the URL keep working; subsequent GET /v1/files/:id calls return 404. # List available composition acts Source: https://docs.aurous-labs.com/api-reference/public-api-v1/list-available-composition-acts /api-reference/openapi.json get /v1/actions Returns the catalog of live composition acts you can pin via `action_id` on POST /v1/images. Each act carries the subject counts it supports (`supported_character_counts`) — pin it with a matching number of subjects, or with zero subjects to render a new person your prompt describes. Selecting an act is optional: omit `action_id` to let act detection run automatically, or send `null` to disable it for a request. # List available LoRA styles Source: https://docs.aurous-labs.com/api-reference/public-api-v1/list-available-lora-styles /api-reference/openapi.json get /v1/loras Returns all LoRA styles available to your team, including public LoRAs and any privately delivered to your team. Each item carries an opaque `lora_*` ID and a URL-friendly `slug`; either form works in `/v1/loras/:id_or_slug` and in the `lora_id` field of `POST /v1/images`. # List available video models Source: https://docs.aurous-labs.com/api-reference/public-api-v1/list-available-video-models /api-reference/openapi.json get /v1/video_loras Returns the catalog of video models available to pin via `video_lora_id` on POST /v1/videos. Selecting a model is optional — omit `video_lora_id` to let the platform choose automatically, or to generate plain video when no model matches your prompt. # List credit-ledger events Source: https://docs.aurous-labs.com/api-reference/public-api-v1/list-credit-ledger-events /api-reference/openapi.json get /v1/usage/events Cursor-paginated stream of every credit movement on your team — holds placed and released or committed by your generations, top-ups via Stripe, refunds, and admin adjustments. Ordered newest-first. Use `?starting_after=` to walk older entries; `?limit=N` (1–100, default 20). Filter by `?type=` when reconciling against your own books. # List delivery attempts for a webhook endpoint Source: https://docs.aurous-labs.com/api-reference/public-api-v1/list-delivery-attempts-for-a-webhook-endpoint /api-reference/openapi.json get /v1/webhook_endpoints/{id}/deliveries Cursor-paged. Each row is one attempt — the same `event_id` may appear on up to 5 rows for a flapping receiver (5-attempt retry policy with exponential backoff [5s, 30s, 2m, 10m, 1h]). # List webhook endpoints Source: https://docs.aurous-labs.com/api-reference/public-api-v1/list-webhook-endpoints /api-reference/openapi.json get /v1/webhook_endpoints Cursor-paged. Returns endpoints in reverse chronological order (newest first). Pass `?starting_after=we_` to walk subsequent pages. # List your generation history Source: https://docs.aurous-labs.com/api-reference/public-api-v1/list-your-generation-history /api-reference/openapi.json get /v1/images Cursor-paginated list of your team's image and video generations, ordered by creation date (newest first). Use `?starting_after=` to walk forward and `?ending_before=` to walk backward; `?limit=N` (1–100, default 20). The cursor is the opaque `id` of any row from the prior page. Internal accounting entries (e.g. character-synthesis cost-ledger rows) are never returned. # Register a webhook endpoint Source: https://docs.aurous-labs.com/api-reference/public-api-v1/register-a-webhook-endpoint /api-reference/openapi.json post /v1/webhook_endpoints Mints a new endpoint, generates a signing secret, and returns the secret EXACTLY ONCE in the `secret` field. Store it on your side — subsequent reads return `secret: null`. Subscribe to `["*"]` to receive every event in the v1.0 taxonomy (the wildcard is expanded at create time; new event types added later do NOT auto-subscribe). # Rotate the signing secret Source: https://docs.aurous-labs.com/api-reference/public-api-v1/rotate-the-signing-secret /api-reference/openapi.json post /v1/webhook_endpoints/{id}/rotate_secret Mints a new plaintext, demotes the old secret to a 24h dual-validate window (receivers verify against the new first; on failure, fall back to the previous), and returns the new plaintext EXACTLY ONCE. After 24h the old secret is dropped. # Update a webhook endpoint Source: https://docs.aurous-labs.com/api-reference/public-api-v1/update-a-webhook-endpoint /api-reference/openapi.json patch /v1/webhook_endpoints/{id} Partial update. Re-passing `events: ["*"]` snapshots the current taxonomy (same semantics as on create). Setting `is_active=true` on a previously-disabled endpoint resets `consecutive_failures` to 0. # Upload a reference file Source: https://docs.aurous-labs.com/api-reference/public-api-v1/upload-a-reference-file /api-reference/openapi.json post /v1/files Stores a reference file for use in subsequent /v1/images or /v1/videos requests. Image purposes (`reference`, `first_frame`, `last_frame`): PNG/JPEG/WebP, max 10 MB, max 4096 px. Media purposes (`reference_video`: MP4/MOV ≤50 MB; `reference_audio`: WAV/MP3 ≤15 MB) feed the `reference_video_url`/`reference_audio_url` fields on POST /v1/videos — duration (2–15 s) and, for video, frame area (409,600–2,073,600 px) are validated at upload, so a stored file never fails media checks at create. 24h TTL — the file is auto-purged after that. The response carries a 1h signed URL; re-mint a fresh URL via GET /v1/files/:id. Multipart form with a `file` (binary) part, plus form fields `purpose` and optional `metadata` (JSON-encoded object, <=50 keys, values <=500 chars). # Cancel or delete a task Source: https://docs.aurous-labs.com/api-reference/seedance/cancel-delete DELETE /v1/contents/generations/tasks/{id} — cancel a queued task for a full refund, or hide a finished one. `DELETE /v1/contents/generations/tasks/{id}` is the native Seedance cancel/delete verb. What it does depends on the task's current state — Aurous mirrors the provider's state machine. * **Scope:** `write` * **Auth:** `X-Api-Key: al_live_…` or `Authorization: Bearer al_live_…` ```bash cURL theme={null} curl -X DELETE https://api.aurous-labs.com/v1/contents/generations/tasks/vid_01J9Z3K7Q0XY2ABCDEFGHJKMNP \ -H "X-Api-Key: al_live_…" # → HTTP 200 {} ``` A successful cancel or delete returns **HTTP 200** with an empty object `{}`. ## State machine | Task state | What `DELETE` does | Response | | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | --------------------- | | **Queued** (not yet running) | Cancels the task and **releases the credit hold in full** — you are charged 0. A `video.cancelled` webhook fires. | `{}` (200) | | **Running** | **Cannot be cancelled.** The provider refuses; Aurous forwards its refusal (status + body) verbatim. | provider-shaped error | | **Terminal** (`succeeded` / `failed` / `expired`) | **Hides the record** from retrieve and list. Money and audit rows are never hard-deleted. | `{}` (200) | | **Already cancelled** | Refused (provider parity) — a cancelled task cannot be cancelled again. | provider-shaped error | **Cancelling while queued is your lever to free pinned credits.** A task's hold can pin credits until it runs (or until `execution_expires_after`). If you no longer need a queued task, delete it — the hold releases immediately and the charge is 0. Once the task is **running** it can no longer be cancelled, and it will settle normally on success. ## Errors | Code | HTTP | When | | -------------------------------------------------- | ---- | ---------------------------------------------------------------------------------------- | | [`resource_not_found`](/errors#resource_not_found) | 404 | Unknown id, another team's task, or an already-hidden task. Indistinguishable by design. | | [`insufficient_scope`](/errors#insufficient_scope) | 403 | The API key lacks the `write` scope. | A refusal on a **running** or already-**cancelled** task is returned in the provider's own error shape, not remapped — that is the raw contract. See [Errors](/errors) for the envelope and support workflow. # Create a task Source: https://docs.aurous-labs.com/api-reference/seedance/create-task POST /v1/contents/generations/tasks — submit a Seedance video generation. Native Seedance request body, forwarded verbatim. `POST /v1/contents/generations/tasks` submits a video generation task. The request body is **shape-identical to the native Seedance video API** — Aurous forwards it to the provider verbatim, including any parameters not listed here, so future provider fields keep working. A credit ceiling is held on your team balance and reported on the `Aurous-Credits-Held` response header. * **Scope:** `write` * **Auth:** `X-Api-Key: al_live_…` or `Authorization: Bearer al_live_…` * **Content-Type:** `application/json` * **Max body size:** 64 MB (base64 data-URIs are accepted inline) ```bash cURL theme={null} curl https://api.aurous-labs.com/v1/contents/generations/tasks \ -H "X-Api-Key: al_live_…" \ -H "Content-Type: application/json" \ -d '{ "model": "seedance-2.0", "content": [{"type": "text", "text": "A kitten yawns at the camera"}], "resolution": "720p", "ratio": "16:9", "duration": 5 }' ``` ## Response **HTTP 200** — provider parity (not `201`). The body is exactly the task id, and nothing else: ```json theme={null} { "id": "vid_01J9Z3K7Q0XY2ABCDEFGHJKMNP" } ``` Billing information rides on **headers only** — the body is never modified: | Header | Meaning | | --------------------- | ------------------------------------------------------------------------------ | | `Aurous-Credits-Held` | Credit ceiling reserved for this task (released or settled at terminal state). | | `Aurous-Request-Id` | Correlation id for support. | Poll the returned id at [`GET /v1/contents/generations/tasks/{id}`](/api-reference/seedance/retrieve-task) until it reaches a terminal state. ## `model` The `model` field accepts a Seedance model id or one of its aliases. Aliases are the friendly names; both resolve to the same model. | Alias | Resolutions | Notes | | ------------------- | --------------------- | ------------------------------------------- | | `seedance-2.0` | 480p, 720p, 1080p, 4k | Full-resolution tier. 4k is this tier only. | | `seedance-2.0-fast` | 480p, 720p | Faster, lower cost. No 1080p/4k. | | `seedance-2.0-mini` | 480p, 720p | Lowest cost. No 1080p/4k. | The full provider model ids (e.g. `dreamina-seedance-2-0-260128`) are also accepted verbatim, so copy-pasted provider tutorial code runs unchanged. See [Models & pricing](/api-reference/seedance/models-and-pricing) for live rates and capabilities via [`GET /v1/models`](/api-reference/openapi#tag/models). An unknown model, an endpoint-style id, or a missing/empty `model` returns **`404 model_not_found`**. List available models with [`GET /v1/models`](/api-reference/openapi#tag/models) and pass a model id or alias — never an endpoint id. ## `content[]` `content` is a non-empty array of items. Each item is one of four types, and image / video / audio items carry a `role`: | Item `type` | `role` values | URL forms accepted | | ----------- | ---------------------------------------------- | ------------------------------------------ | | `text` | — | plain prompt text | | `image_url` | `first_frame`, `last_frame`, `reference_image` | `https://…` URL **or** base64 data-URI | | `video_url` | `reference_video` | `https://…` URL only (no base64 for video) | | `audio_url` | `reference_audio` | `https://…` URL **or** base64 data-URI | ```json theme={null} { "model": "seedance-2.0", "content": [ { "type": "text", "text": "She turns toward the camera and smiles" }, { "type": "image_url", "role": "first_frame", "image_url": { "url": "https://…/frame.jpg" } } ] } ``` ### Generation modes Beyond plain **text-to-video** (a single `text` item), visual inputs select one of three **mutually exclusive** modes: 1. **First-frame** — one `image_url` with `role: first_frame`. Animate forward from a starting frame. 2. **First + last frame** — a `first_frame` plus a `last_frame` image. Interpolate between two frames. 3. **Multimodal reference** — up to **9 reference images**, **3 reference videos** (each 2–15 s, ≤15 s total), and **3 reference audio** clips (≤15 s total). Audio never rides alone — it must accompany another input. That is the **"up to 12 reference files"** budget: 9 images + 3 videos + up to 3 audio refs. Aurous does **not** re-validate per-model rules — durations, resolution support, role combinations, and mode exclusivity are enforced by the provider, which returns its `400` verbatim. Duplicating those rules here would only drift from them. We read (never rewrite) `resolution`, `duration`, `ratio`, and whether any `video_url` item is present, purely to price the credit hold. ### Input constraints | Input | Limit | | ------------------ | ------------- | | Image file | ≤ 30 MB each | | Video file | ≤ 200 MB each | | Audio file | ≤ 15 MB each | | Whole request body | ≤ 64 MB | Reference videos must be passed as `https://` URLs — base64 inline is accepted for images and audio, but not for video. The provider enforces format, dimension, frame-rate, and duration rules on each file and returns a verbatim error if one is rejected; see [Errors](#errors). ### References with people Reference images and videos may contain real people. When you pass a person or face reference, the task is **accepted immediately** — you get a `vid_…` handle right away — and the reference is prepared automatically before generation begins. A task with such a reference may sit in `queued` a little longer while that preparation completes, then proceeds through `running` to `succeeded` like any other task. No extra fields or steps are required on your side; poll the task id as usual. If a reference ultimately can't be used, the task settles `failed` with a plain reason. For identity-consistent video that reuses the **same** person across many renders, the managed [characters](/api-reference/characters/create-character) path on [`POST /v1/videos`](/api-reference/videos/create-video) remains the richer option. ## Parameters All parameters below are optional; defaults are the Seedance defaults. Per-model support is noted where it differs. | Parameter | Type | Default | Notes | | ------------------------- | --------------------------------------------------------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `resolution` | `"480p" \| "720p" \| "1080p" \| "4k"` | `"720p"` | **1080p and 4k are `seedance-2.0` only** (4k is 10-bit HEVC). | | `ratio` | `"16:9" \| "4:3" \| "1:1" \| "3:4" \| "9:16" \| "21:9" \| "adaptive"` | `"adaptive"` | Output aspect ratio. | | `duration` | integer `4`–`15`, or `-1` | `5` | Output length in seconds. `-1` lets the model pick. `frames` is not supported on this family. | | `generate_audio` | boolean | `true` | Generate a mono audio track. Does not change the price. | | `watermark` | boolean | `false` | Burn a watermark into the output. | | `callback_url` | string | — | HTTPS URL that receives an **unsigned** `{ event, data }` completion callback (a prompt to fetch status). See [Differences → Callbacks](/api-reference/seedance/differences#callbacks). | | `return_last_frame` | boolean | `false` | Also return the final frame as a JPEG (an Aurous streaming URL, valid \~24 h). | | `execution_expires_after` | integer seconds `3600`–`259200` | `172800` (48 h) | How long the task may sit before the provider expires it. | | `priority` | integer `0`–`9` | — | Reorders your own queued tasks. `seedance-2.0` family only. | | `safety_identifier` | string ≤ 64 chars | — | Your per-end-user tag. Namespaced per team upstream, **echoed back unchanged** on retrieve. See [Differences](/api-reference/seedance/differences#safety-identifier). | `seed` and `camera_fixed` are **not supported** by the Seedance 2.0 family. `service_tier` is fixed to the online tier for this family. Unknown parameters are forwarded to the provider untouched. ## Errors Non-2xx responses fall into three classes. Knowing which class you are in tells you whether to fix the request, retry, or contact support. ### 1. Aurous pre-dispatch gates Conditions Aurous checks **before** the request reaches the provider. These use Aurous **snake\_case** codes inside the standard envelope `{"error":{ "type", "code", "message", "param", "doc_url", "request_id" }}`. | Code | HTTP | When | | ------------------------------------------------------------------ | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`model_not_found`](/errors#model_not_found) | 404 | Unknown model, an endpoint-style id, or a **missing/empty** `model` field. | | [`invalid_request`](/errors#invalid_request) | 400 | Malformed body, empty/invalid `content[]`, unsupported content type, or an unsupported resolution for the resolved model. | | [`balance_too_low`](/errors#balance_too_low) | 402 | Team balance is below the required credit ceiling (the message names the ceiling). | | [`insufficient_scope`](/errors#insufficient_scope) | 403 | The API key lacks the `write` scope. | | [`payload_too_large`](/errors#payload_too_large) | 413 | Request body exceeds 64 MB. | | [`too_many_requests`](/errors#too_many_requests) | 429 | Per-team request rate exceeded. `Retry-After` echoed. | | [`concurrency_limit_exceeded`](/errors#concurrency_limit_exceeded) | 429 | Too many non-terminal raw tasks in flight for your team (default cap **5**). Count-based — **no** `Retry-After`; retry once an in-flight task finishes, or cancel a queued/pending task. | | [`idempotency_key_in_use`](/errors#idempotency_key_in_use) | 409 | Same `Idempotency-Key` reused with a different body. | If your code switches on the provider's PascalCase error codes (`InvalidParameter`, `MissingParameter`, …), you will meet these Aurous snake\_case codes **first**, because they fire before the provider is called. The envelope is a strict superset of the provider's `{"error":{"code","message"}}`. See [Differences → Error envelope](/api-reference/seedance/differences#error-envelope). ### 2. Provider request-faults (forwarded verbatim) Problems with the request that only the provider can detect — e.g. `InvalidParameter`, `MissingParameter`, and output content checks (`Output*SensitiveContentDetected`, including their `.PolicyViolation` variants). These are returned with the **provider's exact status and body**. This is the point of the raw surface: you get the provider's own validation, unmodified. Input references containing real people are the exception: rather than surfacing as an error, they are prepared automatically and the task continues (see [References with people](#references-with-people)). ### 3. Account / infrastructure faults (remapped) Faults on the Aurous side of the connection that you cannot fix and that would leak our account posture are remapped: | Result | HTTP | Meaning | | ---------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------- | | [`provider_unavailable`](/errors#provider_unavailable) | 503 | A known upstream/account condition. `Retry-After: 30` echoed. Retry with backoff. | | [`provider_unknown_error`](/errors#provider_unknown_error) | 502 | An upstream error our mapping table does not yet recognize. Engineering is alerted; treat as transient. | See the [Errors](/errors) catalog for the full envelope, the `request_id` support workflow, and retry guidance. # Differences from the native API Source: https://docs.aurous-labs.com/api-reference/seedance/differences The honest delta list: what the Aurous raw surface changes vs. the native Seedance video API. The raw surface is a faithful passthrough — request and response bodies are shape-identical (with task identifiers Aurous-native), and the Ark SDK works by swapping `base_url` + key. But a passthrough that runs on a multi-tenant platform has a handful of documented differences. Here is the complete list, so nothing surprises you in production. ## Model identifiers You address models by **model id or alias** (`seedance-2.0`, `seedance-2.0-fast`, `seedance-2.0-mini`, or the full provider slug). Endpoint-style ids are **not** accepted — passing one, or any unknown model, returns **`404`** [`model_not_found`](/errors#model_not_found). See [Create a task → model](/api-reference/seedance/create-task#model). ## Rate & concurrency limits Limits are **per team**, sized to protect shared capacity: * **Request rate:** roughly **30 requests / minute sustained** across `POST` and `DELETE` (a burst bucket sits above that). Over the limit → [`too_many_requests`](/errors#too_many_requests) (429). * **Active-task cap:** at most **5 concurrent non-terminal raw tasks** (queued or running) per team. Over the cap → [`concurrency_limit_exceeded`](/errors#concurrency_limit_exceeded) (429). The request-rate 429 (`too_many_requests`) and any `503` carry a **`Retry-After`** header — sleep that many seconds and retry. The active-task 429 (`concurrency_limit_exceeded`) is **count-based**, so it carries **no** `Retry-After`: retry once an in-flight task reaches a terminal state, or cancel a queued/pending task to free a slot. See [Rate limits](/rate-limits) for headers and how buckets refill. Need a higher tier? Email **[support@aurous-labs.com](mailto:support@aurous-labs.com)**. ## Callbacks Your `callback_url` is **accepted and relayed through Aurous** — you do not have to poll. The relayed callback differs from a bare provider callback in a few honest ways: * **Body shape.** Aurous POSTs a JSON object `{ "event", "data" }`, where `event` is `video.completed`, `video.failed`, or `video.cancelled`, and `data` is the normalized task object — the same shape [Retrieve a task](/api-reference/seedance/retrieve-task#fields) returns. The task id is `data.id` (the Aurous `vid_…` value); on **this unsigned relay** there is **no** separate `task_id` field — the id already *is* the `vid_…` handle (the signed-webhook path is different — see [Signed webhooks for raw tasks](#signed-webhooks-for-raw-tasks)). A task that reaches the terminal `expired` status arrives as a **`video.failed`** callback (there is no `video.expired` event) — the provider status is preserved verbatim on `data.status`, so read that (or the authoritative retrieve) to tell `expired` apart from a plain `failed`. Both charge **0**. ```json theme={null} { "event": "video.completed", "data": { "id": "vid_01J9Z3K7Q0XY2ABCDEFGHJKMNP", "status": "succeeded", "content": { "video_url": "https://api.aurous-labs.com/v1/contents/generations/tasks/vid_01J9Z3K7Q0XY2ABCDEFGHJKMNP/output?token=…" }, "…": "…" } } ``` * **Unsigned.** The relayed POST carries only `Content-Type: application/json` — there is **no** `Aurous-Webhook-Signature` and no other `Aurous-*` headers. This is **not** the signed [registered-webhook-endpoints](/webhooks) envelope, so do not attempt signature verification on it. * **At-least-once, best-effort.** Delivery is attempted up to 3 times (5-second timeout per attempt), and the upstream may itself re-post — so you can receive **duplicates**. **De-duplicate on `data.id`.** Aurous **never bills from the posted body**; the charge always settles from a trusted server-side read-back. Because the callback is unsigned and arrives over the public internet, treat it as a **hint to fetch authoritative status**, not as trusted data. On each callback, read the record with [`GET /v1/contents/generations/tasks/{id}`](/api-reference/seedance/retrieve-task) — that `GET` is the source of truth for status and result. ## Signed webhooks for raw tasks Raw tasks **also** fire your signed [registered webhook endpoints](/webhooks) (created with `POST /v1/webhook_endpoints`) — separately from, and in addition to, the inline `callback_url` relay above. The events are `video.completed`, `video.failed`, and `video.cancelled`, each carrying the `Aurous-Webhook-Signature` header and verified exactly like every other Aurous webhook (see [Webhooks](/webhooks) for the signature mechanics — they are identical for raw and platform events). Correlate terminal webhooks via the **`data.id`** field (a `vid_…` task identifier). ## Safety identifier Your `safety_identifier` is **namespaced per team** before it reaches the provider — this keeps per-end-user abuse attribution scoped to your team and prevents collisions across tenants. It is **echoed back unchanged** on every retrieve and list item, so you always read your own original value. See [Create a task → parameters](/api-reference/seedance/create-task#parameters). ## List & retention supersets * **No 7-day list cutoff.** The native list only returns the last 7 days; the Aurous [list](/api-reference/seedance/list-tasks) has no such window and is served from your own team-scoped records (never proxied). * **Records outlive the provider purge.** A task stays retrievable on Aurous **past the provider's 7-day history purge**. The catch: `video_url` / `last_frame_url` inside a record still **expire 24 hours after generation** (a provider constraint), so an old record remains readable but its media links go stale. Download what you keep. * **Output URLs are Aurous-proxied.** `video_url` and `last_frame_url` point at Aurous streaming endpoints (`…/v1/contents/generations/tasks/{id}/output[/last_frame]?token=…`), not an upstream object store — the bytes stream through Aurous, so the underlying storage host is never exposed. They stay valid \~24 h after generation. ## Idempotency The raw `POST` accepts an **`Idempotency-Key`** header (a bonus over the native API) — reuse the same key to make a create safe to retry. A replay returns the original result and carries the **same `Aurous-Credits-Held` header**, so you are never double-held or double-charged. A same-key retry with a different body returns [`idempotency_key_in_use`](/errors#idempotency_key_in_use) (409). See [Idempotency](/idempotency). ## Error envelope Aurous checks some conditions **before** your request reaches the provider (auth, scope, balance, rate, request shape, model resolution). Those are returned with **Aurous snake\_case codes** inside the standard envelope: ```json theme={null} { "error": { "type": "invalid_request", "code": "balance_too_low", "message": "…", "param": null, "doc_url": "https://docs.aurous-labs.com/errors#balance_too_low", "request_id": "req_…" } } ``` This is a **strict superset** of the provider's `{ "error": { "code", "message" } }`: same two keys, plus `type`, `param`, `doc_url`, and `request_id`. If your integration switches on the provider's **PascalCase** codes (`InvalidParameter`, `MissingParameter`, …), you will meet these **snake\_case** codes **first** — they fire before the provider is called. Match on `error.code` and handle both vocabularies. The three error classes in full: 1. **Aurous pre-dispatch gates** — snake\_case codes, superset envelope (the shape above). 2. **Provider request-faults** — things only the provider can catch (`InvalidParameter`, `MissingParameter`, the `*SensitiveContentDetected` family). Returned with the **provider's exact status and body**. This is the raw contract — with one safe normalization: any upstream identifier that surfaces inside an `error.message` is rewritten to your namespace before it reaches you (provider task ids → your `vid_…`, and the provider model slug → the alias you addressed, e.g. `seedance-2.0-mini`), so no upstream vendor id or endpoint id ever leaks. The code, status, and human-readable text are otherwise untouched. 3. **Account / infrastructure faults** — remapped to [`provider_unavailable`](/errors#provider_unavailable) (503, `Retry-After: 30`) or, for unrecognized upstream errors, [`provider_unknown_error`](/errors#provider_unknown_error) (502). See [Create a task → errors](/api-reference/seedance/create-task#errors) for the per-code table and the [Errors](/errors) catalog for everything else. ## Reference-file budget A multimodal-reference request accepts **up to 12 reference files**: **9 images** + **3 videos** (each 2–15 s, ≤15 s total) + up to **3 audio** clips (≤15 s total, never alone). Details in [Create a task → generation modes](/api-reference/seedance/create-task#generation-modes). # List tasks Source: https://docs.aurous-labs.com/api-reference/seedance/list-tasks GET /v1/contents/generations/tasks — page and filter your Seedance tasks. No 7-day cutoff. `GET /v1/contents/generations/tasks` returns your team's Seedance tasks, newest first, in the native `{ total, items[] }` envelope. Each item is the same normalized task object returned by [Retrieve a task](/api-reference/seedance/retrieve-task). * **Scope:** `read` * **Auth:** `X-Api-Key: al_live_…` or `Authorization: Bearer al_live_…` This list is served **entirely from your Aurous records**, scoped to your team — it is never proxied to the provider (whose list is account-wide and would cross team boundaries). ```bash cURL theme={null} curl -G https://api.aurous-labs.com/v1/contents/generations/tasks \ -H "X-Api-Key: al_live_…" \ --data-urlencode "page_num=1" \ --data-urlencode "page_size=20" \ --data-urlencode "filter.status=succeeded" \ --data-urlencode "filter.model=seedance-2.0" ``` ## Query parameters | Parameter | Type | Notes | | ----------------- | ------------------- | -------------------------------------------------------------------------------------- | | `page_num` | integer `1`–`500` | 1-based page index. Clamped to range. | | `page_size` | integer `1`–`500` | Items per page. Clamped to range. | | `filter.status` | string | Filter by status. Accepts the provider vocabulary, including `expired`. | | `filter.task_ids` | string (repeatable) | Return only these task ids. Repeat the key to pass several. | | `filter.model` | string | Filter by model — accepts a model id or an alias (`seedance-2.0`, `…-fast`, `…-mini`). | The dotted parameter names (`filter.status`, `filter.task_ids`, `filter.model`) are **flat query keys**, not nested objects — pass them literally. To filter by several ids, repeat `filter.task_ids`: ```bash theme={null} curl -G https://api.aurous-labs.com/v1/contents/generations/tasks \ -H "X-Api-Key: al_live_…" \ --data-urlencode "filter.task_ids=vid_01J9Z3K7Q0XY2ABCDEFGHJKMNP" \ --data-urlencode "filter.task_ids=vid_02KA7B4C9DXY3EFGHJKMNPQRST" ``` ## Response ```json theme={null} { "total": 128, "items": [ { "id": "vid_01J9Z3K7Q0XY2ABCDEFGHJKMNP", "model": "seedance-2.0", "status": "succeeded", "content": { "video_url": "https://…" }, "…": "…" }, { "id": "vid_02KA7B4C9DXY3EFGHJKMNPQRST", "model": "seedance-2.0-mini", "status": "running", "…": "…" } ] } ``` * `total` — the count matching your filters across all pages. * `items[]` — normalized task objects, same shape as [Retrieve a task](/api-reference/seedance/retrieve-task#fields). **List superset (delta vs. native).** The native Seedance list has a **7-day history window**. The Aurous list has **no 7-day cutoff** — your full task history stays listable. As with retrieve, the `video_url` inside older items still expires 24 hours after generation. Deleted tasks (see [Cancel or delete](/api-reference/seedance/cancel-delete)) are hidden from this list. # Models & pricing Source: https://docs.aurous-labs.com/api-reference/seedance/models-and-pricing The Seedance rate card, the token formula, and the exact-price guarantee. Billed in Aurous credits. Seedance tasks are billed in **Aurous credits** at **\$0.01 per credit** (so credits = dollars × 100). You pay for the video that was actually rendered: a ceiling is held while the task runs, and the final charge is metered from the provider's reported tokens at success. This page gives the day-one rate card, the formula behind it, and the guarantee that keeps the two aligned. ## Models | Model | Resolutions | Best for | | ------------------- | --------------------- | ---------------------------------------------- | | `seedance-2.0` | 480p, 720p, 1080p, 4k | Full quality. The only tier with 1080p and 4k. | | `seedance-2.0-fast` | 480p, 720p | Faster and cheaper than Pro. | | `seedance-2.0-mini` | 480p, 720p | Lowest cost. | The full provider model ids (e.g. `dreamina-seedance-2-0-260128`) are accepted as `model` too. Live per-model rates and capabilities are on [`GET /v1/models`](/api-reference/openapi#tag/models). ## Rate card Prices below are for a **5-second, 16:9** render. Per-second prices scale linearly (÷5), and are exact at 16:9 and pixel-true within ±\~3% at other aspect ratios. ### Text / image input (no reference video) | Model | 480p | 720p | 1080p | 4k | | -------- | --------------- | -------------------- | ---------------- | ---------------- | | **Pro** | \$0.54  (54 cr) | **\$1.23  (123 cr)** | \$2.74  (274 cr) | \$5.74  (574 cr) | | **Fast** | \$0.44  (44 cr) | \$0.98  (98 cr) | — | — | | **Mini** | \$0.27  (27 cr) | \$0.61  (61 cr) | — | — | **Pro, per second (16:9):** $0.108 (480p) / $0.245 (720p) / $0.548 (1080p) / $1.148 (4k). Multiply by your output seconds for a fixed-duration text/image job. ### Video input (reference footage) When your `content[]` includes a reference video, the price depends on how much footage you send. These ranges span a **2-second** reference (low) to a **15-second** reference (high), for a 5-second output: | Model | 480p | 720p | 1080p | 4k | | -------- | ------------------------ | ------------------------- | ------------------------- | ---------------------------- | | **Pro** | \$0.45–1.30  (45–130 cr) | \$1.03–2.94  (103–294 cr) | \$2.30–6.58  (230–658 cr) | \$4.82–13.78  (482–1,378 cr) | | **Fast** | \$0.37–1.06  (37–106 cr) | \$0.82–2.34  (82–234 cr) | — | — | | **Mini** | \$0.23–0.65  (23–65 cr) | \$0.51–1.46  (51–146 cr) | — | — | ## The token formula Every price above comes from one formula. The provider meters the render in tokens: ```text theme={null} tokens = (input_video_seconds + output_seconds) × W × H × 24 / 1024 ``` Where `W × H` is the output pixel dimensions for your resolution and ratio, and `24` is the frame rate. The credit charge is `tokens × per-model rate`, at the anchor of \$0.01/credit. ### What moves your bill * **Resolution — quadratic.** Cost tracks `W × H`. Stepping 720p → 1080p roughly doubles both dimensions, so it more than doubles the tokens. This is the biggest lever. * **Duration — linear.** Each additional output second adds a fixed slice. * **Reference video — adds input seconds.** A reference clip adds its own seconds into the formula. That is why video-input jobs are priced as a range. * **Free:** generated audio, and image / text reference inputs. They do not change the price. ## The exact-price guarantee > We meter your exact input and output seconds after the render completes and charge that — a ceiling is held while the task runs, and explicit `duration` + fixed `ratio` + no video input ⇒ hold equals the final charge exactly. In practice: * **Fixed, no video input** — pass an explicit `duration`, a fixed `ratio`, and no reference video, and the **held ceiling equals the final charge exactly**. Nothing to reconcile. * **Adaptive duration (`duration: -1`)** — the hold covers the model's maximum length; the charge settles on the length actually delivered. * **Video input** — the hold assumes the maximum 15 seconds of reference footage (we never fetch your clip to measure it), so the held ceiling is an **upper bound** and the settled charge is typically lower. ## How billing works Billing rides on **response headers** — the response body carries no billing fields. 1. **Create** holds a credit ceiling → `Aurous-Credits-Held`. If your balance is below the ceiling, the request is rejected with [`balance_too_low`](/errors#balance_too_low) (402). 2. **Success** settles the charge on the provider's `usage.completion_tokens` → `Aurous-Credits-Charged` on the terminal [retrieve](/api-reference/seedance/retrieve-task#billing). 3. **`failed` / `expired` / (queued) `cancelled`** charge **0** and release the hold in full — you pay only for delivered videos. Aurous runs on **prepaid team credits**: a create is admitted only if the ceiling fits your balance. Cancel a queued task to release its hold immediately (see [Cancel or delete](/api-reference/seedance/cancel-delete)). ### Reading the charge in usage Each task shows up as a line item in [`GET /v1/usage`](/api-reference/openapi#tag/usage). The line's credit **`amount` is the total charged — not a sum of the breakdown rows.** While a task is held, its breakdown carries `tokens_max` (the ceiling basis); once complete, it carries `tokens` (the metered basis). ## Rates can change Seedance rates are **DB-driven and may change without an `Aurous-Version` bump** — the same mutability asymmetry documented for [LLM pricing](/api-reference/chat/pricing#mutability-asymmetry-vs-images-and-videos). Always read the current per-model rate from [`GET /v1/models`](/api-reference/openapi#tag/models) rather than hard-coding the numbers above. Each charged task snapshots the rate in force when it was created, so the amount you were quoted is the amount you pay. # Seedance raw API Source: https://docs.aurous-labs.com/api-reference/seedance/overview Seedance video generation over the Aurous Labs API — provider-shape bodies with Aurous-native identifiers. Point the Ark SDK at Aurous by changing two lines. The **raw Seedance API** is a passthrough to the Seedance video generation surface, mounted on the Aurous Labs V1 API. Request and response bodies are **shape-identical to the native Seedance video API** — the same `content[]` items, the same task object — with the task **identifiers Aurous-native** (the create echo is `{"id": "vid_…"}`), so tutorial code and the official Ark SDK run unchanged. The only thing that changes is where the request goes and how it is billed: * **Base URL** — `https://api.aurous-labs.com/v1` instead of the provider's host. * **Key** — your Aurous Labs API key (`al_live_…`) instead of a provider token. * **Billing** — in Aurous **credits**, metered on the render and settled at success. See [Models & pricing](/api-reference/seedance/models-and-pricing). Everything else — the model names (`seedance-2.0`, `seedance-2.0-fast`, `seedance-2.0-mini`), the parameters, the polling loop, the error bodies — is the Seedance contract you already know. Looking for identity-consistent video from your own reference footage, managed characters, or a first/last-frame helper with server-side asset handling? That is the value-add [`POST /v1/videos`](/api-reference/videos/create-video) surface, a different product. The raw API documented here forwards your body verbatim to Seedance and returns its response verbatim. ## Authentication Pass your key either way — both are accepted on every raw endpoint: ```text theme={null} X-Api-Key: al_live_ # or, what the Ark SDK sends by default: Authorization: Bearer al_live_ ``` The `Authorization: Bearer` form is the Ark SDK default, so pointing the SDK at Aurous Labs needs no auth changes. Get a key from the [dashboard](https://app.aurous-labs.com/dashboard). ## Endpoints | Method | Path | Purpose | | -------- | ------------------------------------- | ----------------------------------------------------------------------------------- | | `POST` | `/v1/contents/generations/tasks` | [Create a task](/api-reference/seedance/create-task) | | `GET` | `/v1/contents/generations/tasks/{id}` | [Retrieve a task](/api-reference/seedance/retrieve-task) (poll for status + result) | | `GET` | `/v1/contents/generations/tasks` | [List tasks](/api-reference/seedance/list-tasks) | | `DELETE` | `/v1/contents/generations/tasks/{id}` | [Cancel or delete a task](/api-reference/seedance/cancel-delete) | Tasks are identified by a `vid_…` id returned on create. The raw surface speaks `vid_…` ids only. ## 60-second quickstart Submit a text-to-video task, then poll until it succeeds. ```python Ark SDK (Python) theme={null} # Install the Ark SDK (its Python import is `byteplussdkarkruntime`), # then point it at Aurous Labs by setting base_url + api_key. from byteplussdkarkruntime import Ark client = Ark( base_url="https://api.aurous-labs.com/v1", api_key="al_live_…", ) task = client.content_generation.tasks.create( model="seedance-2.0", content=[{"type": "text", "text": "A kitten yawns at the camera"}], ) print(task.id) # vid_01J9Z3K7Q0XY2ABCDEFGHJKMNP ``` ```bash cURL theme={null} curl https://api.aurous-labs.com/v1/contents/generations/tasks \ -H "X-Api-Key: al_live_…" \ -H "Content-Type: application/json" \ -d '{ "model": "seedance-2.0", "content": [{"type": "text", "text": "A kitten yawns at the camera"}] }' # → HTTP 200 {"id": "vid_01J9Z3K7Q0XY2ABCDEFGHJKMNP"} ``` The create response is **HTTP 200** with the body `{"id": "vid_…"}` — provider parity, not a `201`. Your credit hold for the task is reported on the `Aurous-Credits-Held` response header; the body is never touched. ### Poll until it is done Generation is asynchronous. Retrieve the task on an interval until `status` reaches a terminal state (`succeeded`, `failed`, `expired`, or `cancelled`): ```python Ark SDK (Python) theme={null} import time while True: t = client.content_generation.tasks.get(task_id=task.id) if t.status in ("succeeded", "failed", "expired", "cancelled"): break time.sleep(10) if t.status == "succeeded": print(t.content["video_url"]) ``` ```bash cURL theme={null} curl https://api.aurous-labs.com/v1/contents/generations/tasks/vid_01J9Z3K7Q0XY2ABCDEFGHJKMNP \ -H "X-Api-Key: al_live_…" # → {"id":"vid_01J9Z3K7Q0XY2ABCDEFGHJKMNP","status":"succeeded","content":{"video_url":"https://…"}, …} ``` A comfortable poll interval is \~10–15 seconds. Prefer fewer polls? Pass a `callback_url` on create and Aurous relays an event to it when the task finishes — but the relayed callback is **unsigned** and best-effort, so treat it as a prompt to fetch authoritative status with a `GET`, not as trusted data. See [Differences → Callbacks](/api-reference/seedance/differences#callbacks). The `video_url` (and `last_frame_url`, if you requested it) **expires 24 hours after the video is generated** — a provider constraint. Download and store anything you want to keep. The task record itself stays queryable on Aurous well past that (see [Retrieve a task](/api-reference/seedance/retrieve-task#read-through-and-retention)). ## Next steps * [Create a task](/api-reference/seedance/create-task) — the full request reference: content items, modes, and every parameter. * [Retrieve a task](/api-reference/seedance/retrieve-task) — the task object, the status enum, and how the final charge settles. * [Models & pricing](/api-reference/seedance/models-and-pricing) — the per-model rate card, the token formula, and the exact-price guarantee. * [Differences](/api-reference/seedance/differences) — the honest delta list vs. the native Seedance API (limits, retention, callbacks, error envelope). # Retrieve a task Source: https://docs.aurous-labs.com/api-reference/seedance/retrieve-task GET /v1/contents/generations/tasks/{id} — poll a Seedance task for status, result, and the settled charge. `GET /v1/contents/generations/tasks/{id}` returns the full task object. Poll it until `status` reaches a terminal state. The response body is **shape-identical to the native Seedance task object** — the same fields, with task ids as Aurous-native `vid_…` identifiers and two fields normalized back to what you sent (see [Normalization](#normalization)). * **Scope:** `read` * **Auth:** `X-Api-Key: al_live_…` or `Authorization: Bearer al_live_…` ```bash cURL theme={null} curl https://api.aurous-labs.com/v1/contents/generations/tasks/vid_01J9Z3K7Q0XY2ABCDEFGHJKMNP \ -H "X-Api-Key: al_live_…" ``` ## Response ```json theme={null} { "id": "vid_01J9Z3K7Q0XY2ABCDEFGHJKMNP", "model": "seedance-2.0", "status": "succeeded", "created_at": 1731948000, "updated_at": 1731948042, "content": { "video_url": "https://api.aurous-labs.com/v1/contents/generations/tasks/vid_01J9Z3K7Q0XY2ABCDEFGHJKMNP/output?token=…", "last_frame_url": "https://api.aurous-labs.com/v1/contents/generations/tasks/vid_01J9Z3K7Q0XY2ABCDEFGHJKMNP/output/last_frame?token=…" }, "seed": 123456, "resolution": "720p", "ratio": "16:9", "duration": 5, "framespersecond": 24, "generate_audio": true, "safety_identifier": "your-original-value", "priority": 0, "draft": false, "draft_task_id": null, "service_tier": "online", "execution_expires_after": 172800, "usage": { "completion_tokens": 108000, "total_tokens": 108000 } } ``` ### Fields | Field | Type | Notes | | --------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | The `vid_…` task id. | | `model` | string | The canonical model id you targeted (normalized). | | `status` | enum | See [Status](#status). | | `error` | object | Present as `{ "code", "message" }` **only on a `failed` task**; omitted on every other status. | | `created_at` / `updated_at` | integer | Unix seconds. | | `content` | object | `{ "video_url", "last_frame_url"? }` on success. Both are **Aurous streaming URLs** — the bytes are proxied through Aurous, so they point at Aurous endpoints (`…/v1/contents/generations/tasks/{id}/output[/last_frame]?token=…`), never an upstream storage host. Valid **\~24 h** — download what you want to keep. `last_frame_url` present only if you passed `return_last_frame: true`. | | `seed` | integer | Seed the model selected. Returned only — `seed` is **not** a settable input on the 2.0 family (see [Create a task → parameters](/api-reference/seedance/create-task#parameters)). | | `resolution` | string | Output resolution. | | `ratio` | string | Output aspect ratio. | | `duration` **or** `frames` | integer | The delivered length. One of the two is present. | | `framespersecond` | integer | Output frame rate (24 for this family). | | `generate_audio` | boolean | Whether an audio track was generated. | | `safety_identifier` | string | **Your original value**, echoed back unchanged (see [Normalization](#normalization)). | | `priority` | integer | Queue priority you set. | | `draft` / `draft_task_id` | boolean / string | Draft-mode fields (not used by the 2.0 family). | | `service_tier` | string | `online` for this family. | | `execution_expires_after` | integer | Seconds the task may sit before expiring. | | `usage` | object | `{ "completion_tokens", "total_tokens" }`. **This is the billing basis** — see [Billing](#billing). For video models input tokens are always 0, so `total_tokens == completion_tokens`. | ## Status | `status` | Terminal? | Meaning | | ----------- | --------- | --------------------------------------------------------------------------------------- | | `queued` | no | Accepted, waiting for a worker. | | `running` | no | Generating. | | `succeeded` | yes | Done. `content.video_url` is populated. | | `failed` | yes | Generation failed. `error` carries the reason. Charged **0**. | | `expired` | yes | Task sat past `execution_expires_after` and was expired by the provider. Charged **0**. | | `cancelled` | yes | You cancelled it while queued. Charged **0** (full refund). | Poll until `status` is one of the four terminal values. A comfortable interval is \~10–15 seconds; a queued task may sit for a while (up to your `execution_expires_after`), so back off rather than hammering. A task whose `content` includes a person reference can spend a little extra time in `queued` while that reference is prepared — no different to poll for. ## Billing Billing on the raw surface is **hold-then-settle**, and rides on headers — the body is never modified. 1. **On create**, a **credit ceiling is held** and reported on `Aurous-Credits-Held`. 2. **At success**, the charge **settles on `usage.completion_tokens`** — the actual metered tokens the provider reports. The settled amount appears on the `Aurous-Credits-Charged` header of a terminal retrieve, and as a line item in [`GET /v1/usage`](/api-reference/openapi#tag/usage). 3. **You are charged only on success.** `failed`, `expired`, and (queued) `cancelled` tasks charge **0** and release the hold in full. | Header (terminal retrieve) | Meaning | | -------------------------- | -------------------------------------------------------------- | | `Aurous-Credits-Charged` | The exact credits deducted (present once `status: succeeded`). | | `Aurous-Request-Id` | Correlation id for support. | When you pass an explicit `duration`, a fixed `ratio`, and no reference video, the held ceiling **equals the final charge exactly** — there is nothing to reconcile. For a **video-input** task the hold assumes the maximum 15 s of reference footage (we never fetch your clip to measure it), so the held ceiling is an **upper bound** and the settled charge is typically lower. See [Models & pricing](/api-reference/seedance/models-and-pricing) for the full math. ## Normalization Two fields are restored to what you sent, so the response reflects your own inputs: * **`model`** — returned as the canonical model id/alias you targeted. * **`safety_identifier`** — returned as your original value (it is namespaced per team upstream for abuse attribution, then restored on the way out). Everything else in the task object is the provider's, verbatim. ## Read-through and retention * **Non-terminal tasks** are read live from the provider on each retrieve, so you always see the current status. * **Terminal tasks** are served from Aurous's stored copy. **Retention delta.** Aurous keeps your task records queryable **past the provider's 7-day history purge** — a superset of the native API. The `video_url` and `last_frame_url` are **Aurous streaming URLs** (the bytes are proxied through Aurous), and they still **expire \~24 hours after the video was generated**, so the record remains readable but its media links go stale. Download what you want to keep. ## Errors | Code | HTTP | When | | -------------------------------------------------- | ---- | -------------------------------------------------------------------------------------------------------- | | [`resource_not_found`](/errors#resource_not_found) | 404 | Unknown id, another team's task, or a deleted task. Indistinguishable by design (no cross-team leakage). | | [`insufficient_scope`](/errors#insufficient_scope) | 403 | The API key lacks the `read` scope. | A `vid_…` id that belongs to a non-raw generation is not addressable here and returns `404` — the raw surface and the [`/v1/videos`](/api-reference/videos/create-video) surface never cross-resolve ids. # Per-request usage event stream Source: https://docs.aurous-labs.com/api-reference/usage/event-stream Stream individual billed requests via GET /v1/usage/events — one row per billed call, the raw data underlying /v1/usage. `GET /v1/usage` returns aggregated buckets — `total_credits`, `total_input_tokens`, etc. summed over a window. When you need event-level granularity (one row per billed request, e.g. for FinOps reconciliation against per-call receipts), use `GET /v1/usage/events` instead. ## Shape ```bash theme={null} curl 'https://api.aurous-labs.com/v1/usage/events?start_time=2026-05-20T00:00:00Z&end_time=2026-05-20T10:00:00Z&limit=50' \ -H "X-Api-Key: $AUROUS_API_KEY" ``` ```json theme={null} { "object": "list", "data": [ { "id": "cmp_01HXMQ7Z3K8Y2VNABCDEFGHJKM", "type": "chat", "model": "aurous-grow-2.0-pro", "status": "completed", "created_at": "2026-05-20T08:14:23.491Z", "completed_at": "2026-05-20T08:14:25.137Z", "duration_ms": 1646, "input_tokens": 247, "output_tokens": 103, "credits_charged": 0.0291, "api_key_id": "apikey_01HXMQ7Z3KCDEFGHJKM23ABCDE", "user_id": "uuid-of-the-caller-user", "request_id": "req_01HXMQ7Z3K8Y2ABCDEFGHJKMZQ" }, { "id": "emb_01HXMQ7Z3K8Y2VNABCDEFGHJKM", "type": "embedding", "model": "aurous-embed-vision-1.0", "status": "completed", "created_at": "2026-05-20T08:13:50.221Z", "completed_at": "2026-05-20T08:13:50.987Z", "duration_ms": 766, "input_tokens": 412, "output_tokens": 0, "credits_charged": 0.00077, "api_key_id": "apikey_01HXMQ7Z3KCDEFGHJKM23ABCDE", "user_id": "uuid-of-the-caller-user", "request_id": "req_01HXMQ7Z3K8YJKMZQXVNCDEFGH" } ], "has_more": true, "next_cursor": "MDFIWE1RN1ozSzhZMkFCQ0RFRkdISktN..." } ``` Each row has the `id` from the original resource (`cmp_*` for chat, `emb_*` for embedding, `img_*` for image, `vid_*` for video), the type, the token counts, the credit charge, and pointers back to the API key + user. ## Query parameters ### Required * **`start_time`** (RFC 3339) — inclusive lower bound. Maximum lookback: 730 days. * **`end_time`** (RFC 3339) — exclusive upper bound; defaults to `now`. ### Filters * **`type`** — `chat` / `embedding` / `image` / `video` (comma-separated for multi-value) * **`status`** — `completed` / `failed` / `cancelled` / `processing` / `pending` * **`model`** — slug or comma-separated multi-value * **`api_key_id`** — `apikey_` * **`user_id`** — uuid * **`lora_id`** — `lora_` (image/video rows only) ### Pagination * **`limit`** — rows per page (default 100, max 500) * **`cursor`** — opaque forward-only cursor The cursor is short-lived and event-stream-specific (different from the `/v1/usage` page\_token). It encodes a `(created_at, id)` tuple — rows are returned in descending `created_at` order, ties broken by `id` ascending. ## Use cases ### Per-call audit against your ledger ```python theme={null} import requests # Pull every billed call from the last hour, reconcile against your billing ledger events = requests.get( "https://api.aurous-labs.com/v1/usage/events", params={"start_time": "2026-05-20T09:00:00Z", "end_time": "2026-05-20T10:00:00Z", "limit": 500}, headers={"X-Api-Key": "al_live_xxxxxxxxxxxxxxxx"}, ).json() for e in events["data"]: expected = your_ledger.find_by_request_id(e["request_id"]) if expected and abs(expected.credits - e["credits_charged"]) > 0.0001: print(f"DRIFT on {e['id']}: ledger {expected.credits} vs platform {e['credits_charged']}") ``` The `request_id` on each event matches the `Aurous-Request-Id` header you got on the original response — that's the canonical identifier for cross-system reconciliation. ### FinOps cost attribution `api_key_id` lets you attribute spend to a particular integration or environment (mint a separate API key per workload), and `user_id` tracks which team member kicked off the call (when calls go via the dashboard or a per-user-token integration). ### Replaying a window into your own analytics For long windows (multiple days), iterate the cursor and stream rows into your warehouse: ```python theme={null} import requests cursor = None while True: params = {"start_time": "2026-05-01T00:00:00Z", "end_time": "2026-05-20T00:00:00Z", "limit": 500} if cursor: params["cursor"] = cursor res = requests.get( "https://api.aurous-labs.com/v1/usage/events", params=params, headers={"X-Api-Key": "al_live_xxxxxxxxxxxxxxxx"}, ).json() for row in res["data"]: warehouse.insert(row) # your sink if not res.get("has_more") or not res.get("next_cursor"): break cursor = res["next_cursor"] ``` For sustained ingest, prefer [webhooks](/webhooks) — we POST a signed event the moment each row reaches a terminal status, so you don't have to poll. ## `/v1/usage/events` vs `/v1/usage` — when to pick which | Question | `/v1/usage` | `/v1/usage/events` | | ------------------------------- | ---------------------------- | ----------------------------------------- | | Total spend on chat last week? | ✅ aggregate sum | ❌ would have to sum | | Which call cost \$50? | ❌ no per-call | ✅ per-row charge | | Daily chart for the dashboard? | ✅ pre-bucketed | ❌ aggregate client-side | | Reconcile to my billing ledger? | ⚠️ aggregate only | ✅ row-by-row | | Audit a specific request\_id? | ❌ | ✅ filter by request\_id (via cursor walk) | | Compute p95 latency last hour? | ✅ duration\_ms\_p95 baked in | ⚠️ compute client-side | In short: `/v1/usage` is the **dashboard** view; `/v1/usage/events` is the **ledger** view. ## Where to next? * [Usage overview](/api-reference/usage/overview) — aggregated usage queries * [Usage pagination](/api-reference/usage/pagination) — cursor walk pattern * [Cost transparency](/guides/cost-transparency) — receipt math reconciliation * [Webhooks](/webhooks) — push-based event ingest as an alternative to polling * [`GET /v1/usage/events`](/api-reference/openapi#tag/account) — endpoint reference # Usage analytics Source: https://docs.aurous-labs.com/api-reference/usage/overview Query your team usage across chat, embedding, image, and video — by status, model, API key, user, and time bucket — via GET /v1/usage. `GET /v1/usage` is the customer-facing analytics surface. It serves the per-team dashboard charts (`/dashboard/usage`), and is the same API you can hit programmatically for billing reports, custom FinOps dashboards, and downstream observability. The endpoint returns time-bucketed usage rows, grouped along one or more dimensions (`type`, `model`, `api_key_id`, `user_id`, `status`) and filtered by any combination of those plus `lora_id` and `character_id`. ## Quick shape ```bash theme={null} curl 'https://api.aurous-labs.com/v1/usage?start_time=2026-05-01T00:00:00Z&end_time=2026-05-20T00:00:00Z&bucket_width=1d&group_by=type' \ -H "X-Api-Key: $AUROUS_API_KEY" ``` ```json theme={null} { "object": "list", "data": [ { "object": "usage.bucket", "bucket_start": "2026-05-20T00:00:00.000Z", "bucket_end": "2026-05-21T00:00:00.000Z", "groups": [ { "key": { "type": "t2i" }, "metrics": { "request_count": 383, "successful_count": 349, "failed_count": 16, "cancelled_count": 12, "credits_used": 19.4045, "duration_ms_p50": 30770.5, "duration_ms_p95": 196566.75, "image_count": 349, "video_seconds": 0, "total_input_tokens": 208004, "total_output_tokens": 3923 } }, { "key": { "type": "t2v" }, "metrics": { "request_count": 5, "successful_count": 5, "failed_count": 0, "cancelled_count": 0, "credits_used": 6.4, "duration_ms_p50": 27952, "duration_ms_p95": 32005.8, "image_count": 0, "video_seconds": 10, "total_input_tokens": 0, "total_output_tokens": 0 } } ] } ], "has_more": false, "next_page": null } ``` The response is **OpenAPI-shaped**: `data[*]` are time buckets, each with `groups[*]` for the slices. Each group's identity is in `key`; the numbers are in `metrics`. Whether you grouped by `type` alone or `type,model` together, this nesting stays the same — `key` carries every grouped field. ## Query parameters ### Required * **`start_time`** (RFC 3339 timestamp) — inclusive lower bound. Maximum lookback is 730 days. * **`end_time`** (RFC 3339 timestamp) — exclusive upper bound. Must be > `start_time`. Defaults to `now` if omitted. ### Bucket sizing * **`bucket_width`** — `1m` / `5m` / `15m` / `1h` / `1d` / `7d` / `30d`. Determines the granularity of the time-bucketed rows. Default: chosen automatically based on `(end_time - start_time)` to keep the result under the bucket cap. The bucket cap is approximately 2,000 buckets per response. Asking for `1m` buckets over a 30-day window would exceed this — the response returns `400 too_many_buckets` with a hint to either widen `bucket_width` or shrink the time range. ### Grouping dimensions * **`group_by`** (comma-separated list) — any combination of: `type`, `model`, `api_key_id`, `user_id`, `status`. Default: no grouping (one row per bucket). `group_by=type` is the most common shape — `t2i` / `t2v` / `chat` / `embedding` rows per bucket. `group_by=model` breaks LLM rows down by `aurous-grow-2.0-pro` vs `aurous-embed-vision-1.0`. `group_by=type,model` does both. The `lora_id` and `character_id` dimensions are NOT valid in `group_by` (they have too many values to be useful as a top-level rollup) but ARE accepted as filters. ### Filters The dimensions below act as filters (returns only rows where the filter matches). Multi-value: comma-separated OR repeated query parameter. * **`status`** — `completed` / `failed` / `cancelled` / `processing` / `pending` * **`type`** — `t2i` (text-to-image) / `i2i` (image-to-image) / `t2v` (text-to-video) / `i2v` (image-to-video) / `chat` / `embedding`. Note: this is the inference-type granularity — broader than `image` / `video`. To filter all image generation rows pass `?type=t2i,i2i`. * **`api_key_id`** — `apikey_` * **`user_id`** — Supabase user uuid * **`lora_id`** — `lora_` (filter only; not valid in `group_by`) * **`character_id`** — `cha_` (filter only; not valid in `group_by`) * **`model`** — slug, e.g. `aurous-grow-2.0-pro`. Multi-value: `?model=aurous-grow-2.0-pro,aurous-embed-vision-1.0` or `?model=aurous-grow-2.0-pro&model=aurous-embed-vision-1.0`. ### Pagination * **`limit`** — buckets per page (default 100, max 500). Each bucket carries all its groups regardless of `limit`. * **`page_token`** — opaque cursor from the previous response's `next_page`. Forward-only walk; the cursor encodes the query fingerprint so changing filters between pages returns `400 invalid_page_token`. See [Pagination](/api-reference/usage/pagination) for the cursor walk pattern and the known edge case. ## Bucket + group shape Each `data[*]` entry is a **bucket** (a time window). Inside, `groups[*]` are the grouped slices for that bucket. If you don't pass `group_by`, each bucket has exactly one group with `key: {}` (the un-grouped total). ```json theme={null} { "object": "usage.bucket", "bucket_start": "2026-05-19T00:00:00.000Z", "bucket_end": "2026-05-20T00:00:00.000Z", "groups": [ { "key": { "type": "chat" }, "metrics": { } }, { "key": { "type": "embedding" }, "metrics": { } }, { "key": { "type": "t2i" }, "metrics": { } }, { "key": { "type": "t2v" }, "metrics": { } } ] } ``` Empty buckets (no usage in the window) are NOT emitted — the response is a sparse list, not a dense time series. Client charts should fill zeros for missing buckets. ## Metric fields Every group's `metrics` includes: * **`request_count`** — total billed requests in the bucket * **`successful_count`** — terminal `completed` rows * **`failed_count`** — terminal `failed` rows (see [Known edge case](#known-edge-cases) below) * **`cancelled_count`** — terminal `cancelled` rows * **`credits_used`** — sum of `credits_charged` across all rows in the group (4dp) * **`image_count`** — total successful image-generation outputs (0 for chat / embedding / video buckets) * **`video_seconds`** — total successful video-generation duration in seconds (0 for chat / embedding / image buckets) * **`total_input_tokens`** — sum of input tokens for chat + embedding rows (0 for image / video) * **`total_output_tokens`** — sum of output tokens for chat rows (0 for embedding / image / video) * **`duration_ms_p50`** / **`duration_ms_p95`** — the 50th and 95th percentile end-to-end duration in milliseconds (only present when `request_count` ≥ 20; otherwise `null`) ## Known edge cases ### `failed_count` does NOT include `failed_provider_unavailable` The current implementation buckets these under a separate `errored_count` that is not yet surfaced on this endpoint. As a result, `successful_count + failed_count + cancelled_count` may be less than `request_count` for buckets where some requests hit `failed_provider_unavailable`. A fix to either rename `failed_count` to `errored_count` (inclusive) or surface separate counters is on the [v1.1 roadmap](/changelog). ### Pagination drops groups when `limit < group_count` If a bucket has, say, 3 groups (chat + embedding + t2i) and you set `limit=1`, the response will return that bucket with the first 1 group and a `next_page` cursor. Calling the cursor returns nothing (the cursor encodes the bucket boundary; the engine has already moved past it). Workaround: choose a `limit` larger than your largest bucket's group count. With `group_by=type` that's ≤ 6 (t2i, i2i, t2v, i2v, chat, embedding). Fix on the v1.1 roadmap. ## Streaming individual events If you need event-level granularity (one row per billed request), see [`GET /v1/usage/events`](/api-reference/usage/event-stream) — the same data without aggregation. ## Where to next? * [Usage pagination](/api-reference/usage/pagination) — cursor walk + known limitations * [Usage event stream](/api-reference/usage/event-stream) — per-event detail rows * [Cost transparency](/guides/cost-transparency) — reconciling totals against per-call receipts * [`GET /v1/usage`](/api-reference/openapi#tag/account) — endpoint reference # Usage pagination Source: https://docs.aurous-labs.com/api-reference/usage/pagination How the GET /v1/usage cursor walk works — and the known edge case when limit is smaller than the per-bucket group count. `GET /v1/usage` is a forward-only cursor pagination — pass `limit` for the page size and use the response's `next_page` token to walk forward. The cursor encodes the **query fingerprint** so filters cannot drift between pages. ## Basic walk ```bash theme={null} # Page 1 curl 'https://api.aurous-labs.com/v1/usage?start_time=2026-04-01T00:00:00Z&end_time=2026-05-01T00:00:00Z&bucket_width=1d&group_by=type&limit=10' \ -H "X-Api-Key: $AUROUS_API_KEY" # Response: # { # "object": "list", # "data": [/* 10 buckets */], # "has_more": true, # "next_page": "eyJ2IjoxLCJxZiI6IjlmYW..." # } # Page 2 — pass the cursor in `page_token` curl 'https://api.aurous-labs.com/v1/usage?page_token=eyJ2IjoxLCJxZiI6IjlmYW...' \ -H "X-Api-Key: $AUROUS_API_KEY" ``` When `page_token` is provided, you do NOT need to re-send `start_time`, `end_time`, `bucket_width`, `group_by`, or filter parameters — they're encoded in the cursor. In fact, sending them with **different values** mismatches the fingerprint and returns: ```json theme={null} { "error": { "type": "invalid_request", "code": "invalid_page_token", "message": "page_token fingerprint mismatch — query parameters drifted between pages." } } ``` This is intentional. A cursor that silently re-resolves params (e.g. resolves a new `now` for `end_time` on page 2) would return different data than page 1 sees — silent skew. The fingerprint check is Stripe-grade and we don't relax it. ## Cursor lifetime * Tokens expire 24 hours after the page-1 response that minted them. Walking pages slower than 24 hours apart returns `400 invalid_page_token` with `token_expired` detail. * Tokens are scoped to your team — using a cursor from team A's response against team B's API key returns `400 invalid_page_token`. ## Cursor opacity The `next_page` value is base64url-encoded JSON with version + query fingerprint + last-bucket-start + expiry. Don't parse it yourself — treat it as opaque. Future versions of the platform may add fields or change the encoding entirely. ## Known edge case — `limit` smaller than per-bucket group count The cursor encodes `last_bucket_start` (the timestamp of the last bucket returned on the current page). If a bucket has N groups and your `limit` is less than N: * Page 1 returns the bucket with the first `limit` groups; `has_more: true`; `next_page` encodes that bucket's `bucket_start` * Page 2 — using the cursor — starts AFTER `bucket_start`, so the remaining groups in that bucket are LOST In practice this matters when: * Your `group_by` is multi-dimensional (e.g. `group_by=type,model` could produce 8 groups per bucket: 4 types × 2 models) * Your `limit` is unusually small (\< the maximum expected groups-per-bucket) **Recommended workaround**: set `limit` ≥ the max possible groups per bucket. For a single `group_by` like `type` (≤ 4 groups), `limit ≥ 4` is safe. For multi-dim `group_by`, multiply: `group_by=type,model` → `limit ≥ 16` is safe up to 8 models. The defaults (`limit=100`) are generous enough that most production callers never hit this. The proper fix is to paginate at the **bucket level** (return whole buckets, never split a bucket's groups across pages). It's on the [v1.1 roadmap](/changelog). ## Stable ordering Within a response, buckets are ordered by `bucket_start` ascending (oldest first). Within a bucket, groups are ordered by total credit charge descending (highest spend first), then by `dimensions` alpha-sort tie-break. The order is stable across page walks for a given fingerprint — paging forward through a 1000-row window deterministically returns the same row order whether you do it in one shot or in five 200-row pages. ## Sample code ```typescript Node.js theme={null} async function fetchAllBuckets(params: Record): Promise { const buckets: unknown[] = []; let pageToken: string | undefined = undefined; for (;;) { const url = new URL("https://api.aurous-labs.com/v1/usage"); if (pageToken) { url.searchParams.set("page_token", pageToken); } else { for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v); } const res = await fetch(url, { headers: { "X-Api-Key": process.env.AUROUS_API_KEY! } }); const body = await res.json() as { data: unknown[]; has_more: boolean; next_page: string | null }; buckets.push(...body.data); if (!body.has_more || !body.next_page) break; pageToken = body.next_page; } return buckets; } const all = await fetchAllBuckets({ start_time: "2026-04-01T00:00:00Z", end_time: "2026-05-01T00:00:00Z", bucket_width: "1d", group_by: "type", limit: "500", }); ``` ```python Python theme={null} def fetch_all_buckets(params: dict) -> list: buckets = [] next_page = None while True: query = {"page_token": next_page} if next_page else params res = requests.get( "https://api.aurous-labs.com/v1/usage", params=query, headers={"X-Api-Key": AUROUS_API_KEY}, ).json() buckets.extend(res["data"]) if not res.get("has_more") or not res.get("next_page"): break next_page = res["next_page"] return buckets all_buckets = fetch_all_buckets({ "start_time": "2026-04-01T00:00:00Z", "end_time": "2026-05-01T00:00:00Z", "bucket_width": "1d", "group_by": "type", "limit": "500", }) ``` ## Where to next? * [Usage overview](/api-reference/usage/overview) — the full query surface * [Usage event stream](/api-reference/usage/event-stream) — per-event detail rows * [`GET /v1/usage`](/api-reference/openapi#tag/account) — endpoint reference # Get the generated video Source: https://docs.aurous-labs.com/api-reference/videos-proxy/get-the-generated-video /api-reference/openapi.json get /v1/videos/{id}/output Returns the generated video. Video outputs are retained for ~24 hours after generation; after that the endpoint returns `410 Gone` with `code: output_expired`. If the generation never produced an output (status `failed`, `cancelled`, `moderation_rejected`, or polling-timeout `expired`), this endpoint returns `422 Unprocessable Entity` with `code: output_not_available` — check `GET /v1/images/{id}` for the failure reason (the generation lookup endpoint accepts both `img_*` and `vid_*` IDs; there is no `GET /v1/videos/{id}`). Range requests (`Range: bytes=…`) are answered with `206 Partial Content` so HTML5 `