Webhooks let PulseLABS push events to your server the moment something happens — a debate finishes, a simulation tick completes, or a report is ready. Instead of polling the API, you register an HTTPS endpoint and receive a signed HTTP POST within seconds of each event.
ℹ
All webhook requests are signed with HMAC-SHA256. Always verify the X-PulseLabs-Signature header before processing the payload.
Events
The following events can be subscribed to per endpoint. You can subscribe to any combination when creating a webhook.
debate.synthesized
A conversation message triggers a full multi-agent debate and produces a synthesis.
data: { conversationId, synthesis }
simulation.tick.completed
One turn of a SimEngine simulation advances (one debate round per active scenario).
data: { simulationId, turn, stateCount }
simulation.report.ready
A simulation is finalized and the full report is ready for retrieval.
data: { simulationId, name, turnsCompleted }
webhook.test
Sent when you click "Send test" in the dashboard or call the test endpoint.
data: { webhookId, message, timestamp }
Payload structure
Every delivery is an HTTP POST with a JSON body. The envelope is the same for all events:
X-PulseLabs-EventEvent name, e.g. debate.synthesized
X-PulseLabs-Signaturesha256=<hmac-hex> — use this to verify the payload
X-PulseLabs-DeliveryUnique delivery attempt UUID
User-AgentPulseLabs-Webhooks/1.0
Content-Typeapplication/json
Retry behavior
If your endpoint returns a non-2xx status or times out (10 second limit), PulseLABS retries automatically:
Attempt 1 — immediately
Attempt 2 — 1 second later
Attempt 3 — 5 seconds later
After 3 failures the delivery is marked as failed and recorded in the delivery log. Respond with 200 as quickly as possible — process the event asynchronously to avoid timeouts.
Verifying signatures
Compute an HMAC-SHA256 over the raw request body using your webhook secret. Compare using a timing-safe equality check to prevent timing attacks.
⚠
Always parse the raw body bytes before JSON-decoding. Parsing first and re-serializing will produce a different byte sequence and fail the check.
import crypto from 'crypto'
function verifySignature(rawBody, signatureHeader, secret) {
const expected = 'sha256=' +
crypto.createHmac('sha256', secret)
.update(rawBody)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader),
)
}
// Express example
app.post('/hooks/pulselabs', express.raw({ type: '*/*' }), (req, res) => {
const sig = req.headers['x-pulselabs-signature']
const body = req.body // raw Buffer
if (!verifySignature(body, sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Signature mismatch')
}
const event = JSON.parse(body.toString())
console.log('Event:', event.event, event.data)
res.status(200).send('ok')
})
API reference
Register a new webhook endpoint. The signing secret is returned once in this response and never again — store it immediately.