The HTTP APIalpha
What endpoints exist, and what do they take and return?
moxzid speaks plain HTTP/1.1 with raw Candid request and response bodies — there is no agent envelope, no CBOR, and no /api/v2. The constraint that catches people first: its own endpoints are matched on the exact request target, so /metrics?x=1 is a 404 and /health?x=1 is not treated as open. Only gateway paths receive the query string, which they forward to the actor as part of url.
Every endpoint#
<actor> is a manifest name or a hex principal, everywhere it appears.
| Method | Path | Purpose | Auth |
|---|---|---|---|
GET | /health | liveness: the process is up | open |
GET | /health/ready | readiness: manifest installed and journals replayed | open |
GET | /version | version and git sha | open |
GET | / | the inspector dashboard, baked into the binary (with --web this path is the site instead) | open |
GET | /inspector | the same dashboard, at a fixed path | open |
GET | /metrics | Prometheus exposition text | token |
GET | /inspect | one JSON snapshot of every actor | token |
GET | /actors | name/id list of everything installed | token |
POST | /call/<actor>/<method> | deliver an update message. At-most-once is the client's job: there is no invocation-ID deduplication yet, so a request retried after a timeout may execute the mutation twice — retry queries freely, retry updates only when the method is idempotent | token. x-caller: <principal> sets msg.caller for this message (default: anonymous); malformed is a 400 |
POST | /query/<actor>/<method> | deliver a non-replicated query | token |
POST | /install/<name> | install a new actor; body is the wasm module | token |
POST | /upgrade/<actor> | replace code, keep state; body is the module or empty. A changed stable signature is pre-flighted through moxzi stable-compatible: a proven-incompatible upgrade is refused with the reasons (M0169/M0170/M0216); ?force=true skips the pre-flight, and the RTS's own memory check still has the final word | token |
GET | /cycles/<actor> | read the remaining compute allowance | token |
POST | /cycles/<actor> | grant more; body is a decimal amount | token |
GET | /logs/<actor> | the actor's debug output, taken and cleared | token |
GET | /history/<actor> | the recent-message ring, as JSON | token |
GET | /events/<actor> | the same debug output as a live SSE stream | token |
GET | /mops/closure/<entry> | a source file's full dependency closure (needs --project) | token |
| any | /site/<actor>/<path> | the HTTP gateway into that actor's http_request | open |
| any | any other path, with --web <actor> | the same gateway, at the root | open |
OPTIONS | any non-gateway path | CORS preflight, answered 204 before auth | open |
Every reply carries moxzid-version and Access-Control-Allow-Origin: *. Anything unmatched is 404 not found.
Calling an actor#
Body in and body out are raw Candid. The empty argument is the six bytes DIDL\x00\x00; an empty body is not the same thing.
printf 'DIDL\x00\x01\x7d\x05' > nat5
curl -s -H "Authorization: Bearer $T" --data-binary @nat5 \
http://127.0.0.1:7000/call/counter/add | xxd -p
# 4449444c00017d05
The reply's Content-Type is application/octet-stream and the bytes are not decoded server-side: moxzid does not know the actor's interface and does not guess.
| Status | Meaning |
|---|---|
200 | the actor replied; the body is the Candid reply |
400 | the actor trapped or rejected (body is the runtime's message), or the path lacked /<method> |
402 | the actor's cycle allowance is dry — top it up, this is not a bad request |
404 | no such actor |
413 | the body exceeded the cap, refused before it was read |
500 | the actor thread produced no reply |
503 | still recovering (starting: <note>), or the actor thread is gone |
/query/ is the only route that can reach a composite query. A query's writes are rolled back and it is never journalled.
Install, upgrade, cycles#
| Request | Body in | Body out |
|---|---|---|
POST /install/<name> | the wasm module | the new actor's hex id and a newline |
POST /upgrade/<actor> | the wasm module, or empty to re-read the manifest's file | upgraded |
GET /cycles/<actor> | — | the remaining balance as decimal text |
POST /cycles/<actor> | a decimal amount as text | the new balance as decimal text |
Install over an existing name is refused with 400 — installing would discard state, and upgrading is the verb that keeps it. A body-less upgrade of an actor created at runtime is also 400: there is no recorded path to re-read, so the module must come in the body.
JSON shapes#
GET /actors:
[{"name":"counter","id":"9c1329bdc670cfa16f66e0afb34959c00481d47b02f1a7143b62a29103"}]
GET /inspect — {"version": …, "actors": [ … ]}, each actor carrying id, name (null for runtime-created actors), cycles (a decimal string), heap_bytes, messages, traps, methods ({"name","kind"}, kind being update or query, with the runtime's @/__motoko plumbing filtered out) and interface, the Candid service text read from the module's own candid:service section.
GET /history/<actor> — an array of ring entries, newest last:
{"seq":2,"time_ns":1787342315895685000,"kind":"call","method":"add","caller":"04",
"arg_len":8,"arg_preview":"4449444c00017d05","ok":true,"error":null,"reply_len":8}
kind is install, call, query or upgrade; arg_preview is bounded, never a whole blob. Ring size is --history N (default 50, 0 disables).
GET /mops/closure/<entry> — {"entry", "pkgSource", "unresolved", "files":[{"vfs","bytes"}]} with bytes base64. Entry paths are confined to the --project directory; one that escapes it is 403, and a missing --project is 404.
GET /metrics — Prometheus text (text/plain; version=0.0.4) with moxzid_uptime_seconds, moxzid_http_requests_total{endpoint=…} by endpoint class, moxzid_actor_messages_total / moxzid_actor_traps_total per actor as the caller addressed it, moxzid_actor_cycles / moxzid_actor_heap_bytes per actor by hex id, and moxzid_state_bytes when -s is set.
The gateway and the event stream#
A gateway request is encoded as the IC's standard HttpRequest record — {method; url; headers; body; certificate_version} — and delivered to the actor's http_request query first, exactly as a boundary node does. If the actor answers upgrade = ?true, the same request is re-delivered to http_request_update as an update. The actor's status code, headers and body pass through; moxzid overrides only Content-Length, Connection and Transfer-Encoding, and never forwards the Authorization header to the actor.
| Situation | Response |
|---|---|
| no such actor | 404 |
the actor has no http_request | 502 Bad Gateway, with the runtime's message |
the reply does not decode as an HttpResponse | 502 Bad Gateway |
certificate_version | always sent as null; there is nothing to certify off-chain |
streaming_strategy present | accepted, logged, not followed — only the first chunk is served |
GET /events/<actor> holds the connection open and emits data: lines as the actor prints, with retry: 2000 and a : keepalive comment roughly every 15 seconds. It drains the same take-and-clear ring as /logs/<actor>, so the two compete for lines rather than each seeing a copy. It is ops surface, not website: without a token it is 401.
Next#
- Authentication — the open set, the token, and what must never be exposed.
- Running a server — manifest, flags, and a first call end to end.
- Operating moxzid — metrics scraping, structured logs, shutdown.