Webhooks

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

Setting up Webhooks

You can set up your own webhooks 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. This allows you to remove webhooks by passing a null value or empty string. You can leave webhooks as-is by simply not sending a new webhooks property.

See Adding Webhooks for an example.

The webhooks are defined as follows:

{
  "webhooks": {
    "session": {
      "start": "<endpoint-url>",
      "end": "<endpoint-url>",
      "idle": "<endpoint-url>"
    },
    "user": {
      "join": "<endpoint-url>",
      "leave": "<endpoint-url>",
      "idle": "<endpoint-url>"
    },
    "chat": {
      "message": "<endpoint-url>"
    },
    "cobrowser": {
      "start": "<endpoint-url>",
      "stop": "<endpoint-url>"
    },
    "transcription": {
      "finish": "<endpoint-url>"
    },
    "knock": {
      "request": "<endpoint-url>",
      "admit": "<endpoint-url>",
      "deny": "<endpoint-url>"
    },
  }
}

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.

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() in Node.js), using the space secret as the signing key.

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. For Python, an equivalent operation would be json.dumps(x, separators=(',', ':')).

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.

User Data Structure

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

{
  "id": "identifier of the user",
  "guest": "boolean flag indicating whether or not the user joined while signed in",
  "readOnly": "boolean flag indicating whether or not the user was in read only mode",
  "allowInvite": "boolean flag indicating whether or not the user had access to the Invite Others button",
  "meta": "...",
  "page": null,
  "av": "..."
}

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.

All the above user webhooks share the same payload schema:

{
  "session": {
    "id": "internal identifier of the session"
  },
  "room": {
    "id": "internal identifier of the space"
  },
  "socketId": "internal socket id of the user",
  "user": {
    "id": "...",
    "name": "...",
    // etc...
  },
  "nbf": "...",
  "exp": "...",
  "canLead": "boolean flag indicating whether or not the user can lead",
  "traceId": "...",
  "iat": "...",
}

Session Events

webhooks.session.start

Triggered when a space registers its first user join.

Payload Schema

{
  "id": "internal identifier of the session",
  "room": {
    "id": "internal identifier of the space"
  }
}

webhooks.session.idle

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

Payload Schema

{
  "id": "internal identifier of the session",
  "room": {
    "id": "internal identifier of the space",
    "allUsersIdle": true,
    "becameIdleAt": "ISO8601 date string indicating the datetime that the session became idle"
  }
}

webhooks.session.end

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

Payload Schema

{
  "id": "internal identifier of the session",
  "room": {
    "id": "internal identifier of the space",
    "allUsersIdle": "boolean indicating whether or not all users are idle",
    "becameIdleAt": "ISO8601 date string indicating the datetime that the session became idle"
  },
  "summary": {
    "start": "ISO8601 date string indicating start datetime of the session",
    "end": "ISO8601 date string indicating end datetime of the session"
  },
  "reason": {
    "state": "ended",
    "extra": "empty"
  }
}

Chat Events

webhooks.chat.message

Triggered when a chat message is sent.

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

{
  "transcriptionUrl": "<pre-signed S3 URL>"
}

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, Incognito tabs, different browsers or different devices will produce new knock IDs. Multiple 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 cancelling and restarting the request to join, or 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 re-requesting or refreshing the browser.

Payload Schema

{
  "session": {
    "id": "internal identifier of the session",
    "started": "whether or not the session has started"
  },
  "room": {
    "id": "internal identifier of the space"
  },
  "knockId": "UUID to uniquely identify the knock attempt",
  "user": {
    "id": "...",
    "name": "...",
    // etc...
  },
  "sequence": "sequence number"
}

webhooks.knock.admit

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

Payload Schema

{
  "session": {
    "id": "internal identifier of the session"
  },
  "room": {
    "id": "internal identifier of the space"
  },
  "knockId": "UUID to uniquely identify the knock attempt",
  "user": {
    "id": "...",
    "name": "...",
    // etc...
  },
  "leader": {
    "id": "...",
    "name": "...",
    // etc...
  }
}

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

{
  "session": {
    "id": "internal identifier of the session"
  },
  "room": {
    "id": "internal identifier of the space"
  },
  "knockId": "UUID string to uniquely identify the knock attempt",
  "user": {
    "id": "...",
    "name": "...",
    // etc...
  },
  "leader": {
    "id": "...",
    "name": "...",
    // etc...
  },
  "banned": "whether the user was banned at the same time"
}