Webhooks & API Keys
Connect StageTick to your own systems: webhooks push events from your rooms to your server, and API keys let your server read and control a room through the REST API. Webhooks are managed in Organisation Settings, API keys in each room's menu.
Webhooks
A webhook sends an HTTPS POST request to your server when one of the events below happens. A webhook belongs to the organisation and fires for every room in your organisation; there is no per-room filter. Use the roomId in the payload to tell rooms apart.
Supported events
| Event | When it is sent |
|---|---|
room.on_air | When someone switches the room On Air — the button that takes audience screens out of Standby. Independent of play and pause. |
room.off_air | When the room is switched off air, so audience screens show Standby again. Independent of play and pause. |
timer.started | Whenever a timer starts running, including resuming after a pause, and for the new timer each time the room moves on to another timer while the clock is running (for example on auto-advance). |
timer.finished | Once when the running timer reaches 0:00 (its duration), also when it then continues into overtime. Not sent when you switch timers, pause or stop before 0:00, and never for time-of-day timers, countdown-to-time (T-minus) timers or timers without a duration. |
The transport commands of the REST API behave like the app: next, prev and jump keep the playback state, so on a running clock they send timer.started for the new timer. With the body { "keepRunning": false } the clock stops on the new timer and no event is sent.
Payload
Requests have Content-Type: application/json and a body like this:
{
"event": "timer.started",
"roomId": "AbC123roomId",
"timestamp": "2026-10-12T09:30:00.000Z",
"data": {
"timerId": "t9XyZ",
"title": "Keynote",
"speaker": "Jane Smith"
}
}
For room.on_air and room.off_air, data is an empty object. The timestamp is set by StageTick when it processes the change.
Verifying signatures
Every request carries the header X-StageTick-Signature: sha256=…: an HMAC-SHA256 of the raw request body, keyed with the webhook's signing secret and written as hex. Use the secret exactly as it was shown (as text). Compute the same value over the raw body before you parse the JSON, and compare in constant time — for example in Node.js:
import crypto from 'node:crypto';
// rawBody: the request body exactly as received (string or Buffer), before JSON parsing
function isFromStageTick(rawBody, signatureHeader, secret) {
const expected =
'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader ?? '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
} Endpoint requirements and retries
- The URL must start with
https://and be reachable from the internet. Addresses in private or local networks (such as localhost, 10.x.x.x or 192.168.x.x) are rejected, and redirects are not followed. - Answer within 10 seconds with a 2xx status.
- If the request fails, times out, is redirected or your server answers with a 5xx status, StageTick tries again: up to three attempts a few seconds apart, and this round is repeated up to two more times within the next minutes. A 4xx answer counts as delivered and is not retried. If your server's host name does not resolve when an event is sent, that event is dropped without a retry.
- Because of retries, the same event can arrive more than once, and events are not guaranteed to arrive in order. Use
event,roomId,data.timerId(timer events) andtimestampto recognise duplicates. This is best effort: in rare cases StageTick processes the same change twice and sends it again with a new timestamp. - If the app shows "Failed to create webhook. Please try again.", check the URL first: a URL that is not allowed produces the same message.
Managing webhooks
Only organisation owners and admins can see, add and delete webhooks; members see a short note instead. On the Free plan, the section links the owner to the billing section on the same page.
- As the owner or an admin, open Organisation Settings — from the account menu on the dashboard or from the room menu (your avatar) in a room.
- Scroll to the section API and find Webhooks.
- Enter the Endpoint URL and tick the events: Room On Air, Room Off Air, Timer Started, Timer Finished.
- Click Add Webhook (it becomes active once a URL and at least one event are set). The signing secret is shown once — copy it now.
- To delete a webhook, click its trash icon and then Confirm?.
Webhook limits
- Pro: up to 5 webhooks per organisation
- Enterprise: up to 20 webhooks per organisation
API Keys
An API key gives your server access to one room through the REST API. Each key belongs to a single room and has a scope.
Scopes
- read — read requests only (room, timers, transport state, messages, questions and the event stream). Any change answers with
403 READ_ONLY_KEY. New keys start with this scope. - readwrite — read and change room data and control the transport.
Managing API keys
Organisation owners and admins can create, view and revoke API keys for every room. Members can do so only for rooms they created, and only while they belong to the organisation.
- Open the room in the Controller and open the room menu (your avatar, top right).
- Select API Keys.
- Enter a Key name (required), choose the Scope (read or readwrite) and click Create.
- The key, starting with
stk_, is shown once. Copy it and store it securely. - To revoke a key, click Revoke next to it and then Confirm? within three seconds. Revoked keys stop working immediately.
API key limits
- Pro: up to 5 API keys per room
- Enterprise: up to 20 API keys per room
- Per organisation: up to 10 API keys on Pro and up to 50 on Enterprise
REST API
The REST API is made for server-to-server integrations. Call it from your server: browsers block requests from other websites (CORS), and an API key in web page code would be visible to everyone.
Base URL and authentication
All paths below start with https://europe-west1-stagetick-e0911.cloudfunctions.net/api/v1. The app shows this base URL under room menu → API Keys, with a copy button and the link API reference. Send the key in the Authorization: Bearer stk_… header:
curl -H "Authorization: Bearer stk_…" \
https://europe-west1-stagetick-e0911.cloudfunctions.net/api/v1/rooms/ROOM_ID/transport
The room ID is the part after /r/ in a room link. Requests and responses are JSON. List endpoints return up to 50 items (?limit= up to 100); use the nextCursor from the response as ?startAfter= for the next page.
The full reference (Swagger) is at https://europe-west1-stagetick-e0911.cloudfunctions.net/api/v1/docs/, the OpenAPI spec at https://europe-west1-stagetick-e0911.cloudfunctions.net/api/v1/docs/openapi.yaml. Both are public and need no key.
Endpoints
| Request | What it does |
|---|---|
GET /rooms/:roomId | Room details and the current transport state |
GET /rooms/:roomId/timersGET /rooms/:roomId/timers/:timerId | List timers or read one, each with remainingSeconds |
POST /rooms/:roomId/timers | Create a timer; duration in seconds is required |
PATCH /rooms/:roomId/timers/:timerIdDELETE /rooms/:roomId/timers/:timerId | Change or delete a timer. Fields: title, speaker, duration, wrapUpTime, dangerTime, trigger, appearance, labelColor, notes, order, targetTime (seconds after midnight), flashOnPhase, soundOnPhase |
GET /rooms/:roomId/transport | Transport state: active timer, running/paused/stopped, elapsed time, On Air, blackout |
POST /rooms/:roomId/transport/play…/pause, …/stop | Start or resume, pause, stop the active timer. If no timer is active, play starts the first timer. |
POST /rooms/:roomId/transport/next…/prev, …/jump | Select the next or previous timer, or the timer given as timerId in the body (jump). The playback state stays: a running clock keeps running on the new timer. Send { "keepRunning": false } to stop the clock on the switch. |
POST /rooms/:roomId/signals/flash…/blackout, …/on-air | Flash the screens; switch blackout or On Air. Without a body these toggle; send enabled as true or false to set them. |
GET / POST /rooms/:roomId/messagesPATCH / DELETE …/messages/:messageId | List, create, change or delete cue messages. Fields: text (required), color (green, yellow, red, blue, white), bold, uppercase |
POST …/messages/:messageId/show…/hide | Show a message (any other visible message is hidden) or hide it |
GET /rooms/:roomId/questionsPOST …/questions/:questionId/show, …/hideDELETE …/questions/:questionId | List audience questions (newest first), put one live, take it off or delete it. Questions cannot be submitted through the API. |
GET /rooms/:roomId/events | Live event stream (see below) |
Keys are valid for exactly one room. GET /rooms returns the room of the key.
Live event stream (SSE)
GET /rooms/:roomId/events keeps the connection open and sends Server-Sent Events. It starts with connected and the current state (transport, one timer:create per timer, message:update and question:update per item), then every change: transport, timer:create, timer:update, timer:delete, message:update, message:delete, question:update, question:delete, plus a heartbeat every 30 seconds.
StageTick closes each stream after 5 minutes. Reconnect and send the ID of the last event you received in the Last-Event-ID header: you get reconnected and the current state again; changes in between are not replayed one by one. Use an SSE client on your server that can send the Authorization header (the browser's EventSource cannot).
Limits and errors
- Up to 120 requests per minute per organisation. Above that the API answers
429 RATE_LIMITED with a Retry-After header. - Open event streams per organisation: 25 on Pro, 100 on Enterprise. Above that:
429 TOO_MANY_CONNECTIONS with Retry-After: 60. If an organisation opens many streams at the same moment, the API can answer 429 CONNECTION_SLOTS_BUSY with Retry-After: 5. 401 UNAUTHORIZED — the key is missing, malformed or revoked. 403 PLAN_REQUIRED — the organisation is on the Free plan, or a payment is past due. 403 READ_ONLY_KEY — a read key tried to change something. 403 FORBIDDEN — the key belongs to another room. 400 BAD_REQUEST and 404 (for example TIMER_NOT_FOUND) — invalid input or an unknown ID.
Errors come back as JSON with error.code and error.message, also for a path that does not exist (404 NOT_FOUND). The previous base URL ending in /api/api/v1 keeps working.