The account API

Everything that is not the OpenAI-compatible surface lives under /api, and this page is the map of it: identity, plan, money, conversations, code runs, support and the request log.

These routes are ours to specify, which is why they are specified at all. The chat surface is not: it is OpenAI's format, and restating it would be claiming ownership of a shape we do not control. The machine-readable version of this page is contract/openapi.yaml, generated from the handlers, with a gate that refuses any route the table does not describe.

The credential is a signed-in session on the Authorization header, as described in authentication. Five things are open and say so where they appear: the plan catalogue, the status report, the health check, a rail's webhook, and a generated image's own URL.

Identity and usage

RouteWhat it answers
GET /api/account/mehandle, tier, plan_state, renews_at, chat_unlimited, extra_usage_enabled, api_access, and images and lookups as used, included, left, in_plan.
GET /api/account/usageOne row per day and operation: day, operation, calls, cost_cents. Thirty days, or ninety with ?days=90.
GET /api/developers/requestsThe recent API calls made with this account's keys. limit defaults to 200 and is capped at 500.

cost_cents is euro cents, settled rather than estimated. Credits are the ledger's internal unit and do not appear on a screen.

The request log carries route, model, status, output_tokens, cost_mcr, latency_ms and created_at, and key_id when the call came from a key rather than the product. It carries no prompt, no answer and no target. A product whose users send the most sensitive prompts they will ever type does not keep a copy of them in an observability table.

Beside data the answer carries summary: calls, failed and median_latency_ms over the same window the rows came from, never a wider one. The rollup is here rather than in the client because a screen that adds up its own rows is a second answer waiting to disagree with this one. failed counts every status at 400 and above, and the median is the latency of a call that actually happened rather than the mean of the two either side of the middle.

Devices, export and deletion

RouteWhat it does
GET /api/account/devicesThe sessions currently able to act as this account: id, created_at, expires_at.
DELETE /api/account/devices/{id}Revoke one session, and the local replica on that device at its next contact.
POST /api/account/sign-out-everywhereRevoke every session, this one included.
GET /api/account/exportEverything held about the account, as drael-export.json.
DELETE /api/accountDelete the account and everything reachable from it.

The export is a readable archive rather than a database dump: conversations with their messages, and generations, opened rather than sealed, because the key is ours and the data is theirs.

Deletion reaches the image files and the sandbox workspaces, not only the rows. What survives is the ledger, anonymised: financial records carry a retention obligation that a deletion request does not override, and that is said here rather than performed quietly. See privacy.

Plan and money

RouteWhat it does
GET /api/billing/plansThe plans, their prices and what each includes. Open, because a price nobody can read before signing up is a price that costs a sale.
GET /api/billing/subscriptionThe current plan, its state and its period.
POST /api/billing/checkout{"tier": "...", "interval": "weekly"} or "monthly". Answers pay_url and attempt_id.
POST /api/billing/checkout/{id}/abandonClose an attempt the payer walked away from. It never revokes anything.
GET /api/billing/invoicesReceipts for purchases that settled.
GET /api/billing/extra-usageThe extra-usage setting and remaining_today_cents.
PUT /api/billing/extra-usageSet extra_usage_enabled and daily_limit_cents.

An account that has never bought anything is on the free tier rather than in an undefined state. A rail's webhook posts to /api/billing/webhook/{provider} with no credential and nothing in the body is believed: the payment status is read back from the rail by id, so a forged notification buys nothing.

Extra usage and the spend limit

Extra usage is spending past what the plan includes. It is off until you turn it on, and it cannot be turned on without naming a daily ceiling: a limit of zero forces the setting off. Negative limits are clamped to zero, and a plan that does not carry extra usage answers plan_required.

The ceiling is enforced on the request path, where the plan's own allowances are known, and it counts what was actually used past them. A request that would pass it answers spend_limit_reached, which is deliberately a different answer from having no credit: this account asked us to stop here today, so it is told that rather than "buy more".

Requests arriving together can pass it by about one call each, and that is stated rather than claimed away. What the ceiling is counted from only moves when a call finishes: a lookup becomes a ledger entry when it settles, an image becomes a record once it is drawn. So calls in flight at the same instant all read the same figure. The overshoot is cents, the credit balance is the hard stop behind it and cannot be passed at all, and closing the gap properly means a reservation taken at the gate rather than a lock around a number that has not moved.

Conversations

RouteWhat it does
GET /api/sessionsThe account's conversations, newest first.
POST /api/sessionsStart one, optionally with a title.
GET /api/sessions/{id}The conversation and its messages.
PATCH /api/sessions/{id}Set title, instructions, or both. An absent field is untouched.
DELETE /api/sessions/{id}Delete the conversation and its messages.
POST /api/sessions/{id}/messagesAppend a message. The role must be user or assistant.
GET /api/instructionsThe account's standing instructions.
PUT /api/instructionsReplace them.
GET /api/syncEverything changed since ?since=, an RFC3339 timestamp.

A turn answered through /v1 is recorded by the server, not by the client, when the request named one of these conversations in X-Drael-Session: the tool loop hides its calls from every client on purpose, so the trace is ours to write or nobody's. See chat.

GET /api/sync answers cursor, sessions, generations and more. The cursor is the server's clock rather than the device's, so a device with a wrong clock cannot skip its own history, and more says another page is waiting, so a device that has been away for a month catches up in bounded steps.

Code

RouteWhat it does
POST /api/code/{session}/runsRun code in python or bash. network is a per-call flag, off by default.
GET /api/code/{session}/runsWhat has been executed in this conversation.
GET /api/code/{session}/filesThe files in the workspace.
GET /api/code/{session}/files/{name}One file, with truncated when it was cut.
POST /api/code/{session}/filesAttach one file, as multipart form data under file, where the model reads it.

The same sandbox the run_code tool uses, with the same bounds: see tools. An attached file larger than 32 MiB answers file_too_large. A run the sandbox could not isolate answers no_isolation and does not run: failing closed, said plainly.

Support

GET /api/support and POST /api/support with subject and body. A ticket carries the account id by construction, so nobody has to prove who they are to an operator who deliberately holds no address, and the body is encrypted like everything else a person writes.

These routes are not the channel that is answered. The product carries [email protected] on its help screen and nothing calls these; whether they are wired to a person or removed is an open decision. Write to the address rather than posting a ticket you may be the only one to read.

Status

GET /api/status is open and carries no account, no usage and no identifier. It answers state, which is up, degraded or down, and components for database, cache and model individually, because "something is wrong" is not actionable and "the model is unreachable" is. A degraded product still answers 200: the endpoint is reporting, not failing.

GET /health is the liveness check, and answers 503 when a dependency is unreachable.