Skip to content

API

Webhooks

Be told when something happens — a tool subscribes by making one.

A webhook is an address of yours that is told when an event happens in the organization: a POST, with what is said of the event as its body, with the organization’s webhook token and signed with it (see Organization and Check that it is ours). One event per webhook. A tool subscribes by making one and unsubscribes by deleting it; the same webhooks are on the dashboard, under Webhooks, with how each sending went.

trigger When
webinarRegistration.created Somebody registered for a webinar, whichever way. Once per person, with a launch’s sessions in a list.
surveyResponse.created A registration sent a response to a survey.

An evergreen has no sessions: the time picked is on the registration’s webinarEvergreenRegistration. A launch’s sessions come in a list, as they stood when it was sent. A date added later says nothing to anyone already registered, unless something of yours does it.

What a webhook is for is said in what its event is of. For the two webinar events, webinars: allWebinars is every webinar, the ones made later too, else the ones in webinarIds. For surveyResponse.created, surveys the same way: allSurveys or surveyIds. The ones picked have to be yours and not deleted (404 WEBINAR_NOT_FOUND, 404 SURVEY_NOT_FOUND).

active says whether events reach it. An active webhook needs all of these, and is refused with what it lacks:

It lacks Refused with
A name 409 ORGANIZATION_WEBHOOK_NAME_MISSING
A trigger 409 ORGANIZATION_WEBHOOK_TRIGGER_MISSING
Webinars, for either webinar event 409 ORGANIZATION_WEBHOOK_WEBINARS_MISSING
Surveys, for surveyResponse.created 409 ORGANIZATION_WEBHOOK_SURVEYS_MISSING

An inactive webhook needs only its name and its url: trigger may be null and it may be for nothing.

While inactive, an event makes no delivery, then or later. A delivery made while it was active still goes out, its retries too, and a failed one can still be tried again from the dashboard. A test can be sent from the dashboard while inactive, if it has an event.

Deleting a webinar or a survey takes it out of webinarIds or surveyIds on every webhook that has it, and restoring it does not put it back. A webhook it was all that was left for becomes inactive, with nothing picked, and the organization’s owners and managers are told; one with others left stays active with those, and one for every webinar or survey goes on as it was.

POST/public/v1/webhooks
{
"name": "CRM sign-ups",
"active": true,
"trigger": "webinarRegistration.created",
"allWebinars": true,
"webinarIds": [],
"allSurveys": false,
"surveyIds": [],
"url": "https://hooks.example.com/sparkwebinar"
}

name and url are always sent. The rest may be left out: active starts true, trigger null, and what it is for empty, so what is left out has to fit its state. url is https (400 otherwise); an address that resolves to a private network, or on a port other than 80, 443, 8080 or 8443, is refused as it is sent, and the delivery says so.

GET/public/v1/webhooks
GET/public/v1/webhooks/:organizationWebhookId
PATCH/public/v1/webhooks/:organizationWebhookId
DELETE/public/v1/webhooks/:organizationWebhookId

A change sends whichever of the fields changed, "trigger": null to clear the event, and what the webhook comes to has to fit its state. A webhook deleted is told nothing more; sendings already on their way are not recalled.

Every sending is a POST to the webhook’s url, content-type: application/json, with the token and the signature in its headers — check either (below), and answer any 2xx. The body opens with the delivery’s own id — the same on every attempt at it, so a request received twice is known to be one — createdAt, the instant the event happened, and test, true on one sent from the dashboard to see it arrive; then the event:

{
"id": 318,
"createdAt": "2026-10-26T15:26:15.600Z",
"test": false,
"event": "webinarRegistration.created",
"webinarRegistration": {
"id": 109,
"webinarId": 2,
"email": "ana@example.com",
"webinarLaunchSessionRegistrations": [],
"...": "..."
},
"webinarLaunchSessions": [{ "id": 412, "startsAt": "2026-10-26T22:00:00.000Z", "status": "open" }],
"webinar": { "id": 2, "name": "Growth webinar", "title": "How we grow", "scheduleMethod": "launch", "webinarLaunch": { "method": "recorded" } }
}

webinarRegistration is the registration, with its access token and where it sits, webinarEvergreenRegistration or webinarLaunchSessionRegistrations, without what it did there.

On a launch’s webinarRegistration.created, top-level webinarLaunchSessions lists its sessions as they stood when it was sent, soonest first, each with id, startsAt and status (open, canceled or closed), leaving out the ones called off. An open session is on from startsAt. It is absent on any other event.

Inside webinarRegistration, webinarLaunchSessionRegistrations lists only the sessions the person has entered, so it is empty as they register. An evergreen’s time is webinarRegistration.webinarEvergreenRegistration.startsAt.

webinar is its id, name, title and scheduleMethod (launch or evergreen), and on a launch webinarLaunch.method (live or recorded). On surveyResponse.created there are also survey, with id and name, and answers, each with fieldId, value and the question’s title.

Not answered with a 2xx, a delivery is tried again a minute later, then after five minutes, half an hour, two hours and twelve — six attempts in all, over about fourteen hours — and then given up on.

Each request carries:

Header What it is
x-webhook-token The organization’s webhookToken, as it is.
x-webhook-timestamp When it was sent, in Unix seconds. Each attempt is signed anew, at its own instant.
x-webhook-signature sha256= and the hex HMAC-SHA256 of <timestamp>.<body>, keyed with the organization’s webhookToken.

The simplest check is x-webhook-token equal to your webhookToken, which any tool can do with no code. The signature is the stronger one: it also says the body was not altered and that the request was sent just now.

The body is the raw bytes received, before any JSON parsing. Refuse a request whose signature does not match, and one whose timestamp is more than five minutes away from your clock: a request recorded and sent again later is then worth nothing. A delivery’s id is the same on every attempt, so one received twice is told apart.

In Node:

import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody: the request's body as received, a string or a Buffer.
function isOurs(rawBody, headers, webhookToken) {
const timestamp = Number(headers['x-webhook-timestamp']);
if (!Number.isInteger(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = 'sha256=' + createHmac('sha256', webhookToken).update(`${timestamp}.${rawBody}`).digest('hex');
const received = String(headers['x-webhook-signature'] ?? '');
return received.length === expected.length && timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}

Once the token is made anew, on the dashboard, every request from then on is signed with the new one, retries of older deliveries included.

GET/public/v1/webhooks/:organizationWebhookId/deliveries

One row per event sent to this webhook, newest first: the body as it was sent — but for the registration’s accessToken, shown as •••••• —, attempts, the last attempt’s instant and status — or lastError: ADDRESS_NOT_ALLOWED, REQUEST_TIMED_OUT, REQUEST_FAILED —, nextAttemptAt while one is due, and its status: pending while attempts go on, then delivered or failed, with deliveredAt or failedAt.

One that ended is deleted 15 days after that instant, so the list and its total go back no further, and the dashboard can no longer try a failed one again. One pending is never deleted this way.

© 2026 Sparkwebinar. All rights reserved.