Calling the API
A program can change a project, ask it for a job, follow the job, stop it and read its log. A job is what the studio’s Make screen starts: a build, a few minutes of one, new takes, an update or a Yoto upload. Every path starts with /api/v1/, and the calls here are written from there. Requests and answers are JSON: a request with a body sends it as Content-Type: application/json. A project’s calls are under projects/{project}/, with the project’s ID from GET projects.
POST /api/v1/projects/{project}/jobs
Content-Type: application/json
Idempotency-Key: 9b1d4e
{"kind": "build", "minutes": 10}
| Call | What it does |
|---|---|
GET projects | The projects you may open, each with its ID. |
GET projects/{project} | The project, with its revision. |
GET projects/{project}/commands | Every command the project takes, each with its cost class, any job it starts (starts), the role it needs and the JSON Schema of its body. A command’s cost counts the job it starts: queue-line-takes is run-costly, since it starts a takes job. |
POST projects/{project}/commands/{name} | Runs one named command, such as rename-character with {"id": "hare", "name": "Mr. Hare"}. See Making a change. |
POST projects/{project}/jobs | Asks for a job. See Jobs. |
GET projects/{project}/jobs | The jobs, newest first, a page at a time (see reads), with limit from 1 to 100 (50 if not given). |
GET projects/{project}/jobs/{id} | One job: what it is, how far it has got, and what it used. |
POST projects/{project}/jobs/{id}/stop | Stops the job, or takes it out of the queue while it waits. It answers 409 if the job has already ended. |
GET projects/{project}/jobs/{id}/log | The last lines of the job’s log. |
GET projects/{project}/events | What happens, as it happens. See Following along. |
GET openapi.json | The whole API, as OpenAPI 3.1. Its public part is here too. |
Signing in
A program signs in to the hosted API with an API key, sent with every call in its Authorization header:
GET /api/v1/projects
Authorization: Bearer ai1y_<key id>_<secret>
- Made by a member of your account. A key starts with
ai1y_, then its id, which is safe to show and log, then its secret. The secret is shown once, when the key is made, and is kept only as a hash, so save it then. Its last characters are a checksum, so a typo can be told from a revoked key without asking. - Scoped. A key reaches the projects it names, or every project of the account; with a role, at most its maker’s; and spends only the costs it is given:
read,edit,run-cheapandrun-costly. A key for an agent starts withreadandedit. - One answer for a key that doesn’t work. An unknown, wrong, revoked, stopped or expired key is
401unauthorized, all alike, so the answer tells a stranger nothing. A project outside a key’s scope isnot-found, as one that doesn’t exist is, and a role or a cost beyond it isforbidden, naming the cost. - Paused, stopped and rotated. The account’s owner or an admin, or the key’s maker, can pause a key, for costly runs or for everything; the owner can stop one at once. Rotating a key makes a new one with the same scope and gives the old one 24 more hours, so a program moves over without a gap. Each takes effect on the next call.
- Never in an address. A key is taken only in the
Authorizationheader. One sent in an address is refused and revoked at once, since addresses end up in logs.
Reading a project
Everything a command changes can be read at its own address under projects/{project}/, so a program can read exactly what it is about to change. Screens that put many things together are views, and audio and the book’s files are under media/:
| Reads | Addresses under projects/{project}/ |
|---|---|
| Subjects | sentences/{id} and sentences/{id}/history, words/{word}, lines/{key}/borrow-sources, characters and characters/{id}, settings, history. One the project doesn’t have is 404. |
| Views | views/studio, views/contents, views/fixes, views/sections/{n}, views/timeline/{n}, views/joins/{part}, views/heard-now/{part}, and views/cleanup-preview?clip=…&noise=…&rumble=…. Each answers as the studio’s own screen reads it. |
| Audio and files | media/readings/{take}, media/lines/{key}/borrow-sources/{source}, media/heard-now/{version} and media/cleanup/{which}, as WAV, and media/files/cast/… or media/files/package/… for the project’s files, with ranges. |
- Two tags. Every read answers with an
ETagof what it sent, such as"c-…", andIf-None-Matchwith it gets304when nothing changed. It also answers with the project’s revision inAi1y-Revision, such as7. Cache by the ETag, and send the revision in a command’sIf-Match, in quotes, as"7". A command sent an ETag there is refused with428. - One shape for lists. Every list answers
{items, next}; ask for the next page withbefore=and thenextyou got, which is null on the last page.
Making a change
To make a change, do what the studio’s own page does: send a named command with the revision you last read and a key of your own.
POST /api/v1/projects/{project}/commands/set-say
Content-Type: application/json
If-Match: "7"
Idempotency-Key: 3f2c9a
{"key": "k1", "say": "Mister Tortoise"}
- The revision in
If-Matchis theAi1y-Revisionfrom your last read, or therevisionin your last command’s answer. Without it the command is refused with428. Commands listed with"revision": falseincommandsonly add or run something and don’t look at it, so it’s fine to always send it. - A
409with the typestale-revisionmeans what you’re changing was changed since you read it, perhaps on another device. Read it again and decide; don’t just send it again. - No answer? Send the command again with the same
Idempotency-Key: it is made once, and answered as the first time was. While the first is still being made, the answer iscommand-in-flight, so wait and try again. - Undo. A command answers with
{revision, subject, undo}, and the undo comes back as{"command", "body"}: send that command with that body. - Try it first.
?dry_run=truechecks the command and says what it would touch, without changing anything. - Saved whole. Each change is saved whole or not at all: if any part of it fails, the project, its revision, its history, the ledger and the event stream are as they were, and the change answers with the
failedproblem, which is safe to send again. The revision moves on and the event goes out only after the change is saved, and a job the change asked for starts only then.
Every command is in the reference, such as rename-character, set-say, cut-clip, borrow-reading and set-book-cleanup. Jobs are asked for at jobs, not here.
Going back
The go-back command, {"to": 42}, goes back to the version after change 42 for the whole project, and answers with the command that undoes it. Given a sentence as well, it touches only that sentence and those it’s read in one go with.
Clean-up models
Which clean-up model cleans what is set for the book, a voice (by cast ID, or narrator), a take (by take name) and a clip (by Timeline key, segment:start-end), each off, deepfilternet3, mossformer2 or sidon. The commands are set-book-cleanup-model (model), set-voice-cleanup-model (voice, model), set-take-cleanup-model (take, model) and set-clip-cleanup-model (clip, model); a null model follows the next level, except for the book, and each answers with an undo, restore-cleanup-models. A job of the kind hear-cleaned, with a voice, take or clip, a model and an optional noise, hears one cleaned; once it ends, views/cleanup-preview?job=N gives what was heard, the model and where the settings come from. A build’s report lists cleaned_with_flaws: each line’s segment, start, end, text, take, model and flaws.
Jobs
POST projects/{project}/jobs asks for a job with the same request the Make screen sends, such as {"kind": "takes"}, {"kind": "build", "update": true} for an update, or {"kind": "build", "through": 2} to build from the start to the end of the third chapter, counting the chapters as the studio’s sections lists them from 0 (not with update, minutes or rest). It answers 202 at once with the job, waiting or running. A job that can’t run is refused with 422 and the same reason the studio shows. A costly job must be accepted first.
Send an Idempotency-Key header to make asking again safe: the same key with the same body starts nothing and answers 200 with the job as it is now, and the same key with another body is refused as idempotency-key-reused.
A job (jobs/{id}) has its kind, state, when it was asked for, started and ended, why it ended, its build’s name and, while it runs, its progress, with the step, how much is done and the time left.
- How it ended.
outcomeissucceeded,failedorstopped, or null while it runs, andtitlesays it in words, such as “Building the book” or “Built the book”.logsays where to read its log. - What it used. In
usage,settledis false while it runs and true once the ledger has it;usedhas one row per provider, model and unit with its count, andmadeone row per unit and output it made, each{unit, output, count, disputed}. Both are counts, never money. - A build. A build job also has a
build: itsname, how it wasmade_as(whole,update,first,more,restorchapter), andkept_at, when this run last saved the book, null until then. - IDs are strings.
"1"is job 1, and"01"is not.
Quotes and costs
Add ?dry_run=true to POST projects/{project}/jobs, or to a command that starts a job such as queue-line-takes, to see what the job would make and cost before it runs. Nothing is asked for or saved, and no Idempotency-Key is kept. A job that couldn’t run is refused just as asking for it would be.
The answer’s quote gives lead, the unit the job will be capped in (chars_rendered for takes and builds, audio_ms_delivered for Yoto, tokens for Claude’s drafts); most and likely in that unit; credits, {most, likely} at today’s prices, or null where nothing is priced; provisional, true when a bound is a guess; and notes, what the quote can’t say and why.
Accepting a quote before a costly run
Anything that spends money (rendering takes, building the audiobook, a Yoto export, or running Claude over the book) has to be accepted first. Send the call without an accept and ai1y makes nothing: it answers 428 accept-required with the quote, the least, likely and most the run could make in its lead unit. Show that quote to the person. Once they agree, send the same call again, with the same Idempotency-Key, plus the header Ai1y-Accept-Up-To: <n>, where n is the most they accept, in the quote’s lead unit. An exact quote still needs an accept. The accept is written to the project’s ledger as job.accepted. Takes asked for while a takes job is still waiting add their amount to what that job may spend. Free reading help is never metered or gated.
The accept is a ceiling
The job is given the amount when it starts and stops itself before going past it: a voice sets aside the characters it is about to read, a whole batch at a time, so it never reads past what was agreed, and drafting with Claude asks only for the tokens that are left. Claude and Yoto report usage only after each step, so those jobs can end at most one step past the amount. A job that reaches its amount shows as stopped, “it reached the amount accepted”, not failed, and a stopped build keeps the chapters it finished. You are never charged for more than you accepted.
Following along
GET projects/{project}/events is an event stream (server-sent events) of what happens, as it happens:
job, with the job, whenever one is asked for, starts, moves on or ends, andjob-removed, with itsid, when a waiting job is taken out.change, with the newrevision, the action and the subject it changed, for each change saved from a program or the studio’s page, andcommand, with the action and subject, for commands that don’t check a revision.helloopens every stream,{"revision": 12, "replayed": true}, with no number;replayedsays whether what follows is what you missed since yourLast-Event-ID.
Each event is numbered, so a client that reconnects with Last-Event-ID gets only what it missed. The stream suggests reconnecting after 2 seconds, and while nothing happens, a comment line arrives every 15 seconds.
To follow a project as it changes, as the studio’s own page does, open the event stream first and then read what you show. Read a subject again when a change or command event names it, and if the stream drops, a browser’s EventSource reconnects with Last-Event-ID by itself. Don’t poll the job list; job events say when a job moves on, and read a running job’s log only while you show it. Every view names its audio under media/.
Problems
Every refusal is a problem (RFC 9457), sent as application/problem+json, with a type, title, status, detail and next:
{"type": "https://ai1y.co/problems/stale-revision",
"title": "…", "status": 409, "detail": "…",
"next": "…", "kind": "refused", "retryable": false}
Act on type; detail is for a person, and never repeats what you sent. next is one fixed sentence for each type saying what to do next: for stale-revision, read the subject again and decide again, and if it is refused as stale once more, stop and ask the person. kind says who acts (transient, refused, fatal or invariant), and retryable whether sending it again may succeed; only a transient one is, answered with Retry-After.
Which problems an operation answers, and when, is listed with it in the reference. accept-required also carries the quote, and stale-revision the revision and the subject that changed. A type is an identifier, not a page to fetch.
Agents
The API is ready for agents. Every operation an agent may use has an operationId and a description: each command by its own name, the job calls ask-job, list-jobs, read-job, read-job-log and stop-job, and the reads an agent bases a command on, named read-…, list-… and view-…. These say x-ai1y-role: editor; each command also gives its cost in x-ai1y-cost and the job it may start in x-ai1y-starts. Agents are offered exactly those, less any command that uploads audio, so never owner commands or media/.
Send Ai1y-Agent: <name>, a lowercase slug of at most 40 characters, and the changes and jobs that call makes are recorded in the project’s ledger and history as agent:<name>. It is a label for the trail, not a login.
Through MCP
An AI agent, such as Claude Code or Claude Desktop, can work on a project through ai1y-mcp studio, an MCP server it starts, which reaches the project only through this API.
- The tools are the API’s own. As it starts, the server reads
openapi.jsonand offers each operation offered to agents as a tool, named by itsoperationId.wait-for-jobfollows a job until it ends, for up to 300 seconds, with progress as it goes. - Only you accept a cost. Every costly tool has an optional
accept_up_toinput. Called without one, it answers with the quote and makes nothing. The agent is told to show you the quote, then call again withaccept_up_toset to the amount you agreed to, and never to accept on your behalf. - The cost comes first. Each tool’s description starts with its cost, the job it starts and whether it needs a revision, such as “Cost: run-costly. Starts a takes job.” The agent is told to check with you before anything costly.
- Changes are made on what the agent read. A change takes the revision of the read it was based on. If the book changed since, the agent is given what is there now; refused again for the same thing, it is told to stop and ask you.
- Who did it. The agent’s changes and jobs show in the studio’s history and ledger as
agent:<name>. It is a label, not a login: the agent signs in as below. - What an agent can’t do. It is never offered owner commands, audio and files, or uploading recordings. It can stop only the jobs it asked for, and asking for the same costly run three times in a row is refused, saying to ask you.
- Where your book goes. Book text the agent reads goes to that agent’s own AI provider, which you allow by connecting it. Recordings and children’s words are never offered to it.
On the hosted service, an MCP client will sign in with OAuth for ai1y’s MCP server alone, and the server never passes a token it was given on to anything else. An agent that should act with less than its member’s reach will get a delegated key: one made from the member’s key or session, for one audience, with a narrower scope, ending within minutes. Both are coming with the hosted API.
Exports
export-samples exports lines (their takes), a character’s voice samples, or whole chapters of a kept build, as WAV or M4A. It renders nothing: you get the audio exactly as it was made. It is a costly command like the others, so you get an exact quote first, in milliseconds of audio delivered, and nothing runs until it is accepted (Ai1y-Accept-Up-To). The job writes one zip with an export.json naming each sample’s voice, its license terms, whether it may be sold, and a credit line. Nothing is stamped into the sound. Chapter exports hold the voice only, with no background sounds. A person downloads the zip at media/exports/{name}; agents can ask for an export but never download it.
Mark an export "use": "commercial" to sell what it holds. ai1y then checks every voice, clean-up model and sound against its license first, and refuses before writing anything: non-commercial models (such as the free Breeze weights or Sidon), a person’s recorded or cloned voice until their consent is kept, and anything whose origin can’t be told. Personal exports are always allowed. The hosted service will treat every export as commercial.
Every operation
The reference lists every operation and schema, each part of the API under its own heading, with its parameters, body and answers. It is written from the API’s OpenAPI description each time the site is built, so it says what the API does today. The same description is openapi.json, for your own tools.