Webhooks (v1)
Authoritative. All tenants receive this payload shape with this signature scheme.
Endpoint registration
Section titled “Endpoint registration”Register your webhook URL in the SNOW portal (Integration → Webhooks). On registration, SNOW issues a whsec_<64hex> secret. Store it in your secret manager.
Request
Section titled “Request”SNOW sends:
POST <your webhook URL>Content-Type: application/jsonX-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 ... }}Events
Section titled “Events”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.
| Event | Fires when |
|---|---|
session.created | POST /v1/sessions accepted |
session.processing | Pipeline started |
session.completed | Summary ready |
session.failed | Summarization failed (data.error present) |
session.test | Test event triggered from the portal |
intake_session.created | Doctor created an intake-session token via POST /v1/intake-sessions |
intake_session.submitted | Patient submitted form via POST /v1/intake-sessions/:token/submit |
intake_session.processing | Pipeline started |
intake_session.completed | Intake summary ready |
intake_session.failed | Intake summarization failed (data.error present) |
document.completed | Document pipeline completed |
document.failed | Document 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.).
Signature verification
Section titled “Signature verification”- Parse
X-Snow-Signatureintotand one or morev1values. - Reject if
|now - t| > 300s(replay window). - Compute
HMAC_SHA256(secret, "${t}.${rawBody}")hex. crypto.timingSafeEqualagainst eachv1value — 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 + idempotency
Section titled “Retry + idempotency”- Retry schedule: 10s → 60s → 5m → 30m (4 attempts total).
- Success: any 2xx response.
- Failure: non-2xx, timeout (>10s), or connection error triggers retry.
- Idempotency:
idis stable across retries. Dedupe on your side by persistingidon first successful processing. - Dead letters: after 4 failed attempts, delivery is marked
dead. Visible inGET /v1/me/webhook/deliveries; replay viaPOST /v1/me/webhook/deliveries/:id/replay.
Delivery guarantees
Section titled “Delivery guarantees”- 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.
Tenant ops
Section titled “Tenant ops”GET /v1/me/webhook/deliveries— recent deliveries with status, attempts, latency.POST /v1/me/webhook/deliveries/:id/replay— reset a delivery topending.PUT /v1/me/webhook— rotate secret (24h grace window; both old and new valid).