Webhooks
Subscribe to events and we'll POST a signed JSON body to your URL when they happen — no polling needed.
Registering a webhook
curl -X POST https://api.userevaluation.com/v1/webhooks \
-H "Authorization: Bearer ue_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example.com/hooks/ue",
"events": ["project.created", "test.published", "engage.session.completed"],
"description": "production handler"
}'
The response includes a one-time secret field starting with whsec_. Save it — it's how you'll verify incoming requests. We can't show it again.
Event types
| Event | When |
|---|---|
project.created | A project is created (UI or API) |
project.updated | Project name/description changed |
project.deleted | Project deleted |
test.published | An engage test went Draft → Live |
test.response.submitted | A participant submits a test response |
file.transcribed | Transcription completed for a file (video, audio, doc) |
report.generated | Report-generation job succeeded |
chat.completed | Chat job succeeded (assistant turn ready) |
engage.session.completed | A live/AI interview call finished |
tag.created | A new tag was created in a project |
Subscribe to as many as you want with one webhook, or split across several.
Request shape
We POST JSON with these headers:
Content-Type: application/json
X-UE-Event: project.created
X-UE-Event-Id: 9a3f7c2d-1715520600000
X-UE-Signature: t=1715520600, v1=8c4e…
User-Agent: UE-Webhooks/1.0
And this body:
{
"id": "9a3f7c2d-1715520600000",
"type": "project.created",
"created_at": "2026-05-12T14:30:00.000Z",
"data": { ...resource payload... }
}
Always return a 2xx status as fast as you can — we time out after 5 seconds.
Verifying signatures
The X-UE-Signature header is t=<unix_timestamp>, v1=<hex>. To verify:
- Take the timestamp
t. - Take the raw request body (don't re-serialize — bytes matter).
- Compute
HMAC_SHA256(secret, "<t>.<rawBody>")in hex. - Compare to the
v1=value with a constant-time comparison.
Optionally, reject signatures older than ~5 minutes to limit replay risk.
Node example
import crypto from "node:crypto";
function verify(req, secret) {
const header = req.headers["x-ue-signature"];
const m = /t=(\d+),\s*v1=([0-9a-f]+)/.exec(header || "");
if (!m) return false;
const [, ts, sig] = m;
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${req.rawBody}`) // raw, not parsed
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
Python example
import hmac, hashlib
def verify(headers, raw_body, secret):
sig = headers.get("x-ue-signature", "")
t, v1 = dict(p.strip().split("=", 1) for p in sig.split(",")).get(...)
expected = hmac.new(secret.encode(), f"{t}.{raw_body}".encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
Retries and auto-disable
If your endpoint returns non-2xx (or times out), we retry with exponential backoff:
| Attempt | Delay before this attempt |
|---|---|
| 1 | (immediate) |
| 2 | 10s |
| 3 | 1m |
| 4 | 5m |
| 5 | 30m |
| 6 | 1h |
After 24 hours of continuous failures, we automatically disable the webhook. We'll email the user that created it. Re-enable by deleting and recreating the subscription once your endpoint is healthy.
Listing past deliveries
curl "https://api.userevaluation.com/v1/webhooks/<id>/deliveries" \
-H "Authorization: Bearer ue_live_..."
Returns the last 30 days of attempts with status, response code, and error.
Best practices
- Idempotency on your side too: we may deliver the same event more than once on retry. Use
X-UE-Event-Idto dedupe. - Verify before parsing: check the signature with the raw bytes BEFORE you JSON-parse. Otherwise you can't catch a tampered body that happens to be valid JSON.
- Respond fast: queue the event, return 200, then process. 5 seconds is the timeout.
- One handler per event class: simpler than dispatching by
typeinside a single handler.
Webhooks vs. Zapier triggers
Both fire on roughly the same events, but they're independent paths — pick one per event you care about.
- Public webhooks (this doc) are fast, signed, retried, owned by your workspace. Best for production integrations you're maintaining.
- Zapier triggers are best for no-code workflows where you don't run your own server.
If you subscribe to the same event in both, expect both to fire — there's no automatic dedupe between the two systems. Either pick one path per event, or have your handlers idempotent on X-UE-Event-Id (webhooks) and the Zapier action's deduplication key.