Webhooks
Get an HTTPS POST the moment something happens in your school — a sale, an enrollment, a completed lesson — instead of polling the API.
Webhooks need the Professional plan. Events from workspaces on other plans are not sent. Compare plans.
Register an endpoint
- Sign in at app.zinstruct.com as an owner or admin and open Settings → Webhooks.
- Click Open webhook portal. The portal is where you add an endpoint URL, choose which events it receives, copy its signing secret, browse delivery logs and replay a failed delivery.
- Give each receiving system its own endpoint, so each gets its own secret and its own log.
The payload
Every event arrives as a JSON POST in the same envelope. api_version is currently 2026-05-24.
| Field | Type | Description |
|---|
| id | string | The event id. Stable across retries and replays — use it to ignore a delivery you have already handled. |
| type | string | The event type, e.g. `transaction.succeeded`. |
| api_version | string | The payload contract version. Currently `2026-05-24`. |
| created_at | string (date-time) | When the event happened. ISO-8601, UTC. |
| data | object | The event’s fields, listed per event below. Always includes `tenant_id`. |
{
"id": "0b6f3a52-6a4e-4d0e-9a4c-2f1f8f0d6a11",
"type": "enrollment.created",
"api_version": "2026-05-24",
"created_at": "2026-01-15T09:30:00.000Z",
"data": {
"tenant_id": "8a1c2e34-5b6d-4f70-8a9b-0c1d2e3f4a5b",
"enrollment_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f",
"product_id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f7a",
"contact_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a8b",
"enrolled_at": "2026-01-15T09:30:00.000Z"
}
}
Verify the signature
Deliveries are signed by Svix, which runs our webhook delivery. Each request carries three headers: svix-id, svix-timestamp and svix-signature. Check them against the endpoint's signing secret (it starts with whsec_) before trusting the body. The simplest way is Svix's open-source library, available for most languages:
import { Webhook } from 'svix' // npm install svix
const wh = new Webhook(process.env.WEBHOOK_SECRET) // whsec_… from the webhook portal
// Verify the RAW request body — re-serialised JSON will not match the signature.
export function handle(rawBody, headers) {
const event = wh.verify(rawBody, {
'svix-id': headers['svix-id'],
'svix-timestamp': headers['svix-timestamp'],
'svix-signature': headers['svix-signature'],
}) // throws if the signature or timestamp is wrong
return event // { id, type, api_version, created_at, data }
}
Or verify by hand — an HMAC-SHA256 over {svix-id}.{svix-timestamp}.{raw body}:
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verify(rawBody, headers, secret) {
const id = headers['svix-id']
const timestamp = headers['svix-timestamp']
// Reject stale deliveries (here: older or newer than 5 minutes).
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64')
const expected = createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest()
// The header can hold several space-separated signatures: "v1,<base64> v1,<base64>"
return headers['svix-signature'].split(' ').some((part) => {
const [version, sig] = part.split(',')
const given = Buffer.from(sig ?? '', 'base64')
return version === 'v1' && given.length === expected.length && timingSafeEqual(given, expected)
})
}
Svix documents the scheme in full, with libraries for other languages, in Verifying webhooks.
Delivery
- Answer with any
2xx status to acknowledge a delivery. - A failed delivery is retried automatically. Every attempt is in the portal's delivery log, and you can replay a delivery from there.
- Delivery is at least once: the same event can arrive more than once. Its
id never changes between attempts, so record the ids you have handled and skip repeats. - Events can arrive out of order. Use
created_at when order matters.
Events
100 event types. Each lists the fields of data. solid fields are always present; dashed fields are sent only by some of the actions that raise the event.
Contacts & forms
tag.added
A tag was added to a contact.
tenant_idcontact_idtag_id
tag.removed
A tag was removed from a contact.
tenant_idcontact_idtag_id
Events
event.registered
A contact registered for an event.
tenant_idevent_idcontact_idregistration_idoccurrence_iddedupe
event.deregistered
A contact’s event registration was cancelled.
tenant_idevent_idcontact_idregistration_iddedupe
Learning & certificates
enrollment.created
A contact was enrolled in a product.
tenant_idenrollment_idproduct_idcontact_idenrolled_at
enrollment.revoked
A contact’s access to a product was revoked.
tenant_idproduct_idcontact_id
lesson.completed
A member completed a lesson.
tenant_idenrollment_idlesson_idproduct_idcontact_id
course.completed
A member completed a course.
tenant_idenrollment_idproduct_idcontact_id
certificate.issued
A completion certificate was issued.
tenant_idenrollment_idcertificate_idserialproduct_idcontact_id
lesson.unlocked
A dripped lesson unlocked for a member.
tenant_idenrollment_idproduct_idcontact_idlesson_idunlocked_at
Commerce
offer.created
An offer was created.
tenant_idid
offer.updated
An offer changed.
tenant_idid
checkout.started
A buyer started a checkout (an abandoned-cart record was opened).
tenant_idabandoned_idoffer_id
offer.free_claimed
A contact claimed a free offer.
tenant_idoffer_idcontact_id
offer.bundle_product_added
A product was added to an offer’s bundle; existing owners are granted it.
tenant_idoffer_idproduct_id
transaction.succeeded
A payment succeeded.
tenant_idtransaction_idoffer_idcontact_idamount_cents
transaction.refunded
A payment was refunded, in full or in part (`partial`).
tenant_idtransaction_idcontact_idproduct_idpartialamount_centsrefunded_centscurrency
subscription.created
A subscription started.
tenant_idcontact_idproduct_idoffer_idstatusstripe_subscription_id
subscription.updated
A subscription changed status.
tenant_idcontact_idproduct_idoffer_idstatus
subscription.canceled
A subscription ended.
tenant_idcontact_idproduct_idoffer_idstatus
subscription.cancel_requested
A subscription was scheduled to cancel at the end of its period.
tenant_idcontact_idproduct_idoffer_idsubscription_idsourcecancel_session_idreason_idreason_labelfeedbackcurrent_period_end
subscription.saved
A member accepted a save offer instead of cancelling.
tenant_idcontact_idproduct_idoffer_idsubscription_idcancel_session_idaccepted_offer_idcoupon_iddiscount_summary
payment.failed
A subscription payment failed.
tenant_idcontact_idproduct_idstripe_subscription_id
charge.disputed
A buyer disputed a charge.
tenant_idtransaction_idcontact_idproduct_idoutcome
Products & content
product.created
A product was created (or duplicated — see `duplicated_from`).
tenant_ididduplicated_from
product.updated
A product’s settings changed.
tenant_ididaction
product.unpublished
A product was unpublished.
tenant_idid
product.deleted
A product was deleted.
tenant_idid
module.created
A course module was created.
tenant_ididproduct_id
module.updated
A course module changed, or modules were reordered.
tenant_ididactionproduct_id
module.deleted
A course module was deleted.
tenant_idid
lesson.created
A lesson was created.
tenant_ididmodule_id
lesson.updated
A lesson’s content or settings changed, or lessons were moved or reordered.
tenant_ididmodule_idaction
lesson.deleted
A lesson was deleted.
tenant_idid
lesson.resource.added
A downloadable resource was attached to a lesson.
tenant_ididlesson_id
lesson.resource.removed
A resource was removed from a lesson.
tenant_ididlesson_id
download.file.added
A file was added to a download product.
tenant_ididproduct_id
download.file.removed
A file was removed from a download product.
tenant_ididproduct_id
newsletter.post.created
A newsletter post was created.
tenant_ididproduct_id
newsletter.post.published
A newsletter post was published.
tenant_ididproduct_id
newsletter.post.sent
A newsletter post was emailed to subscribers.
tenant_ididproduct_idsent
newsletter.post.deleted
A newsletter post was deleted.
tenant_ididproduct_id
coaching.session.created
A coaching session was created.
tenant_ididproduct_id
coaching.session.updated
A coaching session changed.
tenant_idid
coaching.session.deleted
A coaching session was deleted.
tenant_ididproduct_id
coaching.settings.updated
A coaching product’s settings changed.
tenant_idproduct_id
access_group.updated
A community access group changed.
tenant_ididproduct_id
podcast.updated
A podcast’s settings changed.
tenant_ididproduct_idaction
podcast.episode.created
A podcast episode was created.
tenant_ididproduct_id
podcast.episode.updated
A podcast episode changed.
tenant_idid
podcast.episode.published
A podcast episode was published.
tenant_ididproduct_id
podcast.episode.deleted
A podcast episode was deleted.
tenant_ididproduct_id
announcement.created
A product announcement was created.
tenant_ididproduct_id
announcement.updated
A product announcement changed.
tenant_ididactionsent
announcement.published
A product announcement was published.
tenant_idid
announcement.removed
A product announcement was removed.
tenant_ididproduct_id
lesson.quiz.saved
A lesson’s quiz was created or edited.
tenant_idlesson_id
lesson.quiz.removed
A lesson’s quiz was removed.
tenant_idlesson_id
lesson.quiz.submitted
A member submitted a lesson quiz. Sent with `lesson.quiz.passed` or `lesson.quiz.failed`.
tenant_idlesson_idproduct_idcontact_idquiz_idscore_pctpassed
lesson.quiz.passed
A member passed a lesson quiz.
tenant_idlesson_idproduct_idcontact_idquiz_idscore_pct
lesson.quiz.failed
A member failed a lesson quiz.
tenant_idlesson_idproduct_idcontact_idquiz_idscore_pct
Media & storage
storage.object.created
A file upload was started.
tenant_idid
storage.object.ready
An uploaded file finished uploading.
tenant_idid
storage.object.deleted
An uploaded file was deleted.
tenant_idid
Website & blog
site.updated
Website settings changed.
tenant_idid
page.created
A website or funnel page was created.
tenant_ididsite_id
page.updated
A website page changed.
tenant_idid
page.published
A website page was published.
tenant_idid
page.unpublished
A website page was unpublished.
tenant_idid
page.deleted
A website page was deleted.
tenant_idid
post.created
A blog post was created.
tenant_ididsite_id
post.updated
A blog post changed.
tenant_idid
post.published
A blog post was published.
tenant_idid
post.unpublished
A blog post was unpublished.
tenant_idid
post.deleted
A blog post was deleted.
tenant_idid
Email
sequence.subscribed
A contact was added to an email sequence.
tenant_idsequence_idcontact_idsubscription_id
sequence.completed
A contact reached the end of an email sequence.
tenant_idcontact_idsequence_id
sequence.unsubscribed
A contact was removed from an email sequence.
tenant_idcontact_idsequence_id
broadcast.created
An email broadcast was created.
tenant_idid
broadcast.sent
An email broadcast was sent.
tenant_ididsentrecipients
Quiz funnels
survey.published
A quiz funnel was published.
tenant_idsurvey_id
survey.unpublished
A quiz funnel was unpublished.
tenant_idsurvey_id
survey_response.completed
Someone finished a quiz funnel.
tenant_idsurvey_idresponse_idscoreending_refcontact_id
survey_response.partial
A quiz respondent passed the partial-submit point. Sent once per response.
tenant_idsurvey_idresponse_idcontact_id
Migration & import
import.committed
An import finished. One event per import, with counts — never one per row.
tenant_idimport_job_idkindcreatedupdatedskippedfailed
Team
team.ownership_transferred
The workspace owner handed ownership to a teammate.
tenant_idfrom_user_idto_user_id