# API Reference

> Complete endpoint documentation for the Synento server API. Base URL: https://api.synento.com

Complete endpoint documentation for the Synento server API.

Base URL: `https://api.synento.com` in production, `http://localhost:8080` when
running the server locally.

## Authentication

Most authenticated endpoints accept either:

- **API key** — `Authorization: Bearer sk_live_xxxxx` or `X-API-Key: sk_live_xxxxx` (required for direct API, CLI, and server-side integrations)
- **Dashboard login session** — `synento_session` HTTP-only cookie set by `POST /v1/auth/login` (used by the Synento dashboard)

An API key belongs to a project, but acts for its whole organisation on the
management routes (projects, keys, webhooks, members). Keep it on your server.

Two narrower credentials exist for agents:

- **Restricted key** — `Authorization: Bearer rk_live_xxxxx`. One project, and
  accepted only by `POST /v1/sessions`, `POST /v1/projects/{projectId}/sessions`,
  `POST /v1/sessions/{sessionId}/connection` (live and replay) and
  `GET /v1/sessions/{sessionId}/manifest`. Every other route refuses it. This is
  what an agent writes into your app's environment. Manage them with
  `GET/POST /v1/restricted-keys` and `DELETE /v1/restricted-keys/{keyId}`.
- **Agent token** (`sat_…`) and **OAuth access tokens** — accepted only by the
  MCP endpoint at `/mcp`, never by the REST API. See
  [Connect your agent](https://synento.com/docs/guides/mcp.md). Agent tokens, connected OAuth clients
  and the agent audit log are managed from a signed-in session:
  `/v1/agent-tokens`, `/v1/oauth/grants`, `/v1/agent-activity`.

## Response Format

All JSON responses use a standard envelope:

```json
{ "data": { ... }, "error": null }           // Success
{ "data": null, "error": { "code": "...", "message": "..." } }  // Error
```

Error codes: `BAD_REQUEST` (400), `NOT_FOUND` (404), `UNAUTHORIZED` (401), `FORBIDDEN` (403), `CONFLICT` (409), `SERVER_ERROR` (500).

---

## Health

### Get Health Status
| | |
|-|-|
| **Method** | `GET` |
| **Path** | `/health` |
| **Auth** | None (public) |

Response: plain text `ok` or error message.

---

## Projects

### List Projects
| | |
|-|-|
| **Method** | `GET` |
| **Path** | `/v1/projects` |
| **Auth** | API key (scoped to organisation) |

Response:
```json
{ "data": [
  { "id": "...", "name": "My App", "org_id": "...", "environment": null, "created_at": "..." }
], "error": null }
```

### Create Project
| | |
|-|-|
| **Method** | `POST` |
| **Path** | `/v1/projects` |
| **Auth** | API key (scoped to organisation) |

Request body:
```json
{ "name": "My App", "environment": "production" }
```

Response (201):
```json
{ "data": { "id": "...", "name": "My App", "org_id": "...", "environment": "production" }, "error": null }
```

---

## Session Management

### Create Session
| | |
|-|-|
| **Method** | `POST` |
| **Path** | `/v1/projects/:projectId/sessions` |
| **Auth** | API key (scoped to project) |

Request body:
```json
{ "name": "Morning Standup" }
```

Response (201):
```json
{ "data": {
  "id": "...", "project_id": "...", "name": "Morning Standup",
  "created_by": "api-key-user",
  "started_at": null, "ended_at": null, "created_at": "..."
}, "error": null }
```

**Billing:** New session creation is blocked if free tier limits are exceeded and no active Stripe subscription.

### Get Session
| | |
|-|-|
| **Method** | `GET` |
| **Path** | `/v1/projects/:projectId/sessions/:sessionId` |
| **Auth** | API key (scoped to project) |

Response: single `Session` object in envelope. 404 if not found or wrong project.

### List Sessions
| | |
|-|-|
| **Method** | `GET` |
| **Path** | `/v1/sessions` |
| **Auth** | API key (scoped to project) |

Returns all sessions for the authenticated project, sorted by `created_at` DESC. No projectId in the path.

Response:
```json
{ "data": [
  { "id": "...", "project_id": "...", "name": "Session 1", ... },
  { "id": "...", "project_id": "...", "name": "Session 2", ... }
], "error": null }
```

### Set Session Visibility
| | |
|-|-|
| **Method** | `PUT` |
| **Path** | `/v1/sessions/:sessionId/visibility` |
| **Auth** | API key (scoped to project) |

Request body:
```json
{ "is_public": true }  // or false to make private
```

Response:
```json
{ "data": true, "error": null }  // updated visibility state
```

### Get Connection Token (Session)
| | |
|-|-|
| **Method** | `POST` |
| **Path** | `/v1/sessions/:sessionId/connection` |
| **Auth** | API key (scoped to project) |

Request body:
```json
{ "user_id": "alice",
  "replay": false,      // omit or false for live; true for replay mode
  "from_ts": 0,         // optional: start replay from this timestamp (ms)
  "speed": 1.0,         // optional: replay speed multiplier
  "focus": ""           // optional: focus on specific stream
}
```

Response (200):
```json
{ "data": {
  "access_token": "...",      // JWT token, short-lived (5 min)
  "expires_in": 300,          // seconds until expiry
  "session": "...",           // session ID
}, "error": null }
```

---

## Live Streaming (WebSocket)

### Connect via WebSocket
| | |
|-|-|
| **Method** | `GET` (upgrade) |
| **Path** | `/ws?session={sessionId}&access_token={token}&user_id=alice` |
| **Auth** | Connection token (JWT) from `/connection` endpoint |

Connects to the live stream. Messages received are JSON events:
```json
{ "type": "message", "stream_type": "media/alice", "timestamp_ms": 1234567, "body": "<base64-jpeg>" }
{ "type": "message", "stream_type": "chat", "timestamp_ms": 1234568, "body": "<base64-json>" }
{ "type": "control", ... }  // join/leave/control signals as meta events
```

---

## Session Events

### Create Session
| | |
|-|-|
| **Method** | `POST` |
| **Path** | `/v1/sessions` |
| **Auth** | API key (project-scoped) |

Creates a session for the authenticated project. Same response shape as creating a project-scoped session.

### Get Session Timeline
| | |
|-|-|
| **Method** | `GET` |
| **Path** | `/v1/sessions/:sessionId/timeline` |
| **Auth** | API key (project-scoped) or dashboard login session (`synento_session` cookie) |

Returns all events in a session's timeline. For large sessions, prefer the windowed playback API below.

---

## Replay & Playback

### Get Playback Timeline (Metadata)
| | |
|-|-|
| **Method** | `GET` |
| **Path** | `/v1/sessions/:sessionId/playback/timeline` |
| **Auth** | API key (project-scoped) or public session access |

Returns compact timeline metadata without loading full event data:
```json
{ "data": {
  "participant_count": 3,
  "stream_ids": ["events", "director", "chat", "media/alice/cam", "media/alice/mic", "media/bob/cam"],
  "total_events_estimated": 15420,
  "min_ts": 1699876000000,
  "max_ts": 1699876300000
}, "error": null }
```

### Get Session Manifest
| | |
|-|-|
| **Method** | `GET` |
| **Path** | `/v1/sessions/:sessionId/manifest` |
| **Auth** | API key (project-scoped) or public session access |

Returns the layout-agnostic reconstruction contract: participants, separated track index (cam/mic/screen), time bounds, stream paths, and capabilities (`separated-tracks`, `event-timeline`, `director-track`, `configurable-layouts`, `captions` when present).

```json
{ "data": {
  "version": 1,
  "session_id": "sess-abc123",
  "epoch": 1699876000000,
  "start_ts": 1699876000000,
  "end_ts": 1699876300000,
  "participants": [
    { "user_id": "alice", "tracks": [
      { "kind": "cam", "stream_id": "a/.../media/alice/cam", "start_ts": 1000, "end_ts": 5000 }
    ]}
  ],
  "streams": { "events": "a/.../events", "chat": "a/.../chat", "director": "a/.../director" },
  "capabilities": ["separated-tracks", "event-timeline", "configurable-layouts"]
}, "error": null }
```

Public sessions: `GET /v1/public/sessions/:sessionId/manifest`.

### Get Playback Window (Windowed Stream Reading)
| | |
|-|-|
| **Method** | `GET` |
| **Path** | `/v1/sessions/:sessionId/playback?from_ts={ms}&window_size=30000` |
| **Auth** | API key (project-scoped) or public session access |

Query parameters:
- `from_ts` — milliseconds to start reading from (required)
- `window_size` — size of window in ms (optional, defaults 30s)

Response:
```json
{ "data": {
  "events": [
    { "seq": 100, "timestamp_ms": 1699876030000, "stream_id": "a/.../media/alice/cam",
      "body_base64": "<base64-payload>" }
  ],
  "base_seq": 100,
  "next_base_seq": null
}, "error": null }
```

Playback windows include events from all separated media tracks, the `events` timeline, `director` track, and `chat`. Media payloads are raw track bytes (WebCodecs `EVC1` or MJPEG); timeline events are JSON in the envelope `{ v, type, ts, actor, data }`.

### Get Public Playback (Unauthenticated)
| | |
|-|-|
| **Method** | `GET` |
| **Path** | `/v1/public/sessions/:sessionId/playback?from_ts={ms}&window_size=30000` |
| **Auth** | None — only works for public sessions (`is_public = true`) |

Same response shape as authenticated playback, but only accessible for public sessions.

### Get Public Playback Timeline (Unauthenticated)
| | |
|-|-|
| **Method** | `GET` |
| **Path** | `/v1/public/sessions/:sessionId/playback/timeline` |
| **Auth** | None — public sessions only |

Same response as authenticated timeline, for public sessions.

### Replay via Expiring Link
| | |
|-|-|
| **Method** | `GET` |
| **Path** | `/v1/public/sessions/:sessionId/replay?token={linkToken}&from_ts=0&window_size=30000` |
| **Auth** | None — replay token required (or public session) |

Reads a playback window using an expiring replay link token. Same response shape as public playback.

---

## Export (MP4)

Export is **asynchronous**. Enqueue a job, poll status, then download the completed MP4.

Send a project API key on every step below.

### Enqueue Session Export
| | |
|-|-|
| **Method** | `GET` |
| **Path** | `/v1/sessions/:sessionId/export` |
| **Auth** | API key (project-scoped) or dashboard login session (`synento_session` cookie) |

Query parameters (optional):

| Parameter | Values | Default |
|-----------|--------|---------|
| `layout` | `grid`, `presenter` | `grid` |
| `orientation` | `landscape` (1280×720), `vertical` (720×1280) | `landscape` |
| `focus` | Participant user id (for presenter layout) | first participant |

Response (202):
```json
{ "data": { "job_id": "550e8400-e29b-41d4-a716-446655440000" }, "error": null }
```

Returns 503 with `USAGE_LIMITS_EXCEEDED` when free tier limits are exceeded.

### Poll Export Job Status
| | |
|-|-|
| **Method** | `GET` |
| **Path** | `/v1/exports/:jobId` |
| **Auth** | API key (project-scoped) or dashboard login session (`synento_session` cookie) |

Response:
```json
{ "data": {
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "completed",
  "room_id": "sess-abc123",
  "download_url": "/v1/exports/550e8400-e29b-41d4-a716-446655440000/download"
}, "error": null }
```

Status values: `queued`, `processing`, `completed`, `failed`.

### Download Export MP4
| | |
|-|-|
| **Method** | `GET` |
| **Path** | `/v1/exports/:jobId/download` |
| **Auth** | API key (project-scoped) or dashboard login session (`synento_session` cookie) |

Response: binary MP4 data when the job is `completed`.

---

## Participant Redaction

### Redact Participant
| | |
|-|-|
| **Method** | `DELETE` |
| **Path** | `/v1/sessions/:sessionId/participants/:userId` |
| **Auth** | API key (project-scoped) |

Deletes all media track streams for the participant (`cam`, `mic`, `screen`) and appends a `participant.redacted` event to the timeline. The rest of the session remains intact.

Response:
```json
{ "data": { "redacted": "alice", "deletedStreams": 3 }, "error": null }
```

---

## Webhooks

### List Webhooks
| | |
|-|-|
| **Method** | `GET` |
| **Path** | `/v1/webhooks` |
| **Auth** | API key (project-scoped) |

Response: array of active webhooks for the project.
```json
{ "data": [
  { "id": "...", "project_id": "...", "url": "https://example.com/hook",
    "events": ["session.created", "session.ended"], "is_active": true }
], "error": null }
```

### Create Webhook
| | |
|-|-|
| **Method** | `POST` |
| **Path** | `/v1/webhooks` |
| **Auth** | API key (project-scoped) |

Request body:
```json
{ "url": "https://example.com/hook",
  "events": ["session.created", "session.started", "session.ended"] }
```

Valid event types: `session.created`, `session.started`, `session.ended`, `recording.ready`, `export.completed`

Response (201):
```json
{ "data": { "id": "...", "project_id": "...", "url": "...",
  "events": ["session.created"], "is_active": true }, "error": null }
```

---

## Usage & Billing

### Get Usage Records
| | |
|-|-|
| **Method** | `GET` |
| **Path** | `/v1/projects/:projectId/usage` (project-scoped) or `/v1/usage` |
| **Auth** | API key (project-scoped) or dashboard login session (`synento_session` cookie) |

Response:
```json
{ "data": {
  "summary": { "streaming_minutes_total": 125.3, "storage_bytes_total": 524288000 },
  "records": [
    { "id": "...", "project_id": "...", "session_id": "...",
      "usage_type": "streaming_minutes", "value": 125.3,
      "recorded_at": "..." }
  ]
}, "error": null }
```

### Get Billing Summary
| | |
|-|-|
| **Method** | `GET` |
| **Path** | `/v1/billing/summary` |
| **Auth** | API key (project-scoped) — no projectId in path required |

Response:
```json
{ "data": {
  "streaming_minutes_used": 125.3,
  "storage_bytes_used": 524288000,
  "cost_breakdown": {
    "streaming_cost": 12.53,
    "storage_cost": 0.03,
    "total_due": 12.56
  }
}, "error": null }
```

### Get Billing Limits
| | |
|-|-|
| **Method** | `GET` |
| **Path** | `/v1/billing/limits` |
| **Auth** | API key (project-scoped) or dashboard login session (`synento_session` cookie) |

Response:
```json
{ "data": {
  "within_free_tier": true,
  "streaming_minutes_used": 125.3,
  "storage_bytes_used_gb": 0.5,
  "has_active_subscription": false,
  "is_blocked": false,
  "status_message": ""
}, "error": null }
```

### Create Stripe Billing Portal Session (Project-scoped)
| | |
|-|-|
| **Method** | `POST` |
| **Path** | `/v1/projects/:projectId/billing/portal` |
| **Auth** | API key (project-scoped) or dashboard login session (`synento_session` cookie) |

Returns a Stripe billing portal URL. Response includes `portal_url` for redirecting the user to manage their subscription.

### Create Stripe Billing Portal Session (Org-scoped)
| | |
|-|-|
| **Method** | `POST` |
| **Path** | `/v1/billing/portal` |
| **Auth** | API key (project-scoped) — no projectId needed |

Same as above but uses organisation-level Stripe customer. Recommended for dashboards (no projectId required).

---

## API Key Management

### List API Keys
| | |
|-|-|
| **Method** | `GET` |
| **Path** | `/v1/api-keys` |
| **Auth** | API key (project-scoped) — no projectId needed in path |

Returns metadata for keys belonging to the authenticated project. Secrets are not included (only ID and creation date).

Response:
```json
{ "data": [
  { "id": "...", "project_id": "...", "created_at": "..." }
], "error": null }
```

### Create API Key
| | |
|-|-|
| **Method** | `POST` |
| **Path** | `/v1/projects/:projectId/api-keys` |
| **Auth** | API key (project-scoped) |

Response (201): returns the new key's ID and raw secret (`sk_live_...`). The secret is shown only once — store it securely.

### Delete API Key
| | |
|-|-|
| **Method** | `DELETE` |
| **Path** | `/v1/api-keys/:keyId` |
| **Auth** | API key (project-scoped) — no projectId needed in path |

Response: success confirmation. The deleted key is immediately invalidated.

---

## Authentication & User Management (Email/Password)

### Register
| | |
|-|-|
| **Method** | `POST` |
| **Path** | `/v1/auth/register` |
| **Auth** | None (public) |

Request body: `{"email": "user@example.com", "password": "secret123"}`

Response (201): creates user + organisation + project, sets session cookie.
```json
{ "data": { "id": "...", "email": "user@example.com", ... }, "error": null }
```

### Login
| | |
|-|-|
| **Method** | `POST` |
| **Path** | `/v1/auth/login` |

Request body: `{"email": "user@example.com", "password": "secret123"}`

Response (200): sets session cookie (`synento_session`). Session expires in 7 days.

### Logout
| | |
|-|-|
| **Method** | `POST` |
| **Path** | `/v1/auth/logout` |

Response (200): clears session cookie.

### Get Current User
| | |
|-|-|
| **Method** | `GET` |
| **Path** | `/v1/me` |

Response (200): returns current authenticated user data.
```json
{ "data": { "id": "...", "email": "user@example.com" }, "error": null }
```

---

## Organisation Membership (Invitations)

### Send Invitation to Project's Organisation
| | |
|-|-|
| **Method** | `POST` |
| **Path** | `/v1/projects/:projectId/invitations` |
| **Auth** | API key (project-scoped) |

Request body: `{"email": "new@example.com", "role": "member"}`

Response (201):
```json
{ "data": { "id": "...", "org_id": "...", "email": "new@example.com",
  "role": "member", "created_by": "...", "accepted_at": null }, "error": null }
```

### Redeem Invitation Token
| | |
|-|-|
| **Method** | `POST` |
| **Path** | `/v1/invitations/redeem` |

Request body: `{"token": "invite-token-xyz"}`

Auth: session cookie (user must be logged in). Updates the user's `org_id` and role.

Response (200): success confirmation with updated org membership.

---

## Public Session Catalog

### List Public Sessions
| | |
|-|-|
| **Method** | `GET` |
| **Path** | `/v1/public/sessions` |
| **Auth** | None (public) |

Returns all sessions with `is_public = true`, sorted by creation date DESC. No project IDs exposed for privacy.

Response:
```json
{ "data": [
  { "id": "...", "name": "Public Demo", "created_at": "...",
    "started_at": "...", "ended_at": "..." }
], "error": null }
```

---

## Replay Links

### Create Expiring Replay Link
| | |
|-|-|
| **Method** | `POST` |
| **Path** | `/v1/sessions/:sessionId/replay-links` |
| **Auth** | API key (project-scoped) |

Request body: `{"expires_in_minutes": 60}` (optional, defaults to a reasonable TTL)

Response (201):
```json
{ "data": {
  "link": "https://host/v1/public/sessions/{sessionId}/replay?token={token}",
  "token": "...",
  "expires_at": "..."
}, "error": null }
```

The replay link provides unauthenticated windowed playback access until it expires (max 30 days = 43200 minutes).
