Call ZeroLog models from your own code with an OpenAI-compatible endpoint, and start work in your containers from other services with webhooks. 5 endpoints, all listed on this page.
You need a ZeroLog account. The token belongs to that account; every request you make with it is billed to your balance.
Get your token.
Sign in at zerolog.ch, then open /api/cli/token in the same browser. Copy the value of token. If you use ZeroLog Code, zerolog login fetches it for you.
Any model your account may use works here. Without model, the catalogue default is used.
Ask a model
curl -sS https://zerolog.ch/api/cli/v1/chat/completions \
-H "Authorization: Bearer $ZEROLOG_TOKEN" \
-H "Content-Type: application/json" \
-d '{"model":"featherless/google/gemma-4-31b-it","messages":[{"role":"user","content":"Write a haiku about Swiss trains."}]}'
The endpoint speaks the OpenAI wire format, so OpenAI client libraries work if you set the base URL:
Python, OpenAI SDK
import os
from openai import OpenAI
client = OpenAI(
base_url="https://zerolog.ch/api/cli/v1",
api_key=os.environ["ZEROLOG_TOKEN"],
)
reply = client.chat.completions.create(
model="featherless/google/gemma-4-31b-it",
messages=[{"role": "user", "content": "Write a haiku about Swiss trains."}],
)
print(reply.choices[0].message.content)
Authentication
Model endpoints take your client token in one of two headers:
Authorization: Bearer mhc-…, which most clients send;
x-api-key: mhc-…, for clients that send keys this way. If both are present, x-api-key wins.
The token has the form mhc-<account>-<secret>. It is derived from your account, does not expire, and stops working when the account is disabled. Treat it like a password: anyone holding it can spend your balance. You cannot revoke it on your own in the app yet. If it leaks, write to info@zerolog.ch and we revoke it.
Webhooks carry their own token in the URL (mhw_… or mhin_…). It is shown once when you create the webhook, stored only as a hash, and revoked by deleting the webhook. See Webhooks.
Errors
Model endpoints answer errors in the OpenAI form: {"error": {"message": "…", "type": "…", "code": "…"}}. code is set when there is a machine-readable reason. A missing or wrong token gets {"type": "error", "error": {"type": "authentication_error", …}}. Webhooks answer {"error": "…", "code": "…"}.
Error statuses of the model endpoints
Status
Type
What happened
What to do
400
invalid_request_error
The request cannot be served as sent: no model and no default, or the provider rejected it.
Fix the request. Do not retry unchanged.
401
authentication_error
Missing, malformed or wrong token, or the account is disabled.
Check the token. Do not retry.
402
billing_error
Balance used up, or the shared pool is empty.
Top up in Billing, then retry.
403
permission_error
Your account may not use this model (model_not_available), or no usable key exists for it (no_key; for byok/… models byok_key_invalid).
Pick another model, or fix your own provider key.
404
not_found_error
The model provider does not know the model. upstream_status carries its status.
Pick another model.
408 · 409 · 413 · 422
invalid_request_error
The provider answered with this client error, passed on unchanged. A 413 can also come from ZeroLog itself (body over 32 MB).
Read error.message; change the request.
429
rate_limit_error
The provider is rate-limiting. ZeroLog passes on Retry-After when the provider sent one.
Wait, then retry with backoff.
503
api_error
No provider could answer, inference on this provider is paused for customers (provider_temporarily_paused), or the provider has no working key on our side (provider_unavailable).
Retry later or pick another model.
A provider's 401 or 403 concerns ZeroLog's key, not yours, so you get 503 instead and your client keeps its token. When a provider answered, its status is in error.upstream_status and in the X-ZeroLog-Upstream-Status header.
Fallback instead of failure. If the model you asked for fails, ZeroLog may answer with another one. You can tell from the X-ZeroLog-Served-Model, X-ZeroLog-Rerouted-From and X-ZeroLog-Model-Notice headers, and from a one-line notice at the start of the answer. Send X-ZeroLog-Reroute: off when you would rather get the error.
Rate limits and sizes
Model endpoints have no fixed request rate. What limits you is your balance (402) and the provider's own rate limits (429 with Retry-After when the provider sends it). Request bodies may be up to 32 MB.
Goal webhooks: 1 to 60 triggers per minute (default 6) and 1 to 1000 per day (default 100), set per webhook. Bodies up to 32 kB.
Inject webhooks: 1 to 120 events per minute (default 60), set per webhook. Bodies up to 256 kB.
Webhook limits answer 429 with retry_after_s in the body. Back off for that long.
Pagination
None of the public endpoints page their results. GET /api/cli/v1/models returns its whole list in one response.
Streaming
Send "stream": true to receive the answer as text/event-stream. Each data: line is one chunk in OpenAI form; the stream ends with data: [DONE]. ZeroLog asks the provider for a final chunk with usage. If you close the connection early, ZeroLog stops the provider request and bills what was produced up to then.
ZeroLog does not call your servers. The two webhooks are inbound: you give their URL to GitHub, Stripe, Zapier or your own service, and they call ZeroLog. A goal webhook starts the goal run it was created for. An inject webhook writes each event as a JSON line into /work/.webhooks/incoming.jsonl in its container, where an agent can read it. Both accept only POST; any other method gets 405, so a link opened in a browser starts nothing. Before an event is stored, Authorization, Cookie and other credential headers are replaced by ‹redacted›.
There are no public WebSocket endpoints. Terminals, the browser extension and the desktop link use WebSockets inside the signed-in app; they are not part of this API.
Endpoints
Every endpoint below is also in /openapi.json, so you can load it into Postman, Insomnia or a code generator. The examples use invented data.
Models
OpenAI-compatible inference. Billed to your ZeroLog balance, or to your own provider key for byok/… models.
GET/api/cli/v1/models
List models
OpenAI-shaped model list for clients that probe /models. It returns the model your account resolves to by default, if your account may use it. The list can be empty; you can still send any model id from the catalogue that your account may use. It is not the full catalogue.
Authentication
Client token: Authorization: Bearer mhc-… or x-api-key
Responses
Status
Meaning
200
Model list.
401
Missing, malformed or wrong client token, or the account is disabled. A request without a token in the expected form and with a body over 64 kB is refused before it is read, with {"error": "unauthorized"}.
// Node 18+ as an ES module (example.mjs), or a browser console
const res = await fetch(`https://zerolog.ch/api/cli/v1/models`, {
method: "GET",
headers: { Authorization: `Bearer ${process.env.ZEROLOG_TOKEN}` },
});
console.log(res.status, await res.json());
OpenAI-compatible chat completions. The request body is passed to the model provider in OpenAI form, so the usual fields (messages, tools, tool_choice, temperature, max_tokens, …) work. Only model, messages and stream are interpreted by ZeroLog itself.
Model. Without model, the operator's catalogue default is used. A model your account may not use is refused with 403 model_not_available before any provider is called. byok/<provider>/<model> runs on your own provider key and never falls back to the shared pool.
Fallback. If the chosen model fails upstream, ZeroLog may answer with another model. It then sets the X-ZeroLog-Served-Model, X-ZeroLog-Rerouted-From and X-ZeroLog-Model-Notice headers and puts a one-line [ZeroLog] … is unavailable notice at the start of the answer. Send X-ZeroLog-Reroute: off to get the error instead.
Streaming. With "stream": true the answer is text/event-stream in OpenAI chunk format, ending with data: [DONE]. ZeroLog asks the provider for a final usage chunk.
Billing. Each answer is billed to your balance (or your own key for byok/…). A stream that the client aborts is still billed for what was produced.
Authentication
Client token: Authorization: Bearer mhc-… or x-api-key
Parameters
X-ZeroLog-Rerouteheader — off disables the automatic fallback to another model. You then get the provider's error instead of a different model.
Request body
OpenAI chat completions request. Maximum body size 32 MB.
modelstring
Model id. Optional; without it the catalogue default is used.
messagesarray, required
streamboolean
toolsarray
OpenAI tool definitions, passed to the provider.
tool_choiceany
Passed to the provider.
temperaturenumber
max_tokensinteger
Responses
Status
Meaning
200
The completion. JSON without stream, an event stream with "stream": true.
400
No model given and no default configured, the body is not valid JSON (then {"error": "…"} as a plain string), or the provider rejected the request (upstream_status is then set).
401
Missing, malformed or wrong client token, or the account is disabled. A request without a token in the expected form and with a body over 64 kB is refused before it is read, with {"error": "unauthorized"}.
402
Your balance is used up, or the shared pool is empty.
403
Your account may not use this model (model_not_available), or no usable key is available for it (for byok/…: your own key is missing or invalid).
404
The provider answered with a client error, passed on unchanged (404, 408, 409, 422 and other 4xx). upstream_status carries the provider's status.
413
The request body is larger than this endpoint accepts, or the provider refused it as too large (upstream_status is then set).
429
The provider is rate-limiting. Retry after the time in Retry-After when present.
503
No provider could answer (provider down, timed out or refused our key), or customer inference on this provider is paused (provider_temporarily_paused), or the provider behind the model has no working key on our side (provider_unavailable). 401 and 403 from a provider are reported as 503, because they concern ZeroLog's key and not yours.
4XX
The provider answered with a client error, passed on unchanged (404, 408, 409, 422 and other 4xx). upstream_status carries the provider's status.
curl
curl -sS -X POST "https://zerolog.ch/api/cli/v1/chat/completions" \
-H "Authorization: Bearer $ZEROLOG_TOKEN" \
-H "Content-Type: application/json" \
-d '{"model":"featherless/google/gemma-4-31b-it","messages":[{"role":"user","content":"Write a haiku about Swiss trains."}]}'
JavaScript
// Node 18+ as an ES module (example.mjs), or a browser console
const res = await fetch(`https://zerolog.ch/api/cli/v1/chat/completions`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ZEROLOG_TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({
"model": "featherless/google/gemma-4-31b-it",
"messages": [
{
"role": "user",
"content": "Write a haiku about Swiss trains."
}
]
}),
});
console.log(res.status, await res.json());
{
"id": "chatcmpl-example-1",
"object": "chat.completion",
"created": 1793700000,
"model": "featherless/google/gemma-4-31b-it",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Rails through the pass\nsnow on the windows, warm tea\nthe clock is on time"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 16,
"completion_tokens": 19,
"total_tokens": 35
}
}
Authentication
Getting the client token for the endpoints above.
GET/api/cli/token
Get your client token
Returns the client token of the signed-in account and the base URL to use it with. Needs a browser session on zerolog.ch: open the address while signed in, or let ZeroLog Code fetch it with zerolog login.
The token is derived from your account. It does not expire. It stops working when the account is disabled. It cannot be revoked on its own from the app today; if it leaks, write to info@zerolog.ch.
Authentication
Browser session (signed in at zerolog.ch)
Responses
Status
Meaning
200
Your token and base URL.
401
Not signed in, or the account is disabled.
403
The session may not do this: for example a session derived from a device token, or a paused account. code says which.
Open this address in a browser where you are signed in. Scripts cannot use it: they would need your session cookie.
Response 200
{
"token": "mhc-1042-REPLACE_WITH_YOUR_TOKEN",
"base_url": "https://zerolog.ch/api/cli/v1",
"note": "Bearer token for the zerocode terminal client. Pick the model in zerocode; it is sent as body.model."
}
Webhooks
Inbound webhooks. You create them in the app (Automation and container settings); the token in the URL is the only credential and is shown once.
POST/api/hook/{token}
Trigger a goal webhook
Starts the goal run that the webhook was created for, in its chat and container. Created in the app; the token (mhw_…) is shown once and stored only as a hash.
The call returns 202 as soon as the run is queued. status_url points to the job in the app and needs your browser session; it is not a public endpoint.
Every method other than POST answers 405, so that opening the link in a browser never starts a paid run. Maximum body size 32 kB.
Limits are set per webhook: 1–60 triggers per minute (default 6) and 1–1000 per day (default 100). The account needs the goal mode permission and credit.
Authentication
The token in the URL. No header.
Parameters
tokenpath, required — Webhook token, mhw_ followed by 32 URL-safe characters.
Request body
JSON.
promptstring
Appended to the stored objective when the webhook is in append mode; ignored otherwise. Longer text is cut at 4000 characters.
Responses
Status
Meaning
202
Run queued.
400
The webhook's chat and container are no longer attached to each other (not_attached), or the body is not valid JSON (then without code).
402
No prepaid credit left on the account.
403
The account is disabled or lacks the goal mode permission.
404
Unknown or revoked token (no_hook). A wrong token and a revoked one get the same answer, so tokens cannot be probed. no_chat and no_container mean the webhook still exists but its chat or container is gone.
405
Any method other than POST.
409
The container could not be resumed for the run.
413
Body larger than this webhook accepts (32 kB for goal webhooks, 256 kB for inject webhooks).
429
Per-minute (rate_min) or daily (rate_day) limit of this webhook reached. Wait retry_after_s seconds.
500
Unexpected server error. The body never carries internal details.
curl
curl -sS -X POST "https://zerolog.ch/api/hook/$ZEROLOG_HOOK_TOKEN" \
-H "Content-Type: application/json" \
-d '{"prompt":"Deploy finished for commit 3f2a91c. Check the release notes."}'
JavaScript
// Node 18+ as an ES module (example.mjs), or a browser console
const res = await fetch(`https://zerolog.ch/api/hook/${process.env.ZEROLOG_HOOK_TOKEN}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
"prompt": "Deploy finished for commit 3f2a91c. Check the release notes."
}),
});
console.log(res.status, await res.json());
Python
import os, requests
res = requests.post(
f"https://zerolog.ch/api/hook/{os.environ['ZEROLOG_HOOK_TOKEN']}",
json={
"prompt": "Deploy finished for commit 3f2a91c. Check the release notes."
},
)
print(res.status_code, res.json())
Appends the request as one JSON line to /work/.webhooks/incoming.jsonl in the container the webhook belongs to. Point GitHub, Stripe, Zapier or your own service at this URL; an agent in the container can then read the events. The token (mhin_…) is shown once when you create the webhook.
Stored per event: time, method, request headers and the parsed JSON body. Authorization, Cookie, X-Api-Key, X-Auth-Token and Proxy-Authorization are replaced by ‹redacted› before storage.
202 is returned for every accepted event. ok: false with status: "error" means the event was counted but could not be written, usually because the container is not running.
Every method other than POST answers 405. Maximum body size 256 kB. Limit per webhook: 1–120 events per minute (default 60).
Authentication
The token in the URL. No header.
Parameters
tokenpath, required — Webhook token, mhin_ followed by 32 URL-safe characters.
Request body
JSON.
Responses
Status
Meaning
202
Event accepted.
400
The webhook's chat and container are no longer attached to each other (not_attached), or the body is not valid JSON (then without code).
404
Unknown or revoked token (no_hook). A wrong token and a revoked one get the same answer, so tokens cannot be probed. no_chat and no_container mean the webhook still exists but its chat or container is gone.
405
Any method other than POST.
413
Body larger than this webhook accepts (32 kB for goal webhooks, 256 kB for inject webhooks).
429
Per-minute (rate_min) or daily (rate_day) limit of this webhook reached. Wait retry_after_s seconds.
500
Unexpected server error. The body never carries internal details.
No endpoint matches. Clear the filter to see all 5.
Trying requests
This page has no "Try it" button. A live test would need a real token, and every call is billed, so a button in a public page would either spend someone's balance or need a sandbox account that does not exist yet. Copy the examples instead and run them with your own token.
What this API does not cover
The rest of /api/ belongs to the ZeroLog web app. It needs a browser session, changes without notice and is not meant for scripts. If you need something from there as a stable endpoint, tell us at info@zerolog.ch. The Monitoring API is a separate public API with its own specification; both are listed in /.well-known/api-catalog.
The specification is checked against the server code on every deploy: a public endpoint that changes without its documentation stops the deploy.