# Platform API — the `/v1` surface

The tenant-keyed HTTP API a third-party app builds on. JSON in, JSON out, over HTTPS. This page is the map; the
**machine-readable spec is canonical** — point tooling at it:

- Production spec (only ops callable today): `https://api.witbitz.chat/v1/openapi.json` (`.yaml`)
- Full design spec (every op, `x-status`-labelled): `https://api.witbitz.chat/v1/openapi.full.json`
- Discovery index: `https://api.witbitz.chat/v1/`

> The **Bridge** is a *separate* contract (party signatures, not tenant keys) — see [the Bridge](./the-bridge.md).

---

## Base + envelope

- **Base URL:** `https://api.witbitz.chat/v1`
- **Success** is enveloped `{ "data": <payload>, "meta"?: {...} }`; **errors** are
  `{ "error": { "code", "message", "request_id"? } }`. One exception: `POST /space` (`x-envelope: false`) returns a
  raw payload and a flat error shape — unifying it is a tracked follow-up.
- Version `1.0.0-beta`. Private beta — the contract may change (coordinated with invited integrators) until GA.

## Authentication

Two tenant API keys, resolved by the `/v1` authorizer to a `tenantId`:

- **Secret key** `wsk_` — full scope, **server-side only**.
- **Publishable key** `wpk_` — browser-safe, public reads + funnel-entry writes. It *identifies* a tenant; it does
  **not** authenticate an end user.
- **Per-collection capability tokens** (`x-collection-token`): the owner `admin_token` or the add-only
  `contributor_token`, for owner/contributor actions on a collection. Wallet/license credentials travel in the
  request **body**, never a URL.

Capability tokens + raw keys are shown **once** at creation and stored only as a hash (or KMS-sealed). Store them
server-side; lost = re-create.

## Operational contract

- **Idempotency:** money/billable ops (`POST /sessions`, `/wallets/grant`, `/collections/{id}/paint`) require an
  `Idempotency-Key`; same key + same body replays the first 2xx byte-for-byte.
- **Rate limiting + kill switch:** throttled at the gateway; per-tenant/per-key-type limits at the authorizer; an
  operator can suspend a tenant (`POST /tenants/{tenant_id}/status`) — every key is denied within ~15s. That op and
  `POST /tenants` are **operator-only** (`X-Admin-Token`, not a tenant key) and are deliberately absent from the
  published spec, so don't go looking for them in `openapi.json`.
- **Secret handling:** one-time-secret responses are `Cache-Control: no-store`; sensitive fields `x-sensitive`;
  BYOK provider keys + the webhook secret are KMS-sealed at rest.

## The surface (by area)

**Agents**
- `GET /catalog` — the public agent catalog (publishable).

**Collections** — the generic content primitive (opaque metadata + items + curation + a keyless render page)
- `POST /collections` · `GET /collections/{id}` · `DELETE /collections/{id}`
- `GET /collections/{id}/render` — **keyless** share page for published content (public)
- items: `GET/POST /collections/{id}/items` · `PATCH/DELETE /collections/{id}/items/{itemId}`
- `POST /collections/{id}/state` — merge opaque app state onto the collection

**Artifacts** — produced media on a collection
- `GET/POST /collections/{id}/artifacts` · `GET/PATCH/DELETE .../artifacts/{artifactId}`
- `POST /collections/{id}/paint` · `POST /collections/{id}/produce`
- `POST /collections/{id}/artifacts/upload` → presigned S3 PUT, then
  `POST /collections/{id}/artifacts/{artifactId}/ready` to stamp it complete — both halves are required

**Sessions**
- `POST /sessions` — launch (or re-summon) an agent; metered, idempotent. Accepts **either** key: the spend
  credential is the coupon in the body, so a publishable key launches nothing without a funded wallet.

**Credits**
- `POST /wallets/lookup` · `POST /wallets/grant` · `POST /checkout` · `GET /usage`
- `POST /wallets/redeem` — **two presentations**: a Lemon Squeezy `license_key`, or a blinded issuer
  `{token, sig, epoch}`. Both mint a `wallet_code` shown once. For a token the **token IS the idempotency
  key** — a retry gets `409 already_spent`, never a second wallet, so don't send an `Idempotency-Key`.
- `POST /epochs` — **issuer role required.** Publish a batch's PUBLIC key and per-token value, after which
  tokens signed by the matching private key are redeemable. The platform holds only public keys, so it can
  verify what an issuer signed and can never mint on its behalf. Set once: rotation is a *new* epoch.

**Roles.** A tenant is an `app` (builds a product, spends credit) or an `issuer` (holds the customer and
the payment relationship, funds wallets from its own prefunded balance) — or both. `grant` and `/epochs`
are issuer-only. A tenant with no `roles` recorded predates the split and is treated as holding both.
Roles are set by an operator, not self-service.

**Tenants** (onboarding + key management)
- `POST /tenants/signup` · `POST /tenants` · `POST /tenants/{tenant_id}/status`
- `GET /tenant` · `GET/POST /tenant/keys` · `DELETE /tenant/keys/{keyId}`
- `PUT /tenant/provider-keys` (BYOK, KMS-sealed) · `PUT /tenant/webhooks`

**Spaces** — `POST /space`, the async Space's single op-dispatched endpoint. It is in `/v1` but **keyless**: it
authenticates by key possession, not a tenant key (see the note below).

**Infra** — `GET /turn` (ephemeral TURN credentials).

> **Not at `/v1`:** ratings, `/agents/*`, `/earnings` and `/sessions/{id}` are `x-status: legacy` — they exist on
> the older surface but are *not* routed here, and calling them returns 404. They appear in `openapi.full.json`
> labelled as such, never in the production spec.

## Access

Invite-only during private beta. Email **hello@witbitz.chat** for keys. Everything above is served openly as a
contract; the *keys* are what's gated.

## Notes

- The async **Spaces** runtime (`/space`) is a separate, content-blind surface (key possession, not tenant keys) —
  see [async Spaces](./async-spaces.md) and [the Space link is the auth](./room-link-auth.md).
- This page is a summary; the OpenAPI at `/v1/openapi.json` is the source of truth (schemas, per-op auth, examples).
