API examples
Every endpoint a developer calls, with requests and responses. The responses were captured by running Compute Gateway’s own router against its test doubles for Drive, Granite and Spend Gateway; ids and times will differ. The full endpoint list is in the generated API reference.
Base URL: https://j5afbrmr6qzrwewgxdwacvmedq0ylxvy.lambda-url.us-east-1.on.aws,
written $CG below. Bodies are JSON (Content-Type: application/json).
Every call here needs a token; see Authentication.
$AUTH below stands for:
AUTH=(-H "Authorization: Bearer $CHIRP_ID_TOKEN" -H "Content-Type: application/json")
Run a module once
POST /v1/compute/invoke
curl "$CG/v1/compute/invoke" "${AUTH[@]}" -d '{
"module_ref": {"owner_uid": "user-1", "app_id": "acme.echo", "name": "main", "version": "1.0.0"},
"grant_id": "<the Granite grant id>",
"payload": "hello, gateway"
}'
| Field | Required | Rules |
|---|---|---|
module_ref | Yes | owner_uid, app_id, name, version; see Modules |
grant_id | Yes | The Granite grant to run under, a UUID |
payload | No | A string the module reads with get_input; empty if left out |
Response, 200 OK, for a module that copies its input to its output:
{"exit":0,"stdout":"","output":"hello, gateway"}
A module that runs out of fuel returns 400:
{"error":"error while executing at wasm backtrace:\n 0: 0x63 - <unknown>!<wasm function 2>: wasm trap: all fuel consumed by WebAssembly"}
A grant that does not hold every capability the module declares returns 400
with a message starting granite refused grant <id>:.
Start a durable run
POST /v1/compute/runs takes the same body as a one-shot call, plus an
optional spend_grant_id: the person’s spend grant from
POST /v1/model/spend-grants/resolve (see below), which pays for the run’s
model calls. The module must export run_init and run_step; see
Calls and runs.
A run that finished within the request, 200 OK:
{"run_id":"b17f65ee-5d2e-4409-ad01-8214b3d6b475","status":"done","exit":0,"output":"","step_seq":2}
A run that reached a slow command and suspended, 200 OK:
{"run_id":"50160ad1-8036-4c6b-b7bb-d2dc26b6f2d9","status":"suspended","output":"","pending_cap":"model.gateway.call","step_seq":1}
| Field | Meaning |
|---|---|
status | done or suspended. A step that fails returns 400 instead, with the run’s id, and the run is saved as failed (see below). |
exit | The exit code, once done |
output | The run’s final state, once done; empty otherwise |
pending_cap | The capability the next tick will use, while suspended. A request cut off mid-run can leave a fast one here too. |
error | Why the run failed, once failed |
step_seq | Counts the run’s saved steps; it grows as the run moves |
A start that fails once the run exists, because a step trapped, emitted a
malformed command, or named a capability its grant lacks, returns 400 with
the run’s id. Reading the run by that id shows it failed:
{"error":"run 6a0f3c1e-2b7d-4e59-9c84-1f2d3e4a5b6c: command names capability `drive.namespace.append_log` not in the granted set","run_id":"6a0f3c1e-2b7d-4e59-9c84-1f2d3e4a5b6c"}
A start that fails once the run exists because a service Compute Gateway
depends on is down returns 503, also with the run’s id:
{"error":"internal error","run_id":"6a0f3c1e-2b7d-4e59-9c84-1f2d3e4a5b6c"}
If that failure kept the first step’s result from being saved, the run reads
running and has nothing to carry on from: a tick fails with 400 and the run
ends failed.
A start that fails before the run exists (the module is not in Drive, Granite
refused the grant, or Spend Gateway did not reserve the first step’s units)
returns the usual {"error": …} with no run_id.
A module without run_init returns 400:
{"error":"module main@1.0.0 is not run-capable (no `run_init` export)"}
Move a suspended run on
POST /v1/compute/runs/{run_id}/tick
curl "$CG/v1/compute/runs/50160ad1-8036-4c6b-b7bb-d2dc26b6f2d9/tick" "${AUTH[@]}" \
-d '{"owner_uid": "user-runagent-ticks", "app_id": "acme.runagent"}'
owner_uid and app_id must match the run’s module_ref. Each tick performs
the pending command and runs steps until the run suspends again or finishes:
{"run_id":"50160ad1-8036-4c6b-b7bb-d2dc26b6f2d9","status":"suspended","output":"","pending_cap":"pigeon.outbox.send","step_seq":2}
{"run_id":"50160ad1-8036-4c6b-b7bb-d2dc26b6f2d9","status":"done","exit":0,"output":"p2","step_seq":3}
An unknown run returns 404; a run that is already done or failed
returns 409, unless its last step is still to be settled with Spend Gateway
(after a 503): then the tick settles it and returns the run.
{"error":"run 4abb2538-6c1e-4b95-85f6-ac1b21626a00 is already terminal (Done)"}
Read a run
GET /v1/compute/runs/{run_id}?owner_uid=...&app_id=... returns the run in
the same shape, without running anything:
{"run_id":"50160ad1-8036-4c6b-b7bb-d2dc26b6f2d9","status":"done","exit":0,"output":"p2","step_seq":3}
A failed run says why:
{"run_id":"6a0f3c1e-2b7d-4e59-9c84-1f2d3e4a5b6c","status":"failed","output":"","error":"run 6a0f3c1e-2b7d-4e59-9c84-1f2d3e4a5b6c: command names capability `drive.namespace.append_log` not in the granted set","step_seq":1}
Both query parameters are required. An unknown run returns 404.
Nora interactive runs
POST /v1/nora/interactive-runs starts one:
curl "$CG/v1/nora/interactive-runs" "${AUTH[@]}" -d '{
"module_ref": {"owner_uid": "user-nora-counter", "app_id": "acme.nora-counter", "name": "main", "version": "1.0.0"},
"grant_id": "<the Granite grant id>",
"flags_json": "{\"start\":41}"
}'
{"run_id":"61948082-9e78-4fc2-bf43-1ad040c080fb","status":"waiting","stdout":"count 41\n","commands":[{"kind":"noop","payload":{}}],"step_seq":0}
POST /v1/nora/interactive-runs/{run_id}/messages sends one message:
curl "$CG/v1/nora/interactive-runs/61948082-9e78-4fc2-bf43-1ad040c080fb/messages" "${AUTH[@]}" -d '{
"owner_uid": "user-nora-counter",
"app_id": "acme.nora-counter",
"message_json": "{\"msg\":\"Increment\",\"payload\":[]}"
}'
{"run_id":"61948082-9e78-4fc2-bf43-1ad040c080fb","status":"waiting","stdout":"count 42\n","commands":[{"kind":"noop","payload":{}}],"step_seq":1}
flags_json and message_json are JSON written as strings. status is
always waiting. Each start and each message is metered like a one-shot
call.
Link Model Gateway
These six endpoints take a person token only, and act for that person.
POST /v1/model/link (no body) asks the person, through Granite, to let
Compute Gateway call Model Gateway for them:
{"request_id":"95e57b82-305b-4598-90be-ba005881635e","consent_url":"https://granitegranted.com/consent/95e57b82-305b-4598-90be-ba005881635e"}
Send the person to consent_url to approve it. (The captured response named
the test double’s address; the live service names Granite, as shown.)
POST /v1/model/link/resolve with {"request_id": "<id>"} finishes the link
once they have approved:
{"grant_id":"5341126f-d18d-4d0b-971a-77f5dd817e1e","linked":true}
Before approval it returns 409
{"error":"grant not yet approved; approve at Granite then retry"}.
Resolving again returns the same grant.
The link is what a module’s model.gateway.call presents to Model Gateway
for that person. Before linking, a model call fails, and a one-shot call
returns 400 with a message naming POST /v1/model/link.
Once linked, POST /v1/model/credentials passes its body to Model Gateway’s
provider-credential endpoint for the person, and returns Model Gateway’s
response, which names the credential’s id. Model Gateway’s docs describe the
body.
Model calls spend that credential only under the person’s spend grant.
POST /v1/model/spend-grants asks the person, through Granite, to let Compute
Gateway spend the credential, up to limit_units model turns in all:
{"credential_id": "<the credential's id>", "limit_units": 100}
It returns a request_id and consent_url, as POST /v1/model/link does.
Granite asks the person to approve it with their passkey.
POST /v1/model/spend-grants/resolve with {"request_id": "<id>"} then
returns the grant:
{"grant_id":"0b8f3f8e-6a3c-4a8e-9a55-6d3c2f1e7b10","granted":true}
Before approval it returns 409. A new POST /v1/model/spend-grants replaces
the person’s earlier spend grant in Compute Gateway; the earlier one stays
valid in Granite until the person revokes it there. Revoking the spend grant
in Granite stops the next model call it would pay for.
Before linking, POST /v1/model/credentials and POST /v1/model/spend-grants
return 409:
{"error":"no linked Model Gateway grant for this owner; start with POST /v1/model/link"}
Let Compute Gateway spend from your wallet
Every call that runs a module pays from the person’s Spend Gateway wallet,
and only under their spend:wallet grant to Compute Gateway.
POST /v1/spend/wallet-grant asks the person, through Granite, to let Compute
Gateway spend up to limit_units units from their wallet in all:
{"limit_units": 1000000}
It returns a request_id and consent_url, as POST /v1/model/link does.
Granite asks the person to approve it with their passkey.
POST /v1/spend/wallet-grant/resolve with {"request_id": "<id>"} then
returns the grant:
{"grant_id":"5c1d9a2e-7f3b-4e6a-8b0c-2d4e6f8a0b1c","granted":true}
Before approval it returns 409. A new POST /v1/spend/wallet-grant replaces
the person’s grant in Compute Gateway; the earlier one stays valid in Granite
until the person revokes it there. A run already started keeps paying under
the grant it started with.
Until the person has a grant, POST /v1/compute/invoke,
POST /v1/compute/runs and POST /v1/nora/interactive-runs return 409
without running anything:
{"error":"`user-1` has not granted Compute Gateway spend:wallet; start with POST /v1/spend/wallet-grant, approve at Granite, then POST /v1/spend/wallet-grant/resolve"}
A grant the person revoked in Granite makes the next call return 400, and
fails a run’s next step.
Link a Pigeon thread
A module’s pigeon.outbox.send posts into one of the person’s Pigeon
threads: to is the thread id. The person first lets Compute Gateway post
there. Both endpoints take a person token only.
GET /v1/pigeon/link names the subject the person’s Pigeon grant must name,
and the threads they have linked:
{"grant_subject":"cg-machine-real-pigeon-sub","linked_threads":[]}
In Pigeon, the person files a grant request (POST /v1/grant-requests) with
requester_chirp_sub set to grant_subject and one resource,
pigeon:threads:in:<thread_id>, and approves it. Then:
POST /v1/pigeon/link
curl "$CG/v1/pigeon/link" "${AUTH[@]}" -d '{
"thread_id": "94a0739d-9ae5-48d6-91ae-d600c2fa4dd6",
"grant_id": "02b6e021-64d8-49d9-84e9-91cdcded0cf9"
}'
{"grant_id":"02b6e021-64d8-49d9-84e9-91cdcded0cf9","linked":true,"thread_id":"94a0739d-9ae5-48d6-91ae-d600c2fa4dd6"}
Compute Gateway checks the grant with Pigeon first. A grant that reaches any
other thread (such as pigeon:threads:*) returns 400:
{"error":"grant 78329c5d-f590-4618-8893-82a91fe5242d for thread 94a0739d-9ae5-48d6-91ae-d600c2fa4dd6: the grant reaches threads other than this one; approve a grant whose only resource is pigeon:threads:in:<thread_id>"}
So does a grant Pigeon refuses for the thread (it names another subject, covers another thread, or is revoked). Linking a thread again replaces its grant. A send to a thread the person has not linked fails without reaching Pigeon. (These responses were captured against a local Pigeon.)
Connectors
A connector lets a module call one third-party HTTPS API with the person’s credential, without the module seeing the credential. Both endpoints take a person token only.
POST /v1/connectors
curl "$CG/v1/connectors" "${AUTH[@]}" -d '{
"name": "weather",
"base_url": "https://example.com/api",
"auth_style": "bearer",
"credential": "sk_live_example"
}'
| Field | Rules |
|---|---|
name | Not empty. Posting the same name again replaces the connector. |
base_url | https:// only, no user:password@, and must resolve to public addresses only |
auth_style | "bearer" (Authorization: Bearer), "basic" (Authorization: Basic, credential as user:password), or {"header": {"name": "x-api-key"}} |
credential | Stored encrypted; never returned |
Response, 201 Created:
{"name":"weather","owner_uid":"sub_8Q3K2M11XZ","base_url":"https://example.com/api","auth_style":"bearer","created_at":"2026-11-05T20:46:01.8708959Z","updated_at":"2026-11-05T20:46:01.8708959Z"}
A bad base_url returns 400, for example
{"error":"base_url: URL must use https"}.
GET /v1/connectors/{name} returns the same metadata, or 404
{"error":"connector not found"}. There is no list endpoint and no delete
endpoint.
A module calls a connector with connector_call(name, request), where
request is {"method": "GET", "path": "/forecast", "body": "..."}
(method defaults to GET; body, if any, is sent as JSON). It gets back
{"status": 200, "body": "..."}. A redirect is returned as it is, not
followed. The grant must hold both connector.call
and connector.call:<name>.
Public endpoint
curl -fsS https://j5afbrmr6qzrwewgxdwacvmedq0ylxvy.lambda-url.us-east-1.on.aws/health