Webhooks for developers
Webhooks tell your systems when something happens in a workspace. To add an endpoint, see Webhooks in Settings. This page covers what we send and how to check it.
Events
| Event | Sent when |
|---|---|
study.published | A study is launched |
study.updated | A study changes |
session.completed | An interview or response is finished |
session.replaced | A session is replaced |
readout.ready | A study report has been generated |
report.published | A report is published |
report.commented | Someone comments on a report |
finding.created | A Swarm run finds a problem |
finding.confirmed | People confirm a Swarm finding |
file.uploaded | A file is uploaded to the Library |
file.ready | An uploaded file has been processed |
meeting.recorded | The notetaker recorded a meeting |
meeting.failed | The notetaker couldn't record a meeting |
run.started | A Swarm run starts |
member.invited | Someone is invited to the workspace |
member.joined | Someone joins the workspace |
checkout.completed | A payment goes through |
The request
We send an HTTPS POST with a JSON body:
{
"id": "dlv_…",
"type": "readout.ready",
"createdAt": "2026-10-07T09:30:00.000Z",
"workspaceId": "ws_…",
"data": { "…": "depends on the event" }
}
Headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | UserEvaluation-Webhooks/1.0 |
UE-Event | The event type, e.g. readout.ready |
UE-Delivery | The delivery id; the same as id in the body |
UE-Signature | t=<unix seconds>,v1=<hex signature> |
Test events sent with Send test event have "test": true in data.
Verify the signature
The signature is an HMAC-SHA256 of <t>.<raw body>, keyed with your endpoint's signing secret (whsec_…), as lowercase hex.
- Read the
UE-Signatureheader and split it on commas intot=and one or morev1=values. - Compute
HMAC-SHA256(secret, t + "." + rawBody)over the raw request body, exactly as received. - Accept the request if your value matches any
v1value, using a constant-time comparison. - Reject it if
tis more than 5 minutes (300 seconds) old, to stop replays.
For 24 hours after you rotate the secret, the header carries two v1 values, one for each secret.
Node.js (Express)
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.UE_WEBHOOK_SECRET; // whsec_…
app.post('/ue-webhook', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('UE-Signature') || '';
const parts = header.split(',').map((p) => p.split('='));
const t = parts.find(([k]) => k === 't')?.[1];
const sigs = parts.filter(([k]) => k === 'v1').map(([, v]) => v);
const expected = crypto.createHmac('sha256', SECRET).update(`${t}.${req.body}`).digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(t)) <= 300;
const valid = sigs.some((s) => s.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
if (!fresh || !valid) return res.status(400).send('bad signature');
const event = JSON.parse(req.body);
// handle event.type …
res.sendStatus(200);
});
Python (Flask)
import hmac, hashlib, json, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["UE_WEBHOOK_SECRET"].encode()
@app.post("/ue-webhook")
def ue_webhook():
raw = request.get_data()
parts = [p.split("=", 1) for p in request.headers.get("UE-Signature", "").split(",")]
t = next((v for k, v in parts if k == "t"), "0")
sigs = [v for k, v in parts if k == "v1"]
expected = hmac.new(SECRET, f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
if abs(time.time() - int(t)) > 300 or not any(hmac.compare_digest(s, expected) for s in sigs):
abort(400)
event = json.loads(raw)
# handle event["type"] …
return "", 200
Responses and retries
- Reply with any
2xxstatus within 10 seconds to accept a delivery. - Anything else, or no reply, counts as a failure. We try up to 5 times in all, waiting about 10 seconds, 1 minute, 5 minutes and 30 minutes between tries. After that the delivery is marked failed.
- You can retry any delivery by hand from Recent deliveries in Settings.
- The same delivery can arrive more than once. Use
UE-Deliveryto ignore repeats. - Deliveries to a paused endpoint aren't sent.