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

  1. Sign in at app.zinstruct.com as an owner or admin and open Settings → Webhooks.
  2. 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.
  3. 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.

FieldTypeDescription
idstringThe event id. Stable across retries and replays — use it to ignore a delivery you have already handled.
typestringThe event type, e.g. `transaction.succeeded`.
api_versionstringThe payload contract version. Currently `2026-05-24`.
created_atstring (date-time)When the event happened. ISO-8601, UTC.
dataobjectThe 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

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

contact.created
A contact was added.
tenant_ididcontact_id
contact.updated
A contact’s details or email consent changed.
tenant_ididmarketing_status
contact.deleted
A contact was deleted.
tenant_idid
contact.inactive
A contact passed a number of days without activity (`days` says how many). Sent by a daily sweep.
tenant_idcontact_iddaysdedupe
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
form.submitted
Someone submitted an opt-in form.
tenant_idcontact_idform_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
community.updated
A community’s settings changed.
tenant_idid
community.member_joined
A member joined a community through an enrollment.
tenant_idcontact_idproduct_idenrollment_iddedupe
community.post_created
A member’s community post became visible — on posting, or on approval.
tenant_idcontact_idproduct_idpost_iddedupe
community.comment_created
A member commented on a community post.
tenant_idcontact_idproduct_idpost_idcomment_iddedupe
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
lesson.comment.posted
A comment was posted on a lesson.
tenant_idlesson_idproduct_idcontact_ididauthor_kind
lesson.comment.removed
A lesson comment was removed.
tenant_ididlesson_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
media.asset.created
A video or audio upload or import was started.
tenant_idid
media.asset.ready
A video or audio asset finished processing.
tenant_idid
media.asset.deleted
A video or audio asset 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