# 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/` --- ## Base + envelope - **Base URL:** `https://api.witbitz.chat/v1` - **Success** is enveloped `{ "data": , "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. ## 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](./identity-and-admission.md). - This page is a summary; the OpenAPI at `/v1/openapi.json` is the source of truth (schemas, per-op auth, examples).