API Reference
Complete reference for all StreamGate API endpoints. Endpoints are grouped by access level: Public, Admin, Internal, and HLS Streaming.
Public API
These endpoints require no authentication and are used by the viewer's browser.
POST /api/tokens/validate
Validates an access code and returns a JWT playback token.
Rate limit: 5 requests/minute per IP
Request:
curl -X POST http://localhost:3000/api/tokens/validate \
-H "Content-Type: application/json" \
-d '{"code": "Ab3kF9mNx2Qp"}'
Success Response (200):
{
"event": {
"title": "Annual Conference 2025",
"description": "Live keynote stream",
"startsAt": "2025-03-15T09:00:00.000Z",
"endsAt": "2025-03-15T17:00:00.000Z",
"posterUrl": "https://example.com/poster.jpg",
"isLive": true
},
"playbackToken": "eyJhbGciOiJIUzI1NiJ9...",
"playbackBaseUrl": "http://localhost:4000",
"streamPath": "/streams/abc-123-uuid/",
"expiresAt": "2025-03-17T17:00:00.000Z",
"tokenExpiresIn": 3600
}
playbackBaseUrl is dynamically derived from the request's hostname. If the request comes from 192.168.0.11:3000, the response returns http://192.168.0.11:4000 instead of http://localhost:4000.
Error Responses:
| Status | Condition | Body |
|---|---|---|
400 | Missing or non-alphanumeric code | { "error": "Access code is required" } |
401 | Code not found in database | { "error": "Invalid access code" } |
403 | Token is revoked | { "error": "Access code has been revoked" } |
403 | Event is deactivated | { "error": "This event is not currently available" } |
409 | Token in use on another device | { "error": "This access code is currently in use on another device", "inUse": true } |
410 | Token has expired | { "error": "Access code has expired" } |
429 | Rate limited | { "error": "Too many requests. Please try again later." } |
POST /api/playback/refresh
Refreshes an expiring JWT. The current JWT is sent as a Bearer token.
Rate limit: 12 requests/hour per token code
Request:
curl -X POST http://localhost:3000/api/playback/refresh \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
Success Response (200):
{
"playbackToken": "eyJhbGciOiJIUzI1NiJ9...(new token)",
"tokenExpiresIn": 3600
}
Error Responses:
| Status | Condition | Body |
|---|---|---|
401 | Missing or invalid JWT | { "error": "Valid playback token required" } |
403 | Token has been revoked since last refresh | { "error": "Access has been revoked" } |
410 | Token or event has expired | { "error": "Access has expired" } |
429 | Rate limited | { "error": "Too many refresh requests" } |
POST /api/playback/heartbeat
Keeps the active session alive. Called every 30 seconds by the player.
Request:
curl -X POST http://localhost:3000/api/playback/heartbeat \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
Success Response (200):
{
"ok": true
}
Error Responses:
| Status | Condition | Body |
|---|---|---|
401 | Missing or invalid JWT | { "error": "Valid playback token required" } |
404 | Session no longer exists | { "error": "Session not found" } |
POST /api/playback/release
Releases the active session. Called on player close via navigator.sendBeacon().
Request:
curl -X POST http://localhost:3000/api/playback/release \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
Success Response (200):
{
"released": true
}
Error Responses:
| Status | Condition | Body |
|---|---|---|
401 | Missing or invalid JWT | { "error": "Valid playback token required" } |
If the session was already released or timed out, the endpoint still returns { "released": true } to avoid unnecessary errors in the client.
GET /api/events/:id/status
Returns the current status of an event. Used by the pre-event waiting screen.
Request:
curl http://localhost:3000/api/events/abc-123-uuid/status
Success Response (200):
{
"eventId": "abc-123-uuid",
"status": "live",
"startsAt": "2025-03-15T09:00:00.000Z",
"endsAt": "2025-03-15T17:00:00.000Z"
}
The status field is one of: not-started, live, ended, recording.
Error Responses:
| Status | Condition | Body |
|---|---|---|
404 | Event not found | { "error": "Event not found" } |
Admin API
All admin endpoints require an authenticated session via the iron-session cookie. Send credentials first via /api/admin/login.
Authentication
POST /api/admin/login
Rate limit: 10 requests/minute per IP
curl -X POST http://localhost:3000/api/admin/login \
-H "Content-Type: application/json" \
-d '{"password": "your-admin-password"}' \
-c cookies.txt
Success (200): { "success": true }
Error:
| Status | Condition | Body |
|---|---|---|
401 | Wrong password | { "error": "Invalid password" } |
429 | Rate limited | { "error": "Too many login attempts" } |
POST /api/admin/logout
curl -X POST http://localhost:3000/api/admin/logout -b cookies.txt
Response (200): { "success": true }
GET /api/admin/session
Check if the current session is authenticated:
curl http://localhost:3000/api/admin/session -b cookies.txt
Response (200): { "authenticated": true }
Events
GET /api/admin/events
List all events:
curl http://localhost:3000/api/admin/events -b cookies.txt
Response (200):
{
"events": [
{
"id": "abc-123",
"title": "Annual Conference 2025",
"description": "Live keynote stream",
"streamUrl": null,
"posterUrl": null,
"startsAt": "2025-03-15T09:00:00.000Z",
"endsAt": "2025-03-15T17:00:00.000Z",
"accessWindowHours": 48,
"isActive": true,
"isArchived": false,
"createdAt": "2025-03-01T12:00:00.000Z",
"updatedAt": "2025-03-01T12:00:00.000Z",
"_count": { "tokens": 100 }
}
]
}
POST /api/admin/events
Create a new event:
curl -X POST http://localhost:3000/api/admin/events \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{
"title": "Annual Conference 2025",
"description": "Live keynote stream",
"startsAt": "2025-03-15T09:00:00.000Z",
"endsAt": "2025-03-15T17:00:00.000Z",
"accessWindowHours": 48,
"streamUrl": "https://origin.example.com/live/stream.m3u8",
"posterUrl": "https://example.com/poster.jpg"
}'
Response (201): Full event object.
GET /api/admin/events/:id
Get single event with details:
curl http://localhost:3000/api/admin/events/abc-123 -b cookies.txt
PUT /api/admin/events/:id
Update an event:
curl -X PUT http://localhost:3000/api/admin/events/abc-123 \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{ "title": "Updated Title", "accessWindowHours": 72 }'
DELETE /api/admin/events/:id
Delete an event and all associated tokens (cascading delete):
curl -X DELETE http://localhost:3000/api/admin/events/abc-123 -b cookies.txt
Response (200): { "deleted": true }
PATCH /api/admin/events/:id/activate
curl -X PATCH http://localhost:3000/api/admin/events/abc-123/activate -b cookies.txt
PATCH /api/admin/events/:id/deactivate
Deactivating an event effectively revokes all its tokens (picked up by HLS revocation sync).
curl -X PATCH http://localhost:3000/api/admin/events/abc-123/deactivate -b cookies.txt
PATCH /api/admin/events/:id/archive
curl -X PATCH http://localhost:3000/api/admin/events/abc-123/archive -b cookies.txt
PATCH /api/admin/events/:id/unarchive
curl -X PATCH http://localhost:3000/api/admin/events/abc-123/unarchive -b cookies.txt
Tokens
GET /api/admin/events/:id/tokens
List tokens for a specific event:
curl http://localhost:3000/api/admin/events/abc-123/tokens -b cookies.txt
Response (200):
{
"tokens": [
{
"id": "tok-uuid",
"code": "Ab3kF9mNx2Qp",
"eventId": "abc-123",
"label": "VIP Guest 1",
"isRevoked": false,
"revokedAt": null,
"redeemedAt": "2025-03-15T10:30:00.000Z",
"redeemedIp": "192.168.1.50",
"expiresAt": "2025-03-17T17:00:00.000Z",
"createdAt": "2025-03-01T12:00:00.000Z"
}
]
}
POST /api/admin/events/:id/tokens
Generate tokens for an event (batch: 1–500):
curl -X POST http://localhost:3000/api/admin/events/abc-123/tokens \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{ "count": 50, "label": "Batch A" }'
Response (201):
{
"tokens": [
{ "id": "tok-1", "code": "Ab3kF9mNx2Qp", "label": "Batch A", ... },
{ "id": "tok-2", "code": "Xp2mNqR7sT4w", "label": "Batch A", ... }
],
"count": 50
}
GET /api/admin/events/:id/tokens/export
Export tokens as CSV:
curl http://localhost:3000/api/admin/events/abc-123/tokens/export \
-b cookies.txt -o tokens.csv
Returns CSV with headers: code,label,status,createdAt,expiresAt
GET /api/admin/tokens
List all tokens across all events (with optional filters):
curl "http://localhost:3000/api/admin/tokens?status=revoked&eventId=abc-123" \
-b cookies.txt
PATCH /api/admin/tokens/:id/revoke
Revoke a single token:
curl -X PATCH http://localhost:3000/api/admin/tokens/tok-uuid/revoke -b cookies.txt
Response (200): Updated token object with isRevoked: true and revokedAt timestamp.
PATCH /api/admin/tokens/:id/unrevoke
Unrevoke a previously revoked token:
curl -X PATCH http://localhost:3000/api/admin/tokens/tok-uuid/unrevoke -b cookies.txt
POST /api/admin/tokens/bulk-revoke
Revoke multiple tokens at once:
curl -X POST http://localhost:3000/api/admin/tokens/bulk-revoke \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{ "tokenIds": ["tok-1", "tok-2", "tok-3"] }'
Response (200): { "revoked": 3 }
GET /api/admin/dashboard
Get dashboard statistics:
curl http://localhost:3000/api/admin/dashboard -b cookies.txt
Response (200):
{
"totalEvents": 12,
"activeEvents": 5,
"totalTokens": 500,
"redeemedTokens": 150,
"activeViewers": 42
}
Internal API
Used by the HLS Media Server for revocation synchronization. Authenticated via X-Internal-Api-Key header.
GET /api/revocations
Fetches token revocations and event deactivations since a given timestamp.
Request:
curl "http://localhost:3000/api/revocations?since=2025-03-15T10:00:00.000Z" \
-H "X-Internal-Api-Key: your-internal-api-key"
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
since | ISO 8601 string | Yes | Only return revocations after this timestamp |
Success Response (200):
{
"revocations": [
{
"code": "Ab3kF9mNx2Qp",
"revokedAt": "2025-03-15T11:30:00.000Z"
},
{
"code": "Xp2mNqR7sT4w",
"revokedAt": "2025-03-15T12:00:00.000Z"
}
],
"eventDeactivations": [
{
"eventId": "evt-456",
"deactivatedAt": "2025-03-15T11:45:00.000Z",
"tokenCodes": ["Cd4eF5gH6iJk", "Lm7nO8pQ9rSt"]
}
],
"serverTime": "2025-03-15T12:05:00.000Z"
}
Error Responses:
| Status | Condition | Body |
|---|---|---|
401 | Missing or invalid X-Internal-Api-Key | { "error": "Unauthorized" } |
400 | Missing since parameter | { "error": "since parameter required" } |
The serverTime field in the response should be used as the since value for the next poll. This ensures no revocations are missed between polls, even if clocks are slightly out of sync.
HLS Streaming API
These endpoints are served by the HLS Media Server (default port 4000).
GET /streams/:eventId/*.m3u8
Serves HLS manifest files. Requires JWT authorization.
Request:
curl http://localhost:4000/streams/abc-123/stream.m3u8 \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
Response: HLS manifest content with Content-Type: application/vnd.apple.mpegurl.
Error Responses:
| Status | Condition | Body |
|---|---|---|
401 | No JWT provided | { "error": "Authorization required" } |
403 | JWT invalid/expired/revoked/wrong path | { "error": "Access denied" } |
404 | Manifest file not found | { "error": "Not found" } |
GET /streams/:eventId/*.ts
Serves HLS transport stream segments. Same auth as manifests.
curl http://localhost:4000/streams/abc-123/segment-001.ts \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..." \
-o segment.ts
GET /health
Health check endpoint (no auth required):
curl http://localhost:4000/health
Response (200):
{
"status": "ok",
"mode": "hybrid",
"revocationCacheSize": 42,
"lastSyncAgoSeconds": 15
}
DELETE /admin/cache/:eventId
Clears cached segments for an event (no JWT auth — restrict via network policy):
curl -X DELETE http://localhost:4000/admin/cache/abc-123
Response (200): { "cleared": true }
This endpoint is not protected by JWT authentication. In production, restrict access via firewall rules or reverse proxy configuration.