Webhooks

The Lessonspace API provides various webhooks you can use to get information about Spaces in real-time.

Setting up Webhooks

Webhooks can be set in two places: organisation defaults, which apply to all of your Spaces, and Space overrides, which apply to a single Space.

Changes to webhooks may not apply to sessions that are already in progress. To be sure the change applies, make it before a session starts or handle both old and new endpoints for a period.

Organisation Defaults

Configure one default webhook URL per event in Developer Settings. An organisation default is only used when a Space has no webhook URL of its own for that event. If a Space has its own URL for an event, that event is sent only to the Space's URL and never to the default. This applies per event, so a Space with its own session.end URL still uses your defaults for every other event.

Clearing a default removes it without changing Space-specific webhooks.

A default URL receives webhooks from all of your Spaces, and each one is signed with the secret of the Space it came from (see Security). Use the payload's room.id to look up which Space's secret to verify it with.

Space Overrides

You can set webhooks for a single Space by specifying them in the body of your POST request to the Launch endpoint. Any webhooks sent on subsequent calls for the same Space (as identified by the id) will fully overwrite existing ones; they will not be merged in. Any event without a URL, including one set to null or an empty string, is cleared and falls back to organisation default. You can leave webhooks as-is by simply not sending a new webhooks property, or by sending an empty object ("webhooks": {}).

See Adding Webhooks for an example.

The webhooks are defined as follows:

// An HTTP(S) endpoint URL; HTTPS recommended. null or "" clears the Space's URL for that event.
type WebhookUrl = string | null

// The `webhooks` property of the Launch request body
interface Webhooks {
  session?: {
    // the first user joins
    start?: WebhookUrl
    // all users become idle
    idle?: WebhookUrl
    // the session ends
    end?: WebhookUrl
    // the session's recording is ready
    playback?: WebhookUrl
    // the session's recording is deleted
    deleted?: WebhookUrl
  }
  user?: {
    join?: WebhookUrl
    idle?: WebhookUrl
    leave?: WebhookUrl
  }
  cobrowser?: {
    start?: WebhookUrl
    stop?: WebhookUrl
  }
  transcription?: {
    // see Transcriptions
    finish?: WebhookUrl
  }
  summary?: {
    // see Summaries
    finish?: WebhookUrl
  }
  knock?: {
    request?: WebhookUrl
    admit?: WebhookUrl
    deny?: WebhookUrl
  }
}

Example

{
  "id": "biology101_2026",
  "webhooks": {
    "session": {
      "start": "https://example.com/webhooks/session-start",
      "end": "https://example.com/webhooks/session-end"
    },
    "user": {
      "join": "https://example.com/webhooks/user-join",
      "leave": "https://example.com/webhooks/user-leave"
    }
  }
}

Switching a Space to the Defaults

To make an existing Space use your organisation defaults, call the Launch endpoint for that Space again with "webhooks": null. This clears all of the Space's own webhooks, so every event falls back to your organisation default:

{
  "id": "biology101_2026",
  "webhooks": null
}

Because webhooks fully overwrites the Space's existing webhooks, you can also keep some of the Space's own URLs by including only those events; all others are cleared and use your defaults.

Retries & Ordering

If your server returns a status code outside the 200 - 299 range, or takes longer than 30 seconds (including connection and response time), the webhook delivery will be considered a failure and retries will begin.

The retry algorithm uses an exponential backoff, and will attempt to deliver a payload up to 5 times. In order to not count retry delay in statistics, you can use the x-webhook-timestamp header, described below.

Events are guaranteed to be delivered in-order on a per-space basis and per-URL basis, with two exceptions:

  1. The very first user.join hook can rarely be delivered before the session.start hook. This is due to both hooks firing at basically exactly the same time. The timing difference will be absolutely miniscule, usually a matter of a single millisecond, and whether it happens depends on system load at the precise time it occurs.
  2. In the case of all retries failing for a given hook, that hook will be permanently discarded and subsequent events will begin delivering. This means some hooks can arrive "before" others, but in this case the others will never arrive anyway.

Headers

Four custom headers are included with each delivery. These are:

  • x-webhook-id Uniquely identifies the webhook payload. Retries use the same value.
  • x-webhook-event The type of event being dispatched.
  • x-webhook-timestamp An ISO8601 timestamp specifying when the webhook event took place.
  • x-webhook-signature A cryptographic signature for security and authentication, signed with the Space's secret.

Security

The x-webhook-signature header allows you to both authenticate that the request originates with Lessonspace (as only you and Lessonspace know the secret key) and also verify that the payload has not been modified. The signature is calculated as an HMAC (using the sha256 algorithm) of the webhook payload as stringified JSON (generated with JSON.stringify(x) in Node.js), using the secret of the Space the event came from (the secret returned by the Launch endpoint) as the signing key. Webhooks sent to organisation default URLs are signed the same way; there is no separate organisation secret.

If the payload is stringified using a similar operation in another language, such as json.dumps in Python, note that the semantics of JSON.stringify mean that all unimportant whitespace is automatically removed from the final string, and that UTF-16 characters, such as emoji, are kept as a single character instead of being decomposed to individual bytes. For Python, an equivalent operation would be json.dumps(x, separators=(',', ':'), ensure_ascii=False).

It is not sufficient to simply copy-paste the payload and quote it to create a string.

The space secret is only exposed in the response when making a request to the Launch API.

Common Identifiers

Every payload identifies its Space and session with these values:

  • room.id The Space's ID (a UUID), matching the room_id returned by the Launch endpoint.
  • session.id (the top-level id on session.start, session.idle and session.end) The session's ID (a UUID), matching the session_id returned by the Launch endpoint for the current or upcoming session.

User Data Structure

Whenever a user object is included in a payload, it will have this structure:

interface WebhookUser {
  // identifier of the user
  id: number | string
  // whether the user joined as a guest, without signing in
  guest: boolean
  // whether the user was in read-only mode
  readOnly: boolean
  // whether the user had access to the Invite Others button
  allowInvite: boolean
  // display details, such as the user's name
  meta: object
}

Example

{
  "id": 48213,
  "guest": false,
  "readOnly": false,
  "allowInvite": false,
  "meta": { "name": "Jane Doe" }
}

Events

User Events

webhooks.user.join

Triggered when a user joins a Space. This happens when the user opens their web browser to the Space URL and joins the lobby.

webhooks.user.idle

Triggered when a user is considered idle in a space. See Idle under Terms & Concepts for more information.

webhooks.user.leave

Triggered when a user leaves a space. This can occur when they close their browser tab, click the End Session button, or are otherwise disconnected.

webhooks.user.join and webhooks.user.leave share the same payload schema. webhooks.user.idle adds becameIdleAt:

Payload Schema

// webhooks.user.join and webhooks.user.leave
interface UserEventPayload {
  // session ID
  session: { id: string }
  // Space ID
  room: { id: string }
  // identifies this connection; a user with several tabs open has several
  socketId: string
  // see User Data Structure
  user: WebhookUser
}

// webhooks.user.idle
interface UserIdlePayload extends UserEventPayload {
  // ISO 8601 time the user became idle
  becameIdleAt: string
}

Example (webhooks.user.join)

{
  "session": { "id": "b27e6d4a-3f1c-4e8b-9a5d-6c2f8e1b7a39" },
  "room": { "id": "4f8c2a1e-9b3d-4c6a-8e2f-1a7b5d3c9e04" },
  "socketId": "e91a3c7f-5b2d-4f6e-8c1a-3d9b7e2f4a68",
  "user": {
    "id": 48213,
    "guest": false,
    "readOnly": false,
    "allowInvite": false,
    "meta": { "name": "Jane Doe" }
  }
}

Session Events

webhooks.session.start

Triggered when a space registers its first user join.

Payload Schema

interface SessionStartPayload {
  // session ID
  id: string
  // Space ID
  room: { id: string }
}

Example

{
  "id": "b27e6d4a-3f1c-4e8b-9a5d-6c2f8e1b7a39",
  "room": { "id": "4f8c2a1e-9b3d-4c6a-8e2f-1a7b5d3c9e04" }
}

webhooks.session.idle

Triggered when all the users in a space are considered idle.

Payload Schema

interface SessionIdlePayload {
  // session ID
  id: string
  room: {
    // Space ID
    id: string
    allUsersIdle: true
    // ISO 8601 time all users became idle
    becameIdleAt: string
  }
}

Example

{
  "id": "b27e6d4a-3f1c-4e8b-9a5d-6c2f8e1b7a39",
  "room": {
    "id": "4f8c2a1e-9b3d-4c6a-8e2f-1a7b5d3c9e04",
    "allUsersIdle": true,
    "becameIdleAt": "2026-10-22T09:41:12.508Z"
  }
}

webhooks.session.end

Triggered when a space registers its last user leave, or timeouts have been reached.

Payload Schema

interface SessionEndPayload {
  // session ID
  id: string
  room: {
    // Space ID
    id: string
    // whether all users were idle when the session ended
    allUsersIdle: boolean
    // ISO 8601 time all users became idle; omitted if they weren't
    becameIdleAt?: string
  }
  summary: {
    // ISO 8601 time the session started
    start: string
    // ISO 8601 time the session ended
    end: string
  }
  reason: {
    // "ended" normally, or "unload-all" when the server shuts down
    state: string
    // Why the session ended. New values may be added, so handle unknown ones.
    //   "empty":      all users left and the empty-session timeout passed
    //   "inactivity": all users were idle and the idle-session timeout passed
    //   "leader":     a leader ended the session
    //   "leaderless": a webinar Space had no leader for 20 minutes
    extra: string | null
  }
}

Example

{
  "id": "b27e6d4a-3f1c-4e8b-9a5d-6c2f8e1b7a39",
  "room": {
    "id": "4f8c2a1e-9b3d-4c6a-8e2f-1a7b5d3c9e04",
    "allUsersIdle": false
  },
  "summary": {
    "start": "2026-10-22T09:00:03.117Z",
    "end": "2026-10-22T10:02:45.890Z"
  },
  "reason": {
    "state": "ended",
    "extra": "empty"
  }
}

webhooks.session.playback

Triggered when the session's content recording is ready and all AV has finished processing (if AV recording was also enabled). NOTE: Only sessions with content recording enabled produce this event.

Payload Schema

interface SessionPlaybackPayload {
  // Space ID
  room: { id: string }
  // session ID
  session: { id: string }
}

Example

{
  "room": { "id": "4f8c2a1e-9b3d-4c6a-8e2f-1a7b5d3c9e04" },
  "session": { "id": "b27e6d4a-3f1c-4e8b-9a5d-6c2f8e1b7a39" }
}

webhooks.session.deleted

Triggered after a session's recording data is deleted manually. Deletions by the storage retention policy don't trigger it.

userId is the internal Lessonspace user ID of the person who deleted the recording. It is null when an organisation API key or an operational tool deletes the recording without an identified user.

Payload Schema

interface SessionDeletedPayload {
  // Space ID
  room: { id: string }
  // session ID
  session: { id: string }
  // Lessonspace user ID of whoever deleted the recording, or null (see above)
  userId: number | null
}

Example

{
  "room": { "id": "4f8c2a1e-9b3d-4c6a-8e2f-1a7b5d3c9e04" },
  "session": { "id": "b27e6d4a-3f1c-4e8b-9a5d-6c2f8e1b7a39" },
  "userId": 48213
}

Cobrowser Events

webhooks.cobrowser.start

Triggered when a Cobrowser activity starts in a session.

webhooks.cobrowser.stop

Triggered when a Cobrowser activity stops in a session.

Transcription Events

webhooks.transcription.finish

Triggered when a session finishes being transcribed.

Payload Schema

interface TranscriptionFinishPayload {
  // Space ID
  room: { id: string }
  // session ID
  session: { id: string }
  // pre-signed URL to download the transcript JSON
  transcriptionUrl: string
}

Example

{
  "room": { "id": "4f8c2a1e-9b3d-4c6a-8e2f-1a7b5d3c9e04" },
  "session": { "id": "b27e6d4a-3f1c-4e8b-9a5d-6c2f8e1b7a39" },
  "transcriptionUrl": "https://example-bucket.s3.amazonaws.com/transcriptions/4f8c2a1e-9b3d-4c6a-8e2f-1a7b5d3c9e04/b27e6d4a-3f1c-4e8b-9a5d-6c2f8e1b7a39/transcription.json?X-Amz-Signature=..."
}

Waiting Room Events

webhooks.knock.request

Triggered when a user knocks on the waiting room for the first time, and at approximately 5 minute intervals thereafter, until the request is cancelled by the user or the user is admitted.

Unlike other webhooks, this hook can be fired before a session has started (since joining the waiting room does not count as joining the session). You can use the started field on the session object to tell whether the session has started. If a session is started and then ended without the user being accepted, the session.id will change to reflect the next upcoming session ID.

The knockId is generated when a user first joins the waiting room. Knock IDs will persist in local storage: using Incognito tabs, different browsers or different devices will produce new knock IDs. Multiple separate knock IDs for the same user can indicate they're trying to join from multiple devices concurrently.

sequence is a monotonic counter that can be used to infer user behaviour. This counter resets whenever the user restarts the admission process, usually by refreshing or reopening the browser. This does not affect the logic for whether or not the user gets admitted to the room, but it does indicate if a user was taking actions like constantly refreshing the browser.

Payload Schema

interface KnockRequestPayload {
  session: {
    // current session ID, or the upcoming one if no session has started
    id: string
    // whether a session is in progress
    started: boolean
  }
  // Space ID
  room: { id: string }
  // UUID identifying this knock attempt
  knockId: string
  // the user waiting to be admitted
  user: WebhookUser
  // 1 for the first knock, then 2, 3, ... for each ~5 minute repeat
  sequence: number
}

Example

{
  "session": {
    "id": "b27e6d4a-3f1c-4e8b-9a5d-6c2f8e1b7a39",
    "started": true
  },
  "room": { "id": "4f8c2a1e-9b3d-4c6a-8e2f-1a7b5d3c9e04" },
  "knockId": "7c3e9a1b-2d4f-4a6c-8e5b-9f1d3a7c2e46",
  "user": {
    "id": 51877,
    "guest": false,
    "readOnly": false,
    "allowInvite": false,
    "meta": { "name": "Sam Lee" }
  },
  "sequence": 1
}

webhooks.knock.admit

Triggered when a user is admitted to the waiting room. leader is the user who admitted them.

Payload Schema

interface KnockAdmitPayload {
  // session ID
  session: { id: string }
  // Space ID
  room: { id: string }
  // UUID identifying the knock attempt
  knockId: string
  // the user who was admitted
  user: WebhookUser
  // the leader who admitted them
  leader: WebhookUser
}

Example

{
  "session": { "id": "b27e6d4a-3f1c-4e8b-9a5d-6c2f8e1b7a39" },
  "room": { "id": "4f8c2a1e-9b3d-4c6a-8e2f-1a7b5d3c9e04" },
  "knockId": "7c3e9a1b-2d4f-4a6c-8e5b-9f1d3a7c2e46",
  "user": {
    "id": 51877,
    "guest": false,
    "readOnly": false,
    "allowInvite": false,
    "meta": { "name": "Sam Lee" }
  },
  "leader": {
    "id": 48213,
    "guest": false,
    "readOnly": false,
    "allowInvite": false,
    "meta": { "name": "Jane Doe" }
  }
}

webhooks.knock.deny

Triggered when a user is rejected from the waiting room. leader is the person who denied them. banned is whether they were also banned from retrying.

Payload Schema

interface KnockDenyPayload {
  // session ID
  session: { id: string }
  // Space ID
  room: { id: string }
  // UUID identifying the knock attempt
  knockId: string
  // the user who was denied
  user: WebhookUser
  // the leader who denied them
  leader: WebhookUser
  // whether the user was also banned from knocking again
  banned: boolean
}

Example

{
  "session": { "id": "b27e6d4a-3f1c-4e8b-9a5d-6c2f8e1b7a39" },
  "room": { "id": "4f8c2a1e-9b3d-4c6a-8e2f-1a7b5d3c9e04" },
  "knockId": "7c3e9a1b-2d4f-4a6c-8e5b-9f1d3a7c2e46",
  "user": {
    "id": 51877,
    "guest": false,
    "readOnly": false,
    "allowInvite": false,
    "meta": { "name": "Sam Lee" }
  },
  "leader": {
    "id": 48213,
    "guest": false,
    "readOnly": false,
    "allowInvite": false,
    "meta": { "name": "Jane Doe" }
  },
  "banned": false
}