Skip to main content
Webhooks push what happens in Usertour to a URL you control: a user completes a flow, answers a survey question, a company’s attributes change, a version is published. Each message is a signed JSON POST that references the same objects and ids the REST API uses, so a receiver can look anything up without a translation layer. Key ideas, up front:
  • Endpoints belong to an environment. Production and Staging each have their own list; an endpoint only ever receives that environment’s traffic.
  • You subscribe by topic. event.tracked.flow_completed, user.updated, content.published, … — or a whole family at once so future additions flow in automatically.
  • Every message is signed with a per-endpoint secret (X-Usertour-Signature), so your receiver can verify it really came from Usertour and hasn’t been tampered with.
  • Delivery is at-least-once with retries. A stable message id lets you deduplicate; the message log shows every attempt and lets you re-send.
  • Availability. On Usertour Cloud, webhooks are included from the Starter plan; self-hosted instances are never gated. Managing endpoints requires the Owner role.

Create an endpoint

Go to Settings → Webhooks (pick the environment first — the page is per environment) and click New webhook. The New webhook dialog Creating the endpoint generates its signing secret (whsec_…). Open the endpoint’s detail page to reveal or copy it, and to rotate it — rotation takes effect immediately, including for retries already in flight, so update your receiver first. Endpoint detail page with the signing secret
Trying it out before you have a receiver.
  • Fastest check: save the endpoint and hit Send test event — a webhook.test message arrives immediately, so you can confirm the URL is reachable and your signature check works before any real event fires.
  • No receiver yet? Point the endpoint at a request-inspection service such as webhook.site or Beeceptor: they hand you a throwaway HTTPS URL and show every request they receive, headers included.
  • Receiver running on your machine? It needs a public HTTPS address, so put a tunnel in front of it — ngrok or cloudflared forward a public URL to localhost. (A tunnel is not a receiver: your own service still has to be running behind it.) Self-hosted operators can skip the tunnel by setting ALLOW_PRIVATE_NETWORK_EGRESS=true and pointing the endpoint straight at http://localhost:….

Topics

A topic names one kind of message. Subscriptions are a tree — pick a leaf, a family, or everything: <codeName> is the event’s code name as shown in Settings → Events — built-in ones such as flow_started, flow_step_seen, flow_completed, flow_ended, checklist_started, checklist_task_completed, checklist_completed, launcher_activated, banner_seen, resource_center_opened, event_tracker_completed, question_answered, announcement_seen, plus any custom event you track with usertour.track().
page_viewed is high-volume and is not included by * or event.tracked. Subscribe to event.tracked.page_viewed explicitly if you really want every page view.
In the dashboard the same tree is a picker: Select all stores *, checking a family (Tracked events / Content / Users / Companies) stores its prefix, and checking individual events stores exact topics. Groups such as Flows or Checklists are a convenience that expands to their current events — they don’t auto-include events added later; check the whole Tracked events family for that. The subscription picker

Message format

Every delivery is a JSON body with a fixed envelope. type is the topic, id is the message id (stable across retries and re-sends — use it to deduplicate), and data depends on the topic.
Two naming layers. Field names that Usertour defines — the envelope and the objects inside data — are camelCase, identical to the REST API. Keys inside attributes are event or user/company attribute code names, passed through exactly as they exist in your workspace: built-in ones are snake_case (flow_id, page_url), and custom ones appear under whatever code name you created them with. They are deliberately not rewritten, so the key you filter on in Usertour is the key you receive here.

event.tracked.*

data.event is the event object — the same shape the REST API returns. userId / companyId are the ids you passed to identify() / group(); sessionId / contentId / versionId are set for session-scoped events (flow, checklist, banner, …) and null for sessionless ones (custom track() events, trackers); attributes holds the event’s properties (the per-type *_id / *_name / *_version_* fields plus page_url and viewport size).

content.published

A thin notification — ids only. Fetch details from GET /v2/projects/{projectId}/content/{contentId} and its versions endpoint if you need them.

user.created / user.updated / user.deleted and company.*

data.user (or data.company) is the full object as of the change — for deleted, as it was just before deletion. updated additionally carries previousAttributes: the old values of only the attributes that changed or were removed, so you can tell what changed without diffing.
created and updated fire only when attributes actually change — an identify() that repeats the same values sends nothing. Timestamp bookkeeping such as last_seen_at never triggers a notification.

webhook.test

Sent by the Send test event button: the envelope with "type": "webhook.test" and "data": {}. Handy for checking connectivity and signature verification.

Verify the signature

Each request carries an X-Usertour-Signature header:
  • t — Unix timestamp (seconds) of the attempt
  • v1 — HMAC-SHA256, hex, keyed with the endpoint’s signing secret (the whole whsec_… string), over the string "{t}.{raw body}"
To verify: read the raw request body bytes (before any JSON parsing or re-serialization), recompute the HMAC over t + "." + body, and compare it to v1 in constant time. Reject requests whose t is outside a tolerance window (5 minutes is typical) to block replays.
Verify against the raw body. Frameworks that parse JSON before your handler runs (and hand you a re-serialized string) will produce a different byte sequence and the signature will not match.

Delivery, retries, and the message log

  • Success is any 2xx response within 10 seconds. Respond first, do the work afterwards.
  • Anything else — a non-2xx status, a timeout, a connection error, or a redirect — counts as a failure and is retried with growing delays over roughly 24 hours: 8 attempts in total (after ~5 s, 1 m, 10 m, 1 h, 4 h, 8 h, 12 h). An outage on your side that lasts less than a day heals itself — every message is delivered once the receiver is back, no action needed. All attempts carry the same body and message id; only the signature timestamp changes.
  • Rate limiting: respond 429 (or 503) with a Retry-After header and the next attempt waits at least that long (never sooner than the schedule above); the header is ignored on other statuses.
  • Ordering is not guaranteed across messages; use createdAt (and the object’s own timestamps) rather than arrival order.
  • Repeated failures pause deliveries briefly. After 10 failed attempts in a row the endpoint cools down (starting at 1 minute, growing to at most 1 hour while failures continue — the dashboard shows a Cooling down badge). Deliveries are held, not dropped: everything is sent once the pause ends or a Send test event succeeds. If an endpoint keeps failing for 7 days, Usertour disables it and emails the project owner; re-enable it after fixing the receiver and use Resend for anything you need.
  • Disabled or deleted endpoints drop their in-flight deliveries; the drops are recorded in the message log as failed attempts, so after re-enabling you can see and re-send what was missed.
  • The message log on the endpoint’s detail page keeps every message for 30 days: the payload as sent and each attempt with status code, response excerpt, and error. Open a message to inspect it or Resend it — the same payload under the same message id, as one more attempt — after fixing your receiver.
The Recent messages log Message detail with payload and attempts

Manage endpoints from the API or MCP

Endpoints are also a REST resource under the environment (…/environments/{environmentId}/webhooks), so you can provision them from scripts or infrastructure-as-code — the operations are listed under Webhooks in the API reference, and the MCP server exposes them as list_webhooks, create_webhook, update_webhook, and delete_webhook. The token needs the webhook:read / webhook:manage scopes and, because webhooks act on a specific environment, must name that environment in its allowlist; single-object reads return the signing secret only when the token holds webhook:manage (the secret is the ability to sign deliveries); the list never includes it.