Skip to content

Webhooks (v1)

Authoritative. All tenants receive this payload shape with this signature scheme.

Register your webhook URL in the SNOW portal (Integration → Webhooks). On registration, SNOW issues a whsec_<64hex> secret. Store it in your secret manager.

SNOW sends:

POST <your webhook URL>
Content-Type: application/json
X-Snow-Signature: t=<unix_seconds>,v1=<hmac_sha256_hex>[,v1=<hmac_sha256_hex>]
X-Snow-Delivery: <delivery_id>

Body:

{
"id": "<delivery_id — UUID, stable across retries>",
"event": "session.completed",
"created_at": "2026-04-23T10:15:00.000Z",
"data": { ... event-specific payload ... }
}

Every async resource emits the same four-verb lifecycle: <resource>.{created,processing,completed,failed}. New resources added to the platform inherit this contract — handlers can route on event.split(".")[0] and switch on event.split(".")[1] without further changes.

EventFires when
session.createdPOST /v1/sessions accepted
session.processingPipeline started
session.completedSummary ready
session.failedSummarization failed (data.error present)
session.testTest event triggered from the portal
intake_session.createdDoctor created an intake-session token via POST /v1/intake-sessions
intake_session.submittedPatient submitted form via POST /v1/intake-sessions/:token/submit
intake_session.processingPipeline started
intake_session.completedIntake summary ready
intake_session.failedIntake summarization failed (data.error present)
document.completedDocument pipeline completed
document.failedDocument pipeline failed

data always includes the client-supplied metadata object echoed back verbatim — use it to correlate deliveries with your own records (appointment_id, patient_id, etc.).

  1. Parse X-Snow-Signature into t and one or more v1 values.
  2. Reject if |now - t| > 300s (replay window).
  3. Compute HMAC_SHA256(secret, "${t}.${rawBody}") hex.
  4. crypto.timingSafeEqual against each v1 value — accept if any matches.

During a 24h secret rotation window, SNOW sends two v1 values (old + new). Verify against your current secret; accept either.

Reference Node.js verifier:

import crypto from "node:crypto"
export function verifySnowSignature(secret, rawBody, header) {
const parts = header.split(",").map(s => s.trim())
const t = Number(parts.find(p => p.startsWith("t="))?.slice(2))
const sigs = parts.filter(p => p.startsWith("v1=")).map(p => p.slice(3))
if (!Number.isFinite(t) || sigs.length === 0) return false
if (Math.abs(Math.floor(Date.now()/1000) - t) > 300) return false
const expected = crypto.createHmac("sha256", secret)
.update(`${t}.${rawBody}`).digest("hex")
const exp = Buffer.from(expected, "hex")
return sigs.some(v => {
const got = Buffer.from(v, "hex")
return got.length === exp.length && crypto.timingSafeEqual(got, exp)
})
}
  • Retry schedule: 10s → 60s → 5m → 30m (4 attempts total).
  • Success: any 2xx response.
  • Failure: non-2xx, timeout (>10s), or connection error triggers retry.
  • Idempotency: id is stable across retries. Dedupe on your side by persisting id on first successful processing.
  • Dead letters: after 4 failed attempts, delivery is marked dead. Visible in GET /v1/me/webhook/deliveries; replay via POST /v1/me/webhook/deliveries/:id/replay.
  • At-least-once within the retry window.
  • Order is not guaranteed — process by id + payload content, not arrival order.
  • Replay window: 300 seconds.
  • No payload larger than 256 KB.
  • GET /v1/me/webhook/deliveries — recent deliveries with status, attempts, latency.
  • POST /v1/me/webhook/deliveries/:id/replay — reset a delivery to pending.
  • PUT /v1/me/webhook — rotate secret (24h grace window; both old and new valid).