# API Reference Base URL: `http://:3000/api/v1` (behind the nginx reverse proxy: `https:///api/v1`). ## Authentication All `/api/v1/*` endpoints (except none — everything is authenticated) require: ``` Authorization: Bearer ``` API key scopes: | Scope | Access | |------------|--------| | `admin` | All endpoints, including cross-tenant admin and billing administration | | `rooms` | Room CRUD, token issuance/revocation, own billing (`/billing/me`) | | `readonly` | Usage queries (`GET /usage`) | Keys are per-tenant: every room-scoped endpoint enforces tenant isolation and returns `404` (not `403`) for resources owned by another tenant. ## Common errors | Status | `error` code | When | |--------|--------------|------| | 400 | `validation_error` | Malformed request body | | 401 | `unauthorized` | Missing/invalid API key | | 403 | `forbidden` | Scope insufficient for the endpoint | | 404 | `not_found` | Resource missing **or owned by another tenant** | | 409 | `conflict` | e.g. regenerating a finalized invoice | | 429 | `rate_limited` | Per-key token bucket exhausted. `Retry-After` header (seconds) + body `{ "retry_after_seconds": N }`. Defaults: 10 req/s sustained, burst 20 | | 503 | `capacity_exceeded` | Platform at its 30 concurrent open-room ceiling. `Retry-After: 120` + body `{ "current_load": 30, "maximum_capacity": 30 }` | All error bodies: `{ "error": "", "message": "" }` (plus the extra fields above). ## Health & metrics (unauthenticated) | Endpoint | Description | |----------|-------------| | `GET /health`, `GET /api/v1/health` | `200` healthy / `503` degraded (CPU > 90%, memory > 85%). Body includes `cpu_percent`, `memory_percent`, `uptime_seconds` | | `GET /metrics` | Prometheus text exposition. Families: `videochat_http_requests_total`, `videochat_http_request_duration_seconds`, `videochat_rooms_open`, `videochat_tokens_issued_total`, `videochat_rate_limited_total` | The SFU exposes the same two endpoints on port 8080: `GET /health` (adds `active_rooms`/`active_tracks` vs. the 30/120 ceilings) and `GET /metrics` (`sfu_active_rooms`, `sfu_active_peers`, `sfu_active_tracks`, `sfu_ws_connections_total`). ## Tenants (admin only) | Method | Path | Description | |--------|------|-------------| | POST | `/tenants` | Create tenant. Body: `{ "name", "billing_email", "plan_tier"? }` (`free`/`starter`/`pro`/`enterprise`) | | GET | `/tenants` | List all tenants | | GET | `/tenants/:id` | Get tenant | | PUT | `/tenants/:id` | Update tenant (any subset of the create fields) | ## API keys | Method | Path | Description | |--------|------|-------------| | POST | `/tenants/:tenant_id/api-keys` | Create key. Body: `{ "scope"?: "rooms" }`. **The secret is returned only once.** Admin scope required to create keys for other tenants | | GET | `/tenants/:tenant_id/api-keys` | List keys (prefix + metadata, never the secret) | ## Rooms | Method | Path | Description | |--------|------|-------------| | POST | `/rooms` | Create room. Body: `{ "name"?, "max_participants"?: 2 }` (1–50). `503 capacity_exceeded` when 30 rooms are open platform-wide | | GET | `/rooms` | List the tenant's rooms (newest first) | | GET | `/rooms/:id` | Get room (404 across tenants) | | POST | `/rooms/:id/close` | Close room (frees capacity; idempotent-404 if already closed) | Room: `{ "id", "tenant_id", "name", "max_participants", "created_at", "closed_at", "status": "open"|"closed" }`. ## Tokens (SFU join credentials) | Method | Path | Description | |--------|------|-------------| | POST | `/tokens` | Issue join token. Body: `{ "room_id", "display_name"?, "simulcast"?: false }`. Room must be open and tenant-owned. Response: `{ "token", "jti", "ws_url", "expires_at" }` | | DELETE | `/tokens/:jti` | Revoke token. Takes effect on the SFU within its revocation poll interval (5 s default) | | GET | `/tokens/revoked?since=` | Revocation feed consumed by the SFU (not intended for SDK users) | Tokens are ES256 JWTs embedding tenant, room, participant and `jti` claims; the SFU validates them without a database lookup. Default TTL: 1 h (`TOKEN_TTL_SECONDS`). ## Usage | Method | Path | Description | |--------|------|-------------| | GET | `/usage?start=YYYY-MM-DD&end=YYYY-MM-DD` | Daily usage rows for the tenant (`audio_track_minutes`, `video_track_minutes`). `admin` or `readonly` scope | Metering events stream from the SFU into PostgreSQL and are rolled up hourly (`ROLLUP_INTERVAL_SECONDS`). ## Billing — tenant-facing | Method | Path | Description | |--------|------|-------------| | GET | `/billing/me?period=YYYY-MM` | Current tenant's usage, pricing and computed charges for the period (defaults to current month) | ## Billing — admin only Periods are `YYYY-MM`. Amounts are integer cents. | Method | Path | Description | |--------|------|-------------| | GET | `/billing/summary?period=` | All tenants: usage split (audio/video), computed charges | | GET | `/billing/tenants/:id?period=` | Per-tenant detail incl. line items | | GET / POST | `/billing/tenants/:id/adjustments` | List / create adjustments | | DELETE | `/billing/adjustments/:id` | Delete adjustment (locked once the period's invoice is finalized) | | GET / POST | `/billing/tenants/:id/invoices` | List / generate draft invoice (regenerating a draft is idempotent; finalized → 409) | | POST | `/billing/invoices/:id/finalize` | Finalize draft (locks adjustments) | | POST | `/billing/invoices/:id/void` | Void invoice | | GET / POST | `/billing/pricing-tiers` | List / add pricing tiers (effective-dated: new entry for an existing name supersedes from its `effective_from`) | Adjustment kinds: `bonus_credit` (`amount_cents` off), `bonus_minutes` (extends allowance), `percent_discount` (`percent`, scoped `audio`/`video`/`all`), `correction` (signed `amount_cents`). All require a free-text `reason`. Draft invoices are generated automatically for every tenant with usage on `INVOICE_GENERATION_DAY` each month (`AUTO_INVOICE_ENABLED`).