Skip to content

Reference

Webhooks

Every change to a simulation - from your requests, the behaviour plan or a fast-forward - emits an event. Webhooks deliver them to you, signed.

Registering

Add a webhook in the dashboard or with POST /v1/simulations/{id}/webhooks. Filter with exact event names (task.created), prefixes (task.*) or everything (*). The response includes the signing secret, which starts whsec_.

  • Targets must be public https URLs. Private, loopback, link-local and cloud metadata addresses are refused, and re-checked when each connection is made.
  • Redirects are not followed. Respond with any 2xx within 10 seconds.
  • Your plan sets how many webhooks each simulation can have.

Request format

headers
POST /hooks/slurry HTTP/1.1
Content-Type: application/json
User-Agent: Slurry-Webhooks/1.0 (+https://slurry.io)
Slurry-Event: task.updated
Slurry-Delivery: 9b2d6c1e-3f7a-4e0b-8a55-1c4f2e7d9a10
Slurry-Signature: t=1790327472,v1=5f1c0b6e2a...
body
{
  "id": "e3b1f4a2-7c9d-4d6e-b8a1-2f5c6d7e8f90",
  "type": "task.updated",
  "created": "2026-09-25T09:41:12.000Z",
  "data": {
    "collection": "tasks",
    "id": "1204417763512",
    "object": {
      "gid": "1204417763512",
      "name": "Invoice sync drops line items over 250",
      "status": "in_review",
      "assignee": { "gid": "1204417760001", "name": "Priya Raman" },
      "modified_at": "2026-09-25T09:41:12.000Z"
    }
  }
}

data.object is the record as your code would see it from the API. On deletes it may be null, so use data.id. Use Slurry-Delivery or id to make your handler idempotent: a retry carries the same delivery id.

Verifying signatures

Slurry-Signature is t=<unix seconds>,v1=<hex>, where the hex is HMAC-SHA256(secret, t + "." + raw body). Compute it over the raw bytes you received, compare in constant time, and reject timestamps more than five minutes old to stop replays.

import crypto from 'node:crypto'

// Use the raw request body, exactly as received, before any JSON parsing.
export function verifySlurry(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
  const t = Number(parts.t)
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
  const a = Buffer.from(expected, 'hex')
  const b = Buffer.from(parts.v1 ?? '', 'hex')
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

Retries

Any non-2xx response, timeout or connection error is retried with backoff:

AttemptDelay after previous failure
1Immediate
21 minute
35 minutes
430 minutes
52 hours
612 hours
After the sixth failed attempt the delivery is marked failed. An endpoint that fails 50 deliveries in a row is switched off automatically; turn it back on in the dashboard once it is fixed.