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.