ZeroLog
Private preview

API reference

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.

Base URL
https://zerolog.ch
Specification
/openapi.json · OpenAPI 3.1.0
Version
1.0.0
Format
JSON, UTF-8. Streams as server-sent events.

Quickstart

You need a ZeroLog account. The token belongs to that account; every request you make with it is billed to your balance.

  1. 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.

    Your token, in the shell
    export ZEROLOG_TOKEN="mhc-1042-REPLACE_WITH_YOUR_TOKEN"
  2. Check that it works.

    A 200 with a model list means the token is valid. A 401 means it is not.

    List models
    curl -sS https://zerolog.ch/api/cli/v1/models \
      -H "Authorization: Bearer $ZEROLOG_TOKEN"
  3. Send a request.

    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:

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
StatusTypeWhat happenedWhat to do
400invalid_request_errorThe request cannot be served as sent: no model and no default, or the provider rejected it.Fix the request. Do not retry unchanged.
401authentication_errorMissing, malformed or wrong token, or the account is disabled.Check the token. Do not retry.
402billing_errorBalance used up, or the shared pool is empty.Top up in Billing, then retry.
403permission_errorYour 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.
404not_found_errorThe model provider does not know the model. upstream_status carries its status.Pick another model.
408 · 409 · 413 · 422invalid_request_errorThe 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.
429rate_limit_errorThe provider is rate-limiting. ZeroLog passes on Retry-After when the provider sent one.Wait, then retry with backoff.
503api_errorNo 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

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.

Streamed answer (shortened)
data: {"id":"chatcmpl-example-2","object":"chat.completion.chunk","model":"featherless/google/gemma-4-31b-it","choices":[{"index":0,"delta":{"content":"Lake Geneva"},"finish_reason":null}]}

data: {"id":"chatcmpl-example-2","object":"chat.completion.chunk","choices":[],"usage":{"prompt_tokens":21,"completion_tokens":9,"total_tokens":30}}

data: [DONE]

Webhooks and WebSockets

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
StatusMeaning
200Model list.
401Missing, 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"}.
curl
curl -sS "https://zerolog.ch/api/cli/v1/models" \
  -H "Authorization: Bearer $ZEROLOG_TOKEN"
JavaScript
// 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());
Python
import os, requests

res = requests.get(
    f"https://zerolog.ch/api/cli/v1/models",
    headers={"Authorization": f"Bearer {os.environ['ZEROLOG_TOKEN']}"},
)
print(res.status_code, res.json())
Response 200
{
  "object": "list",
  "data": [
    {
      "id": "featherless/google/gemma-4-31b-it",
      "object": "model",
      "created": 0,
      "owned_by": "zerolog"
    }
  ]
}

POST /api/cli/v1/chat/completions

Create a chat completion

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-Reroute header — 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.
model string
Model id. Optional; without it the catalogue default is used.
messages array, required
stream boolean
tools array
OpenAI tool definitions, passed to the provider.
tool_choice any
Passed to the provider.
temperature number
max_tokens integer
Responses
StatusMeaning
200The completion. JSON without stream, an event stream with "stream": true.
400No 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).
401Missing, 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"}.
402Your balance is used up, or the shared pool is empty.
403Your 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).
404The provider answered with a client error, passed on unchanged (404, 408, 409, 422 and other 4xx). upstream_status carries the provider's status.
413The request body is larger than this endpoint accepts, or the provider refused it as too large (upstream_status is then set).
429The provider is rate-limiting. Retry after the time in Retry-After when present.
503No 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.
4XXThe 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());
Python
import os, requests

res = requests.post(
    f"https://zerolog.ch/api/cli/v1/chat/completions",
    headers={"Authorization": f"Bearer {os.environ['ZEROLOG_TOKEN']}"},
    json={
      "model": "featherless/google/gemma-4-31b-it",
      "messages": [
        {
          "role": "user",
          "content": "Write a haiku about Swiss trains."
        }
      ]
    },
)
print(res.status_code, res.json())
Response 200
{
  "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
StatusMeaning
200Your token and base URL.
401Not signed in, or the account is disabled.
403The 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
  • token path, required — Webhook token, mhw_ followed by 32 URL-safe characters.
Request body
JSON.
prompt string
Appended to the stored objective when the webhook is in append mode; ignored otherwise. Longer text is cut at 4000 characters.
Responses
StatusMeaning
202Run queued.
400The webhook's chat and container are no longer attached to each other (not_attached), or the body is not valid JSON (then without code).
402No prepaid credit left on the account.
403The account is disabled or lacks the goal mode permission.
404Unknown 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.
405Any method other than POST.
409The container could not be resumed for the run.
413Body larger than this webhook accepts (32 kB for goal webhooks, 256 kB for inject webhooks).
429Per-minute (rate_min) or daily (rate_day) limit of this webhook reached. Wait retry_after_s seconds.
500Unexpected 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())
Response 202
{
  "job_id": 5821,
  "status_url": "/api/jobs/5821"
}

POST /api/inject/{token}

Deliver an event into a container

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
  • token path, required — Webhook token, mhin_ followed by 32 URL-safe characters.
Request body
JSON.
Responses
StatusMeaning
202Event accepted.
400The webhook's chat and container are no longer attached to each other (not_attached), or the body is not valid JSON (then without code).
404Unknown 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.
405Any method other than POST.
413Body larger than this webhook accepts (32 kB for goal webhooks, 256 kB for inject webhooks).
429Per-minute (rate_min) or daily (rate_day) limit of this webhook reached. Wait retry_after_s seconds.
500Unexpected server error. The body never carries internal details.
curl
curl -sS -X POST "https://zerolog.ch/api/inject/$ZEROLOG_INJECT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ref":"refs/heads/main","after":"3f2a91c","repository":{"full_name":"example-org/example-repo"}}'
JavaScript
// Node 18+ as an ES module (example.mjs), or a browser console
const res = await fetch(`https://zerolog.ch/api/inject/${process.env.ZEROLOG_INJECT_TOKEN}`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    "ref": "refs/heads/main",
    "after": "3f2a91c",
    "repository": {
      "full_name": "example-org/example-repo"
    }
  }),
});
console.log(res.status, await res.json());
Python
import os, requests

res = requests.post(
    f"https://zerolog.ch/api/inject/{os.environ['ZEROLOG_INJECT_TOKEN']}",
    json={
      "ref": "refs/heads/main",
      "after": "3f2a91c",
      "repository": {
        "full_name": "example-org/example-repo"
      }
    },
)
print(res.status_code, res.json())
Response 202
{
  "ok": true,
  "status": "stored",
  "path": "/work/.webhooks/incoming.jsonl"
}

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.

Something missing or wrong here? Say so. The documentation is part of the product, not an afterthought. All topics · Security · Status · X · Legal & privacy · Home