Skip to main content

Webhooks

Instead of polling the catalog, you can register a webhook receiver and have the Data API POST an event to your endpoint whenever a learning opportunity changes. Snapshot events carry the full public resource snapshot, so most consumers can apply them directly without a follow-up read. Delete events carry data: null.

note

Webhook receivers are currently provisioned manually for internal consumers, alongside API keys. Self-serve receiver management is planned but not yet available.

Event types

You subscribe a receiver to one or more event types. A receiver with no subscription set receives all of them.

TypeWhen it fires
LEARNING_OPPORTUNITY_PUBLISHEDA learning opportunity becomes publicly visible.
LEARNING_OPPORTUNITY_UPDATEDA published learning opportunity's public data changes.
LEARNING_OPPORTUNITY_ARCHIVEDA learning opportunity is archived and is no longer publicly visible.
LEARNING_OPPORTUNITY_DELETEDA learning opportunity is deleted.

Changes to records that are not publicly visible are suppressed: you only receive events that correspond to a change a public consumer can observe.

Event payload

Every delivery is a POST with a JSON body in this shape:

{
"id": "5f0d8f3e-0b6a-4a3e-9b1a-2c9b1e7d4f21",
"type": "LEARNING_OPPORTUNITY_UPDATED",
"occurredAt": "2026-07-01T12:34:56.000Z",
"resource": {
"type": "LEARNING_OPPORTUNITY",
"id": "9c3a1b2d-4e5f-6a7b-8c9d-0e1f2a3b4c5d"
},
"data": {
// Full public learning opportunity resource, identical in shape to a
// GET /v1/learning-opportunities/{id} result.
}
}
  • id is stable across delivery attempts and redeliveries of the same change. Dedupe on it — treat delivery as at-least-once.
  • resource.id matches the /v1 resource id, so you can reconcile against the REST API.
  • data holds the full snapshot at event time for published/updated/archived events, and is null for LEARNING_OPPORTUNITY_DELETED (the resource no longer exists).

Delivery headers

HeaderValue
content-typeapplication/json
user-agentSnowday-Data-API-Webhooks/1
x-snowday-event-idThe event id, so you can dedupe without parsing the body.
x-snowday-event-typeThe event type, so you can route without parsing the body.
x-snowday-timestampDelivery timestamp (epoch milliseconds), part of the signature.
x-snowday-signatureHMAC signature of the delivery (only when a signing secret is set).

Verifying signatures

If your receiver is configured with a signing secret, every delivery includes an x-snowday-signature header of the form sha256=<hex>. The signature is an HMAC-SHA256, keyed with your shared secret, over the string "{x-snowday-timestamp}.{raw request body}".

To verify a delivery:

  1. Read the x-snowday-timestamp and x-snowday-signature headers.
  2. Recompute HMAC_SHA256(secret, timestamp + "." + rawBody) over the raw request body bytes (before any JSON parsing/re-serialization).
  3. Compare in constant time against the hex in the header.
  4. Reject deliveries whose timestamp is outside your tolerance window (for example a few minutes) to bound replay.
import { createHmac, timingSafeEqual } from "node:crypto"

function isValidSignature(
secret: string,
timestamp: string,
rawBody: string,
signatureHeader: string,
): boolean {
const expected = `sha256=${createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex")}`

const a = Buffer.from(signatureHeader)
const b = Buffer.from(expected)
return a.length === b.length && timingSafeEqual(a, b)
}

Responding

Return a 2xx status to acknowledge receipt. Any non-2xx response, a connection error, or a timeout is treated as a failed attempt and retried with exponential backoff. After the configured maximum attempts, the delivery is dead-lettered and no longer retried.

Because deliveries are retried and can be redelivered, your handler must be idempotent: dedupe on the event id. For snapshot events, apply data as an upsert keyed by resource.id; for LEARNING_OPPORTUNITY_DELETED (where data is null), remove the record keyed by resource.id instead.

tip

Acknowledge quickly (persist the raw event and return 2xx), then process asynchronously. Slow handlers risk hitting the delivery timeout and being retried even though they eventually succeeded.