Build with Ultri
Sign in with your Ultri account to make API keys and see what each call cost. The same credits pay for the app and the API.
Overview
Every request is charged to your credits at the model's listed price, to the token, and recorded with the key that made it.
Models
First call
API keys
A key acts as your account: it can call the models and read your balance and usage, and nothing else. Keys are shown once. Give each integration its own key and a monthly limit, so a leak or a bug cannot spend more than you meant.
| Name | Key | Created | Last used | Requests | This month | Limit |
|---|
Usage
What you were charged, grouped how you like. "Cost" is what the call cost us; "charged" is what came off your credits. Prices that changed are dated on each model's page, and every row keeps the price it was charged at.
Recent requests
| When | Product | Model | Key | In | Out | Charged | Request id |
|---|
Documentation
Ultri's own writing models over HTTP. The chat endpoint speaks the OpenAI wire format, so the OpenAI SDKs work with a different base URL. Everything is priced per token or per word, charged to your credits as it happens.
Authentication
Make a key under API keys and send it as a bearer token on every request:
Authorization: Bearer ultri_sk_…
A key is tied to the account that made it and spends that account's credits. It cannot change the account, make other keys or read conversations from the app. Revoke a key any time; the usage it generated stays on the books under its name.
Quickstart
The base URL is . With the OpenAI SDKs set base_url to it and api_key to your key; nothing else changes.
Chat completions
POST /v1/chat/completions. Send the conversation, get the next assistant message. Ultri's writing rules (plain words, no stock phrases, no em dashes) are prepended to your system prompt unless you pass "ultri_style": false.
| Field | Type | What it does |
|---|---|---|
model | string | A chat model from GET /v1/models. Default ultri-writer. |
messages | array | Roles system, user, assistant (developer counts as system). Content is a string or an array of {"type":"text"} parts. |
stream | bool | Server-sent events, one chat.completion.chunk per delta, then data: [DONE]. |
stream_options | object | {"include_usage": true} adds a last chunk with usage and ultri (cost) before [DONE]. |
max_tokens | int | Cap on the reply. Default 1024, at most the model's max_output_tokens. max_completion_tokens is accepted too. |
temperature, top_p, stop | As in OpenAI. Defaults 0.7, 0.95, none. Up to 4 stop strings. | |
user | string | Your own id for the end user; stored on the usage row so you can split your bill. |
ultri_style | bool | Default true. Set false to run the model on your system prompt alone. |
Response
{
"id": "chatcmpl-5f1c…", "object": "chat.completion", "created": 1794220000, "model": "ultri-writer",
"choices": [{"index": 0, "message": {"role": "assistant", "content": "Hi Mr. Patel, …"}, "finish_reason": "stop"}],
"usage": {"prompt_tokens": 412, "completion_tokens": 86, "total_tokens": 498},
"ultri": {"cost_usd": 0.001272, "balance_usd": 4.9871, "model_version": "ultri-writer@dpo6",
"request_id": "chatcmpl-5f1c…", "estimated": false, "price_per_1m_tokens": {"input": 1.0, "output": 10.0}}
}
The ultri object is the bill for this call: what came off your credits, your balance after it, and exactly which weights answered. A reply you cut off mid-stream is still paid for (the prompt was read); its output count is estimated from the text you received and the row is marked estimated.
Streaming
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hi Mr. Patel,"},"finish_reason":null}]}
…
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[],"usage":{…},"ultri":{…}} ← with include_usage
data: [DONE]
Ultri's model runs on a machine that sleeps when nobody is using it. If your call wakes it, the stream sends comment lines (: waking the model…) while it starts, which SDKs ignore; a non-streaming call waits up to five minutes and then answers 503 with Retry-After. Keep a retry in your client for that case.
Humanize
POST /v1/humanize rewrites text that reads like a machine wrote it. A run takes one to several minutes and is billed at metered cost as it goes (model calls, GPU seconds, GPTZero reads), so the usual flow is: start the job, follow it, read the result.
POST /v1/humanize
{"text": "…at least 15 words…", "wait": false, "voice": "auto", "candidates": 2, "length": "natural", "fidelity": "natural",
"style_sample": "optional: your own writing to match", "notes": "optional facts the writer may use", "verify": true}
→ {"job_id": "a1b2c3d4e5f6", "status": "running", "estimate": {"usd": 0.84, "needed": 0.84, "balance": 12.1, "enough": true},
"status_url": "/v1/jobs/a1b2c3d4e5f6", "events_url": "/v1/jobs/a1b2c3d4e5f6/events"}
GET /v1/jobs/{job_id} status running | paused | done | error | cancelled, the log, the running cost, then result
GET /v1/jobs/{job_id}/events server-sent events: log, cost (every time it moves), paused, running, done
DELETE /v1/jobs/{job_id} stop it (what it already spent stays charged)
POST /v1/credits/estimate {"words": 800} what a run of this length is likely to cost, before you start it
"wait": true (the default) holds the HTTP request open and returns the finished job instead; fine for scripts, not for a web server. The result has output, before and after (detector scores and statistics), meaning (how faithfully the meaning survived), verify (the GPTZero verdict when on) and cost. Not enough credits for the estimate is a 402 with the estimate in the body. A run that spends the balance to zero mid-way pauses and carries on when credits are added; your keys' monthly limits also apply here.
GET /v1/voices lists the voice ids you can pass instead of auto.
Detection
POST /v1/check {"text": "…"} runs Ultri's own detector of machine writing habits: a 0 to 100 score, a verdict, the flagged phrases with what a person would say instead. Free.
POST /v1/score {"text": "…", "detectors": ["gptzero"]} reads the text with GPTZero and returns the probability it is machine-written, per sentence too. Priced per word at our cost (see Pricing); the balance must cover the whole read before it starts. Leave detectors out for the free local detectors only.
Balance and usage
GET /v1/platform/balance credits left, this month's API spend, this key's month to date and limit
GET /v1/platform/usage?days=30&group=day rows by day | model | key | product; ?key=… for one key; ?api_only=1; &format=csv
GET /v1/usage the account's full ledger view (the same one the app shows)
GET /v1/credits balance, free credits and when they expire, purchases, per-run subtotals
Each usage row carries the model, the token counts, our cost, what you were charged, the key, your user id, and the model version. Nothing is ever edited after the fact: a price change applies to calls after it.
Pricing
Prices come from GET /v1/models, so this table is live. Credits are prepaid in dollars and never expire when bought; every account also gets a small free allowance each month.
| Model | Endpoint | Unit | Input | Output | Since |
|---|
Errors
Errors are JSON: {"error": {"message", "type", "code", "param"}}, in the OpenAI shape, with the HTTP status that matters.
| Status | Code | Meaning |
|---|---|---|
| 401 | No key, an unknown or revoked key, or a route keys may not use. | |
| 402 | insufficient_credits, key_spend_limit | Not enough credits for the call, or this key hit its monthly limit. The body says how much is needed. |
| 404 | model_not_found | No such model, or not for this endpoint. The message lists what you can use. |
| 400 / 413 | invalid_request_error | A bad field (param names it) or too much text. |
| 429 | rate_limit_exceeded, concurrency | Too many requests this minute on this key, or too many in flight. Honour Retry-After. |
| 502 / 503 | model_starting, server_error | The model is waking up or failed; retry after Retry-After. |
Limits
Model versions
Ultri keeps training its models. A model id like ultri-writer always points at the weights in production; the exact version answering is in every response's ultri.model_version and on every usage row, so you can see when a change happened and compare. A new version or a new price is dated under GET /v1/models/ultri-writer (version, price_history). Models move through preview, active, deprecated and retired; a deprecated model keeps working until it is retired, and the list says which is which.
The full machine-readable reference is at /reference (openapi.json).