Skip to main content

API

The Ochra API

The Ochra API lets your own servers and scripts do what you do on the website: generate images, video and audio, read your library, characters and projects, upload files and check your credits. Every request uses a key you make in Settings, acts as your account and is charged to your balance at the prices the website shows.

  • REST and JSON over HTTPS
  • Same prices as the website
  • A monthly cap on every key
  • An OpenAPI 3.1 description

What it is

A REST API over HTTPS that answers JSON, at https://www.ochra.io/api/v1. It runs the same tools as the website, on the same models, at the same prices, and what it makes lands in your library beside everything you make on the site.

It is for your own servers and scripts: a shop that makes product shots, a backend that turns a brief into a video, a nightly job. To use Ochra from an AI app such as Claude, add the Ochra connector in that app instead; it needs no key.

Get a key

  1. Sign in, open Settings → API keys and choose New key.
  2. Name it after what will use it, choose Read or Read and generate, set its monthly cap in credits and choose Make key.
  3. Copy the key when it is shown. It is shown once: keep it in your server’s secrets, never in code you commit.
  • Every key has a cap: the most credits it may spend in any 30 days, from 10 to 100,000 (1,000 unless you choose). Your balance still has to cover each request. GET /api/v1/me shows a key its cap and what it has spent in the last 30 days.
  • You can also limit a key to some kinds of media or some models, and lower or raise its cap later.
  • Up to 10 keys at a time. Revoking one stops it on the next request; we email you whenever a key is made.

Settings → API keys

Authentication

Send the key on every request, in the Authorization header. Keys look like och_live_xxxxxxxx…; the samples below read yours from an environment variable, OCHRA_API_KEY.

Every request
Authorization: Bearer och_live_xxxxxxxx…

Call the API from your server only. A key in a web page or an app is a key anyone can copy, so the API sends no CORS headers, and a request that carries an Origin header, as a browser's does, is refused with 403. Never put a key in a URL, and revoke one you think has leaked: it stops on the next request.

Your first request

Check the key: GET /me answers who the key acts as, your balance and its limits. It costs no credits.

curl — check the key
curl https://www.ochra.io/api/v1/me \
  -H "Authorization: Bearer $OCHRA_API_KEY"

Price an image before you make it: the same request with ?price_only=true answers what it would cost and whether your balance covers it, and holds nothing.

curl — the price
curl -X POST "https://www.ochra.io/api/v1/images?price_only=true" \
  -H "Authorization: Bearer $OCHRA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"prompt": "A lighthouse on a cliff at dawn, soft film photograph", "count": 1}'
The answer, shortened
{
  "tool_name": "by_prompt",
  "total": 3,
  "balance": 1240,
  "affordable": true,
  "shortfall": 0,
  …
}

Then make it. The credits are held and the task comes back at once, with 202.

curl — make an image
curl -X POST https://www.ochra.io/api/v1/images \
  -H "Authorization: Bearer $OCHRA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"prompt": "A lighthouse on a cliff at dawn, soft film photograph", "count": 1}'
curl — poll the task
curl https://www.ochra.io/api/v1/tasks/TASK_ID \
  -H "Authorization: Bearer $OCHRA_API_KEY"

The same loop in JavaScript, with fetch, from a server:

JavaScript — start, poll, read the files
// On your server (Node 18 or later) — never in a web page.
import { randomUUID } from 'node:crypto';

const API = 'https://www.ochra.io/api/v1';
const auth = { Authorization: `Bearer ${process.env.OCHRA_API_KEY}` };
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function call(path, init = {}) {
  for (;;) {
    const response = await fetch(`${API}${path}`, { ...init, headers: { ...auth, ...init.headers } });
    const body = await response.json();
    // 429: a window is full and nothing was run. Wait as long as it says, then send the same request
    // again (a generation keeps its Idempotency-Key, so it is never made twice).
    if (response.status === 429) {
      await sleep(Number(response.headers.get('Retry-After') ?? 5) * 1000);
      continue;
    }
    if (!response.ok) throw new Error(`${body.error.code}: ${body.error.message} (${body.request_id})`);
    return body;
  }
}

// 1. Start it. The credits are held and the task comes back at once (202).
let task = await call('/images', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'Idempotency-Key': randomUUID() },
  body: JSON.stringify({ prompt: 'A lighthouse on a cliff at dawn, soft film photograph', count: 1 }),
});

// 2. Poll until the task is finished: every 5 seconds at first, then every 15 (a video takes minutes).
//    Every poll is a request, counted against the key's 60 a minute and 2,000 a day.
const finished = ['completed', 'partial', 'failed', 'cancelled', 'held_for_review'];
let wait = 5000;
while (!finished.includes(task.status)) {
  await sleep(wait);
  wait = Math.min(wait * 2, 15000);
  task = await call(`/tasks/${task.id}`);
}

// 3. The files are in `outputs`; each `url` is signed and expires, so download it soon.
for (const output of task.outputs) console.log(output.kind, output.url);

Idempotency

Every request that starts a generation, and every change to a project, needs an Idempotency-Key header: a UUID you make, new for each new request. If the answer is lost, send the same request again with the same key: you get the first task back (200, with Idempotency-Replayed: true) and are not charged twice.

A key you already used, sent with a different request, is refused with 409 conflict and charges nothing. A request refused before anything was held — a wrong field, too few credits — makes no task, so the same key can be sent again once you have fixed it.

Tasks and polling

Generation takes time, so it is a task. The request is checked and priced, the credits are held, and the answer is 202 with the task while it renders. Images usually take 10–60 seconds and videos 1–6 minutes.

202 — the task, shortened
{
  "id": "8f0c6a52-0000-4000-8000-000000000001",
  "status": "processing",
  "tool_name": "by_prompt",
  "progress": { "total": 1, "done": 0, "failed": 0 },
  "outputs": [],
  …
}

Poll GET /tasks/{id} every 5 seconds or so, and every 15 for a video, until status is completed, partial, failed, cancelled or held_for_review. Every poll is a request, counted against the key's 60 a minute and 2,000 a day; to follow several tasks, read them together with GET /tasks/batch. A 429 means wait the Retry-After seconds and carry on: nothing was lost.

The files are in outputs, each with a signed url that expires, so download what you keep. You are charged only for what was delivered; anything that failed is refunded by itself.

Errors

Every error is the same JSON: an HTTP status, error.code, a sentence you can show, and sometimes meta with the details. Every answer carries X-Request-Id; include it when you write to us.

402 — a request larger than what is left of a key’s cap
{
  "status_code": 402,
  "detail": "This request needs 29 credits, but this key has 4 of its 30-day limit of 1000 credits left (996 used). Raise the limit in Settings → API keys.",
  "error": {
    "code": "key_cap_reached",
    "message": "This request needs 29 credits, but this key has 4 of its 30-day limit of 1000 credits left (996 used). Raise the limit in Settings → API keys.",
    "meta": { "reason": "key_cap_reached", "cap_credits": 1000, "spent_credits": 996, "required_credits": 29 }
  },
  "request_id": "6f1c2a9e-0000-4000-8000-000000000002"
}
  • 400 bad_request

    The request could not be read: a generation or project change sent without an Idempotency-Key header, or with one that is not a UUID, or a body that is not valid JSON. Nothing was run.

  • 401 unauthenticated

    No key, a key that is not recognised, or a revoked key. Check the Authorization header.

  • 402 insufficient_credits

    Your balance cannot cover this request. Nothing was held; top up and send it again.

  • 402 key_cap_reached

    This request would take the key past its cap. meta.required_credits is what the request needs, meta.cap_credits the cap and meta.spent_credits what the key has used in the last 30 days. Send a smaller request, raise the cap in Settings → API keys, or use another key.

  • 403 forbidden · meta.reason: browser_origin

    The request carried an Origin header, as one from a web page does. Call the API from your server; never put a key in a web page.

  • 403 forbidden · meta.reason: scope

    The key has Read access and the request would generate or change something.

  • 403 forbidden · meta.reason: kind_not_allowed / model_not_allowed

    The request is outside the limits set on the key: a kind of media or a model it may not use. meta.allowed_models lists what it may.

  • 403 forbidden · meta.reason: customer_key_on_mcp

    A key was sent to the MCP endpoint. To use Ochra from an AI app, add the Ochra connector there instead.

  • 404 not_found

    No such thing in your account — or it belongs to another account, which looks the same.

  • 409 conflict

    The Idempotency-Key was already used for a different request. Nothing was charged; send a new key for a new request.

  • 422 validation_error

    A field is missing or wrong. error.details names each field and what to change.

  • 429 rate_limited

    Too many requests in one of the windows below. Wait the Retry-After seconds, then retry; nothing was run.

  • 500 internal_error

    Something failed on our side. Retry later; if it keeps happening, send us the request_id.

  • 503 maintenance

    The site has reached its generation limit for the day, or is briefly down for maintenance. Nothing was charged. Try again after Retry-After seconds; the daily limit resets at 00:00 UTC.

Limits

Every answer to a request with a key carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset for the window nearest to refusing you. A refused request is a 429 rate_limited with Retry-After in seconds, and spends nothing.

  • 60 per minute · 2,000 per day

    Per API key

    Every authenticated request, per API key.

  • 10 per minute

    Per API key

    Requests that create a generation task (POST /generations, /images, /videos, /photoshoots and the character endpoints), per API key.

  • 300 per day

    Per account (all its keys together)

    Requests that create a generation task the account has the credits for, counted across every API key of one account together.

  • 600 per hour

    Per deployment (all customer keys together)

    Requests that create a generation task the account has the credits for, counted across every customer’s API keys together.

  • 30 per hour

    Per API key

    POST /assets/import (the server downloads a URL), per API key, whether or not the import succeeds.

  • 30 per 5 minutes

    Per client network (hashed; IPv6 by /64)

    Failed authentications that reach the key table (an unknown or revoked key, or an owner who may not use it), per client network: the IPv4 address, or the /64 of an IPv6 one. A missing or malformed key is refused without being counted.

Prices

The API charges exactly what the website charges, in credits from the same balance, and the price of every request is known before it runs (?price_only=true, or POST /price). The current prices are on the pricing page, and GET /models?view=summary lists every model with what one image or clip costs.

Endpoints

Paths are under https://www.ochra.io/api/v1. Marked ones need a key with Read and generateaccess, and an Idempotency-Key where noted.

Your account

  • GET/api/v1/me

    The key, its owner, balance and limits

  • GET/api/v1/openapi.json

    This reference, for a machine

  • GET/api/v1/ledger

    Your credit ledger, newest first

Models and prices

  • GET/api/v1/models

    Tools and models you can generate with

  • GET/api/v1/models/{tool}

    One tool: models, caps and input schema

  • GET/api/v1/models/{tool}/voices

    The voices a text-to-speech model speaks with

  • POST/api/v1/price

    Price a request before you run it

  • GET/api/v1/photoshoot-categories

    Photo shoot categories

Generate

  • POST/api/v1/generations

    Run any tool (raw tool call)Read and generate · Idempotency-Key

  • POST/api/v1/images

    Generate images from a prompt, images or a characterRead and generate · Idempotency-Key

  • POST/api/v1/videos

    Generate a video from a prompt, frames, references or a characterRead and generate · Idempotency-Key

  • POST/api/v1/photoshoots

    A photo shoot of a person or a characterRead and generate · Idempotency-Key

Tasks

  • GET/api/v1/tasks

    Your tasks, newest first

  • GET/api/v1/tasks/{id}

    A task: status, calls, outputs and cost

  • GET/api/v1/tasks/batch

    Several tasks by id, each in full

Your library

  • GET/api/v1/generations

    Your outputs (the library), newest first

  • GET/api/v1/generations/{id}

    One output with the prompt and parameters that made it

  • POST/api/v1/generations/delete

    Delete generated files from your libraryRead and generate

  • GET/api/v1/media/{id}

    One file by asset id, with its short links

  • GET/api/v1/media

    Several files by asset id, as a gallery shows them

Characters

  • GET/api/v1/characters

    Characters you own or that are shared with you

  • GET/api/v1/characters/{id}

    A character: sheet, cover and reference photos

  • POST/api/v1/characters

    Create a character, as the Character Generator doesRead and generate · Idempotency-Key

  • POST/api/v1/characters/{id}/cover

    Make a character's cover portraitRead and generate · Idempotency-Key

  • POST/api/v1/characters/{id}/sheet

    Rebuild a character's sheetRead and generate · Idempotency-Key

Projects

  • GET/api/v1/projects

    Your projects, newest first

  • POST/api/v1/projects

    Start a projectRead and generate · Idempotency-Key

  • GET/api/v1/projects/{id}

    A project and what it holds

  • POST/api/v1/projects/{id}/rename

    Rename a projectRead and generate · Idempotency-Key

  • POST/api/v1/projects/{id}/add

    Put generated files, uploads and characters into a projectRead and generate · Idempotency-Key

  • POST/api/v1/projects/{id}/remove

    Take things out of a projectRead and generate · Idempotency-Key

  • POST/api/v1/projects/{id}/delete

    Delete a project (its contents are kept)Read and generate · Idempotency-Key

Your files

  • POST/api/v1/assets

    Reserve an upload and get a URL to PUT the file toRead and generate

  • POST/api/v1/assets/{id}/confirm

    Confirm an upload so it can be usedRead and generate

  • POST/api/v1/assets/import

    Import a file from an https URLRead and generate

  • GET/api/v1/assets/{id}

    One of your assets with a fresh URL

  • GET/api/v1/assets

    Your own files (uploads and imports), newest first

  • POST/api/v1/assets/delete

    Delete files you uploaded or importedRead and generate

The OpenAPI document

Every endpoint, field and error above, as an OpenAPI 3.1 document your tools can read — generated from the same contract the API checks requests with, so it is never behind it. It needs no key; with your key it describes exactly what that key may call. Send its ETag back as If-None-Match and an unchanged document answers 304.

https://www.ochra.io/api/v1/openapi.json

Make your first key

Choose what it may do and how much it may spend, then copy it into your server's secrets.