> ## Documentation Index
> Fetch the complete documentation index at: https://docs.enfinitos.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sandbox API reference

> Public HTTP endpoints the EnfinitOS sandbox exposes — provisioning, rights bounded context, delivery, proof packs, constraint demos, audit inspector.

The sandbox HTTP API is a complete subset of the production API,
cookie-scoped per visitor. All routes run on Cloudflare Pages'
edge runtime at `https://sandbox.api.enfinitos.com` and respect a
per-IP rate limit of 10 req/10s (burst) + 200 req/hour (sustained).

## Auth

Every route after `POST /api/sandbox/tenant` requires the
`enf_sandbox_tenant` cookie set by that initial call. The cookie
value is `<tenantId>.<hmac>`; tampering breaks the HMAC and the
cookie is rejected.

## Tenant lifecycle

### POST /api/sandbox/tenant

Provision a fresh tenant with the standard seed scenario (3 bases,
4 rights across DOOH, CTV, mobile, messaging). Idempotent — if a
valid cookie is present and the tenant exists, returns the
existing tenant.

**Response:**

```json theme={null}
{
  "ok": true,
  "data": {
    "tenantId": "tnt_abc...",
    "orgId": "org_xyz...",
    "createdAt": "2026-05-14T10:00:00.000Z",
    "counts": { "bases": 3, "rights": 4, ... },
    "rights": [{ "id": "rgh_...", "substrate": "DOOH", ... }]
  },
  "rebound": false
}
```

### GET /api/sandbox/tenant

Read-only tenant summary. 404 if no tenant; 410 if tenant evicted
(Worker isolate eviction).

### POST /api/sandbox/reset

Drop the tenant + clear the cookie. Idempotent.

## Rights bounded context

### POST /api/sandbox/rights/issue

Body: `{ basisId, substrate, scope, effectiveFrom, effectiveUntil?, parentRightId? }`

Issue a new right under an existing basis.

### POST /api/sandbox/rights/suspend

Body: `{ rightId, reason?, action?: "suspend" | "resume" }`

Suspend a right (default) or resume a suspended one.

### POST /api/sandbox/rights/revoke

Body: `{ rightId, reason? }`

Terminal — cannot be undone.

## Offers

### POST /api/sandbox/offers/propose

Body: `{ rightId, toOrgId, scope, termsRef?, expiresAt? }`

Propose an offer to share / cede / sublet a right. Default expiry
is 7 days from now.

### POST /api/sandbox/offers/accept

Body: `{ offerId }`

Issues a derived right whose `parentRightId` points at the original
right and `contentHash` references the accepted offer.

### POST /api/sandbox/offers/counter

Body: `{ offerId, scope, termsRef?, expiresAt? }`

Counter-offer with revised terms. Original transitions PROPOSED →
COUNTERED; a new offer goes the opposite direction with
`termsRef: "counter_of:<originalOfferId>"`.

### POST /api/sandbox/offers/withdraw

Body: `{ offerId, action?: "withdraw" | "reject", reason? }`

Withdraw (proposer) or reject (recipient). Both move PROPOSED to
WITHDRAWN/REJECTED respectively.

## Challenges

### POST /api/sandbox/challenges/open

Body: `{ rightId, challengerOrgId?, reason }`

Open a challenge against an active right. Default challengerOrgId
is `org_thirdparty_sandbox`.

### POST /api/sandbox/challenges/resolve

Body: `{ challengeId, upheld?, resolution?, action?: "resolve" | "withdraw" }`

Resolve as upheld (revokes the right) or overturned (right stays
ACTIVE). `action: "withdraw"` resolves as WITHDRAWN — the challenger
backed down.

## Delivery + proof

### POST /api/sandbox/run-demo

Body: `{ eventCount?: number (1..100, default 12), seed?: number }`

Fire N synthetic delivery events with substrate-realistic dwell
times. Seals a fresh `SignedProofPack` containing every receipt
plus the projected metering and reconciled settlement.

### POST /api/sandbox/constraint-demo

Body: `{ scenario: "messaging-optout" | "automotive-speed" | "drone-remote-id-missing" }`

Deliberately configure a delivery that fails the pre-render
constraint gate. Returns structured violations; no receipt is
signed. Demonstrates real compliance enforcement.

### GET /api/sandbox/proof-packs

List sealed packs (summaries only). Newest last.

### GET /api/sandbox/proof-packs/\[packId]

Full pack JSON plus the sandbox `VerificationKey`. The verification
key is embedded so an offline auditor can verify without a second
round-trip.

## Provenance + inspector

### GET /api/sandbox/provenance?rightId=...

Returns the lineage chain — basis → root right → derived rights →
target right.

### GET /api/sandbox/inspect

Full read-only tenant snapshot — every basis, right, offer,
challenge, event, pack header. Consumed by the Tenant Inspector
UI.

## Verification keys

### GET /api/sandbox/runtime-keys

Public, unauthenticated. Returns the sandbox's verification-key
directory. The auditor SDK consumes this to verify sandbox-issued
packs.

```json theme={null}
{
  "ok": true,
  "contractVersion": "ws30.v1",
  "data": {
    "keys": [{
      "keyId": "<sandbox-key-id>",
      "algorithm": "ed25519",
      "publicKey": "<base64url>",
      "notBefore": "2026-01-01T00:00:00.000Z",
      "notAfter": null,
      "revokedAt": null,
      "purpose": "sandbox_proof_signing"
    }],
    "issuedAt": "<timestamp>",
    "snapshotId": "<sandbox-key-id>"
  }
}
```

## Error responses

Every error returns the same envelope:

```json theme={null}
{
  "ok": false,
  "error": "STABLE_CODE",
  "message": "Human-readable explanation"
}
```

Stable codes:

| Code               | Meaning                                                                                         |
| ------------------ | ----------------------------------------------------------------------------------------------- |
| `NO_TENANT`        | Cookie missing — call `POST /api/sandbox/tenant` first.                                         |
| `TENANT_EVICTED`   | Worker isolate evicted the tenant. Hit Reset and start fresh.                                   |
| `INVALID_BODY`     | Body wasn't valid JSON.                                                                         |
| `MISSING_FIELDS`   | Required body fields not supplied.                                                              |
| `RATE_LIMITED`     | Per-IP quota hit. Respect the `Retry-After` header.                                             |
| `HANDLER_ERROR`    | Server-side exception (e.g. state-machine refusal). The `message` carries the structured cause. |
| `UNKNOWN_SCENARIO` | constraint-demo scenario string isn't recognised.                                               |
