API reference — sandbox surfaces
This page documents the GRIP sandbox HTTP/WS surfaces against the
GRIP spec v0.1 (stable). Everything on this page is
PUBLIC tier per SPEC.md §7
— no key required to read these docs. Public sandbox hosting is coming
soon; until then, both SDKs ship an in-process MockSandbox with the same
method surface, so every flow below is exercisable today without a server —
see the Python and
TypeScript quickstarts.
Base URL for a locally-run sandbox: http://127.0.0.1:8765. All request and
response bodies are canonical-JSON GripRecords where noted; every
quantity is an integer in the units of SPEC.md §1.3 — never a float.
Registrar — identity & registration (P1)
DEV CONSOLE Auth: API key (self-serve OIDC, not yet wired in the sandbox)
Issues an identity.cert for a new asset (SPEC.md §3.1).
POST /v1/register
Content-Type: application/json
{
"node_label": "bldg7",
"public_key": "<base64url ed25519 pubkey>",
"asset_class": "storage",
"capability": {
"import_max_w": 500000,
"export_max_w": 500000,
"ramp_max_w_s": 100000,
"energy_capacity_wh": 2000000,
"control_latency_ms": 500
},
"firmness": "flexible"
}
node_label is combined with the zone suffix to produce the returned
node_id (e.g. bldg7.sandbox.grid). Response: 201 Created with
{ "cert": <signed identity.cert GripRecord>, "chain": [] }. chain is
empty in the sandbox — cross-zone delegation chains arrive with federation
(SPEC.md §6.1).
Envelope server — operating-envelope leases (P2)
DEV CONSOLE Auth: cert key — GRIP-Auth possession-proof header (SPEC.md §7.1)
Requests or re-reads an operating-envelope lease (SPEC.md §3.2). If you know
DHCP, you know this: an envelope is a short-lived lease on watts instead of
an IP address — request it, hold it, renew it. Accepts
GET (read the current grant) or POST (request specific import/export/duration).
Both methods require a GRIP-Auth: kid=<kid> ts=<ts> nonce=<nonce> sig=<sig>
header (SPEC.md §7.1); node_id is derived from the verified cert, not from
a caller-supplied parameter — a request cannot claim another asset's
identity. Both SDKs build this header for you (GripClient.get_envelope /
GripClient.getEnvelope, .request_envelope / .requestEnvelope).
GET /v1/envelope
GRIP-Auth: kid=<kid> ts=<ts> nonce=<nonce> sig=<sig>
POST /v1/envelope
Content-Type: application/json
GRIP-Auth: kid=<kid> ts=<ts> nonce=<nonce> sig=<sig>
{
"want_import_w": 450000,
"want_export_w": 0,
"want_duration_s": 900
}
Response: 200 OK with a signed envelope.grant GripRecord. Lease
semantics (G1-G4) and floor coupling to cleared Capacity Exchange positions
(F1/F2/F5) are covered in SPEC.md §3.2 / §4.4.
The Capacity Exchange isn't wired into the sandbox yet, so grants default
to no forward-position floor — SPEC.md states explicitly that this is a
valid state, not a stub.
Congestion signal — WebSocket fan-out (P3)
DEV CONSOLE Auth: cert key
Per-node subscription to signal.tick / signal.flag records
(SPEC.md §3.3), replayed from PJM Data Miner 2 5-minute RT LMP data every 5
seconds (falls back to bundled fixtures when live PJM access is
unavailable).
wss://127.0.0.1:8765/v1/signals/{node_id}
Subscribe-only: the sandbox does not expect clients to send protocol messages over this socket.
Market data — Capacity Exchange context (§4.7)
PUBLIC Auth: none
BRA-seeded capacity-market context feed. No key required — aggregate market data is public per SPEC.md §7.
GET /v1/market/capacity-context?delivery_year=2027
delivery_year is optional; omit it for the current default.
Transparency log (§6.2, §6.3)
PUBLIC Auth: none
Single-zone, in-memory RFC 6962 Merkle log. Federation (cross-zone delegation, checkpoint cosigning) arrives in a later release — the sandbox signs checkpoints on demand rather than on a periodic loop.
GET /v1/log/checkpoint
GET /v1/log/proof?leaf_index=0
GET /v1/log/entries?start=0&end=10
/v1/log/proof returns the inclusion (audit) path for a leaf; verification
recomputes the Merkle root from the leaf hash and audit path and compares
it against a checkpoint's root_hash — see
SPEC.md §6.3
for the exact algorithm. The Python SDK's grip verify CLI composes these
two endpoints; verify_record in either SDK is the same logic, callable
programmatically.
Errors (§8)
Every error is RFC 9457 application/problem+json with a type token
under urn:grip:error:*. These tokens are immutable wire constants (SPEC.md
§9) — not brand strings, so they don't change on a rebrand.
type |
HTTP status | Meaning |
|---|---|---|
urn:grip:error:invalid-request |
400 | Malformed body or missing required field |
urn:grip:error:unknown-node |
404 | node_id has no registration / no envelope state |
urn:grip:error:envelope-expired |
409 | Grant's valid_until has passed — re-request |
urn:grip:error:floor-violation |
409 | Requested envelope would violate a cleared-position floor |
urn:grip:error:insufficient-collateral |
402 | Exchange order/position rejected on collateral check |
urn:grip:error:out-of-terms-exercise |
409 | Curtailment-option exercise outside its contract terms |
Both SDKs raise a typed GripProtocolError per token with a .hint
telling you what to do next — see the TypeScript
and Python quickstarts.
Not in the sandbox yet
Full federation (root zone keys, delegation chains, checkpoint
cross-signing) and the Capacity Exchange order book (exchange.*). Neither
gap changes the wire schemas in the spec — they're implementation
sequencing, not spec scope.