WatchLive API
Automate your events, pull recordings into your own systems, and react to streams and sales in real time.
Introduction
The WatchLive API is a REST interface over your organization's events, camera feeds, recordings and sales. It speaks JSON, authenticates with an API key, and sends webhooks when things happen.
The base URL is https://api.watchlive.org/v1. Every endpoint is scoped to the organization that owns the key — there is no way to read another organization's data.
Getting access
The API requires:
- An Enterprise WatchLive organization with an active subscription.
- An API key, created from your dashboard.
Enterprise is priced per contract. Talk to us about upgrading. Once you are on Enterprise, create keys at Dashboard → Developers.
Calling the API without Enterprise returns 402 PLATFORM_ACCESS_REQUIRED.
Quickstart
List your events:
curl https://api.watchlive.org/v1/events \
-H "Authorization: Bearer wl_live_your_key_here"{
"object": "list",
"data": [
{
"object": "event",
"id": "45a45b4e-c865-454b-9df3-ce48b1c69b96",
"name": "County Finals",
"slug": "county-finals",
"status": "live",
"visibility": "public",
"ticket_price": "12.00",
"created_at": "2026-07-28T00:46:52.402Z"
}
],
"has_more": false,
"next_cursor": null
}Authentication
Pass your key as a bearer token. An x-api-key header is also accepted.
# Either of these works
curl https://api.watchlive.org/v1/events -H "Authorization: Bearer wl_live_..."
curl https://api.watchlive.org/v1/events -H "x-api-key: wl_live_..."Keys are shown once, when you create them. We store only a hash, so a lost key cannot be recovered — rotate it instead.
Rotating issues a new key and keeps the old one working for 24 hours, so you can roll it through a deploy without downtime.
Scopes
Each key carries an explicit list of scopes. A call outside them returns 403.
read:eventsread:fieldsread:recordingsread:purchasesread:scheduleread:analyticswrite:eventswrite:fieldswrite:recordingswrite:schedulemanage:webhooksGrant the narrowest set that works. A key that only reads recordings cannot touch your sales data even if it leaks.
Write bodies are validated strictly: an unrecognised or wrong-case field returns 400 rather than being quietly ignored, so a typo can never look like a successful save.
Test mode
Keys are either wl_live_ or wl_test_.
- Test keys read your real data, so responses look exactly like production.
- Writes with a test key are accepted and discarded — you get a
test_mode_responseand nothing changes.
Pagination
List endpoints are cursor paginated. Pass limit (default 20, max 100) and cursor. Keep going while has_more is true.
curl "https://api.watchlive.org/v1/recordings?limit=50" \
-H "Authorization: Bearer wl_live_..."
# then
curl "https://api.watchlive.org/v1/recordings?limit=50&cursor=eyJjcmVhdGVkQXQi..." \
-H "Authorization: Bearer wl_live_..."Cursors are keyset-based, so pages stay stable even while new records are being created.
Errors
Failures return a stable machine-readable code. Branch on the code, not the message.
{
"error": {
"type": "permission_error",
"code": "INSUFFICIENT_SCOPE",
"message": "This API key is missing the required \"read:purchases\" scope.",
"status": 403,
"request_id": "req-14",
"required_scope": "read:purchases"
}
}| Code | Status | Meaning |
|---|---|---|
| API_KEY_REQUIRED | 401 | No key supplied |
| INVALID_API_KEY | 401 | Unknown, revoked or expired key |
| PLATFORM_ACCESS_REQUIRED | 402 | Organization is not on Enterprise |
| SUBSCRIPTION_REQUIRED | 402 | Subscription is not active |
| INSUFFICIENT_SCOPE | 403 | Key lacks the required scope |
| NOT_FOUND | 404 | No such object in your organization |
| INVALID_REQUEST | 400 | Malformed parameters |
| RATE_LIMIT_EXCEEDED | 429 | Too many requests |
Rate limits
Limits are per key: 600 requests/minute for live keys, 60/minute for test keys.
Every response carries x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset. On a 429, honor retry-after and back off.
Events
/v1/eventsread:eventsList your events. Filter with status.
/v1/events/{'{id}'}read:eventsRetrieve one event.
/v1/events/{'{id}'}/scheduleread:scheduleThe event's schedule items.
/v1/eventswrite:eventsCreate an event. It starts as a draft; publish it with a PATCH.
/v1/events/{'{id}'}write:eventsUpdate an event. Setting status to published emits event.published.
/v1/events/{'{id}'}/schedulewrite:scheduleAdd a schedule item.
/v1/schedule/{'{itemId}'}write:scheduleRemove a schedule item.
Fields
A field is a camera feed within an event.
/v1/events/{'{id}'}/fieldsread:fieldsList the feeds for an event, with live status and viewer counts.
/v1/fields/{'{id}'}write:fieldsRename a feed or change its price, sort order or viewer-count visibility.
Ingest credentials — stream keys, RTMP/SRT URLs and passphrases — are never returned by the API. They are visible only to signed-in organizers in the dashboard.
Recordings
/v1/recordingsread:recordingsList recordings. Filter with event_id or status.
/v1/recordings/{'{id}'}read:recordingsRetrieve one recording.
/v1/recordings/{'{id}'}write:recordingsChange a recording's title, visibility or VOD pricing.
Purchases
/v1/purchasesread:purchasesList ticket sales. Filter with event_id, status, created_after and created_before.
Read-only. Purchases must go through checkout so that fees and entitlements are applied correctly, so there is no endpoint for creating one.
Webhook endpoints
/v1/webhook_endpointsmanage:webhooks/v1/webhook_endpointsmanage:webhooksRegister an endpoint. The signing secret is returned once.
/v1/webhook_endpoints/{'{id}'}manage:webhooks/v1/webhook_endpoints/{'{id}'}manage:webhooks/v1/events_logmanage:webhooksEvery event we generated for you, whether or not delivery succeeded.
/v1/events_log/{'{id}'}/replaymanage:webhooksRe-deliver an event to your active endpoints.
/v1/webhook_deliveriesmanage:webhooksDelivery attempts, with response codes and errors.
Receiving webhooks
Register an HTTPS endpoint and choose the events you want:
curl -X POST https://api.watchlive.org/v1/webhook_endpoints \
-H "Authorization: Bearer wl_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/watchlive/webhook",
"enabled_events": ["stream.started", "stream.ended", "purchase.completed"]
}'Event types:
stream.startedstream.endedrecording.readyevent.publishedpurchase.completedvod_purchase.completedDelivery is at least once. Retries run at 1m, 5m, 30m, 2h and 6h; deduplicate on the event id. An endpoint that keeps failing is disabled automatically and can be re-enabled from your dashboard.
Respond 2xx quickly and do your work asynchronously — we time out after 10 seconds.
Verifying signatures
Every delivery carries an X-WatchLive-Signature header:
X-WatchLive-Signature: t=1785199740,v1=98140a550dbf5b76dc0686...v1 is HMAC-SHA256(secret, "{t}.{raw body}"). Verify against the raw request body — parsing and re-serializing the JSON changes the bytes and the signature will not match.
import crypto from 'crypto';
// express.raw keeps the body as bytes — express.json() would break verification
app.post('/watchlive/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.headers['x-watchlive-signature'];
const [tPart, v1Part] = String(header).split(',');
const timestamp = tPart.split('=')[1];
const signature = v1Part.split('=')[1];
// Reject replays of an old, previously-valid delivery
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.status(400).send('Timestamp too old');
}
const expected = crypto
.createHmac('sha256', process.env.WATCHLIVE_WEBHOOK_SECRET)
.update(`${timestamp}.${req.body}`)
.digest('hex');
// Constant-time compare — a plain === leaks timing information
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
if (!ok) return res.status(400).send('Bad signature');
const event = JSON.parse(req.body);
console.log(event.type, event.data);
res.sendStatus(200); // ack fast, process asynchronously
});{
"id": "0fac1f7f-1a02-4a99-a7c8-89f8ab17718e",
"type": "stream.started",
"livemode": true,
"created": 1785199740,
"data": {
"field": { "id": "9b804156-...", "name": "Cam 1", "status": "live" },
"event": { "id": "45a45b4e-...", "name": "County Finals", "slug": "county-finals" }
}
}