Webhooks
The events WTN sends, the signed envelope, how to verify it and the delivery guarantees.
Webhooks tell your server when feedback arrives, so you do not have to poll. They carry ids and counts only, never a word a person said or a theme label.
Events
| Event | When |
|---|---|
respondent.acknowledged | A text acknowledgement was sent to the respondent. |
feedback.received | A feedback session finished. |
feedback.analyzed | The analysis of that session completed (once per session). |
feedback.received and feedback.analyzed add deviceId and userId when the app proved them.
Register an endpoint
Scope channel:webhooks. The URL must be https://, resolve to a public address (re-checked at every send) and not embed a username or password.
POST /api/v1/channel/webhooks register (body has eventTypes); the secret is shown ONCE
GET /api/v1/channel/webhooks list (never the secret)
GET /api/v1/channel/webhooks/:id one endpoint
PATCH /api/v1/channel/webhooks/:id update url, description, event filter, active state
DELETE /api/v1/channel/webhooks/:id remove
POST /api/v1/channel/webhooks/:id/simulate send a synthetic webhook.test delivery right now
GET /api/v1/channel/webhooks/:id/deliveries the delivery trail, keyset-pagedProve your receiver works first: register, then call simulate. It uses the same signed path as a real event.
The flow
Your endpoint must:
- Verify the signature over the exact raw bytes (see below).
- Store the event id (
X-WTN-Event) and skip it if it was already seen. - Reply
2xxquickly.
A 2xx within 10 seconds means delivered. Anything else, or a timeout, is retried after 1 min, 5 min, 30 min, 2 h, 12 h: 5 attempts in total, then the delivery is marked DEAD. Every delivery carries the headers X-WTN-Signature, X-WTN-Event, X-WTN-Event-Type and X-WTN-Delivery.
Verify the signature
Every delivery carries X-WTN-Signature: t=<unix-seconds>,v1=<hex-hmac-sha256>, computed over <t>.<raw request body> with your webhook secret. Verify the exact bytes received: re-serialising parsed JSON changes key order and whitespace and the signature will never match. Reject a timestamp more than 5 minutes off, and compare in constant time.
# Scope channel:webhooks. Register your endpoint, then send yourself a test delivery.
# VALUES - replace these
API_BASE="https://api.wtn.live"
ENDPOINT_URL="https://your-server.example.com/wtn"
curl -sS -X POST "$API_BASE/api/v1/channel/webhooks" \
-H "Authorization: Bearer $WTN_API_KEY" -H 'Content-Type: application/json' \
-d '{"url":"'"$ENDPOINT_URL"'","eventTypes":["feedback.received","feedback.analyzed","respondent.acknowledged"]}'
# The answer holds the signing secret once: keep it as WTN_WEBHOOK_SECRET.
SUBSCRIPTION_ID="ID_FROM_THE_ANSWER"
curl -sS -X POST "$API_BASE/api/v1/channel/webhooks/$SUBSCRIPTION_ID/simulate" -H "Authorization: Bearer $WTN_API_KEY"
# Check one delivery by hand. X-WTN-Signature is t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">.
# Save the body exactly as received to body.json and the header value to WTN_SIGNATURE_HEADER.
T=$(printf '%s' "$WTN_SIGNATURE_HEADER" | sed -E 's/.*t=([0-9]+).*/\1/')
V1=$(printf '%s' "$WTN_SIGNATURE_HEADER" | sed -E 's/.*v1=([0-9a-f]+).*/\1/')
EXPECTED=$( { printf '%s.' "$T"; cat body.json; } | openssl dgst -sha256 -hmac "$WTN_WEBHOOK_SECRET" | sed 's/^.* //')
AGE=$(( $(date +%s) - T ))
# Replay window 300 seconds. A shell compare is not constant-time: real receivers use the code in the other tabs.
if [ "$AGE" -le 300 ] && [ "$AGE" -ge -300 ] && [ "$EXPECTED" = "$V1" ]; then echo "signature OK"; else echo "REJECT"; fi
{
"id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a60",
"type": "feedback.received",
"aggregate": "conversation",
"aggregateId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a61",
"orgId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a62",
"occurredAt": "2026-10-19T10:00:00.000Z",
"payload": {
"conversationId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a61",
"campaignId": "YOUR_CAMPAIGN_ID",
"publicRef": "EXAMPLE_PUBLIC_REF",
"appInstallId": null,
"deviceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"userId": null,
"answered": 3,
"total": 3
}
}Headers: X-WTN-Signature, X-WTN-Event (the id above), X-WTN-Event-Type, X-WTN-Delivery. Ids and counts only, never a word the person said.
Delivery guarantees
- At least once, never exactly once. Deduplicate on the event id (
X-WTN-Event, andidinside the envelope). - No ordering guarantee. Sort by
occurredAtif you need an order. - A failure never blocks the fact. Results are available whether or not your receiver was reachable; polling is always a valid fallback.
- An endpoint that is DEAD five times in a row is disabled with the reason recorded. Fix it and re-enable it.
- A leaked secret: rotate by re-registering the endpoint; the new secret is shown once.