Skip to main content

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​

EventSent when
study.publishedA study is launched
study.updatedA study changes
session.completedAn interview or response is finished
session.replacedA session is replaced
readout.readyA study report has been generated
report.publishedA report is published
report.commentedSomeone comments on a report
finding.createdA Swarm run finds a problem
finding.confirmedPeople confirm a Swarm finding
file.uploadedA file is uploaded to the Library
file.readyAn uploaded file has been processed
meeting.recordedThe notetaker recorded a meeting
meeting.failedThe notetaker couldn't record a meeting
run.startedA Swarm run starts
member.invitedSomeone is invited to the workspace
member.joinedSomeone joins the workspace
checkout.completedA 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:

HeaderValue
Content-Typeapplication/json
User-AgentUserEvaluation-Webhooks/1.0
UE-EventThe event type, e.g. readout.ready
UE-DeliveryThe delivery id; the same as id in the body
UE-Signaturet=<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.

  1. Read the UE-Signature header and split it on commas into t= and one or more v1= values.
  2. Compute HMAC-SHA256(secret, t + "." + rawBody) over the raw request body, exactly as received.
  3. Accept the request if your value matches any v1 value, using a constant-time comparison.
  4. Reject it if t is 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 2xx status 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-Delivery to ignore repeats.
  • Deliveries to a paused endpoint aren't sent.