Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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"
}'
FieldRequiredRules
module_refYesowner_uid, app_id, name, version; see Modules
grant_idYesThe Granite grant to run under, a UUID
payloadNoA 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}
FieldMeaning
statusdone or suspended. A step that fails returns 400 instead, with the run’s id, and the run is saved as failed (see below).
exitThe exit code, once done
outputThe run’s final state, once done; empty otherwise
pending_capThe capability the next tick will use, while suspended. A request cut off mid-run can leave a fast one here too.
errorWhy the run failed, once failed
step_seqCounts 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.

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.

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"
}'
FieldRules
nameNot empty. Posting the same name again replaces the connector.
base_urlhttps:// 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"}}
credentialStored 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