The raw event, signed, at your endpoint in seconds.

Every click and every QR scan is POSTed to a URL you own as structured JSON — carrying HMAC signature headers your code can verify, retried on a backoff ladder when you're down, and logged attempt by attempt. This is the primitive behind our delivery guarantees, pointed at your own code.

Webhook channel set-up in the xengo console
No SDK, no OAuth, no library

One POST URL. Everything else is yours.

Any route that accepts a JSON POST is a valid endpoint — a Lambda, a Rails controller, a Flask route, a Worker. Paste its URL, keep the signing secret, and verify deliveries in five lines of standard-library code.

  1. 1

    Stand up an endpoint

    In your own stack: any route that accepts an HTTP POST with a JSON body — a serverless function, an API route, a five-line Flask app. All it has to do is read the event and return a 2xx.

  2. 2

    Add the channel in xengo

    Console → Notifications → Add channel → Webhook. Paste your URL into the POST URL field and save. Copy the whsec_ signing secret when it's shown — that's the only time it ever appears — then hit Send test.

  3. 3

    Choose what it hears

    Attach the channel to the links and QR codes it should hear about — one campaign's codes, or every link you run. One endpoint per system or one for everything: channels are cheap; make as many as your architecture wants.

A delivery log, not a black hole

A delivery log, not a black hole

Every attempt — the immediate one and every retry — is recorded per channel with its HTTP result and attempt number. When someone asks whether the webhook fired at 14:02, you answer with a row, not a guess.

  • Per-attempt rows with the actual response code
  • Retries logged in real order, stamped at attempt time
  • Terminal failures recorded, never silently dropped
Failures retry themselves

Failures retry themselves

A non-2xx, a timeout or a refused connection schedules the next attempt automatically — 30 seconds, then 2, 10, 15 and 15 minutes. Six attempts in all, and the event id never changes, so de-duplication on your side is one dictionary lookup.

  • Six attempts across roughly 45 minutes of backoff
  • Same webhook-id on every attempt — your de-dupe key
  • Paused or deleted channels stop retrying immediately
The event

What Webhook receives — the whole story, as JSON

Not a bare ping: the full scan event — code, source, destination, tags, timestamp, device — arrives at your Endpoint URL within seconds of the scan.

Every delivery carries Svix-compatible signature headers (webhook-id, webhook-timestamp, webhook-signature) signed with your channel's whsec_ secret, and failures retry on a backoff ladder for up to six attempts — with every attempt visible in the console's delivery log. Verification docs →

{
  "event": "click",
  "source": "qr",
  "code": "spring-launch",
  "shortUrl": "go.yourbrand.com/spring-launch",
  "title": "Spring launch pack",
  "destination": "https://yourbrand.com/spring",
  "tags": { "campaign": "spring-24", "sku": "SB-114" },
  "createdAt": "2026-05-02T09:14:11Z",
  "ts": 1752319441000,
  "at": "2026-07-12T10:44:01+00:00",
  "ip": "203.0.113.7",
  "ua": "Mozilla/5.0 (iPhone; …)",
  "referer": null,
  "customerId": "cus_9f2k…"
}

The details that make it dependable

Svix-compatible signing

Three headers on every delivery — webhook-id, webhook-timestamp, webhook-signature (v1, base64 HMAC-SHA256). Verify with any Svix library or a few lines of stdlib.

A secret only you hold

Each channel mints its own whsec_ secret at creation and shows it exactly once, like an API key. It is never readable again — not by you, not by support.

Byte-for-byte verifiable

The signature covers id.timestamp.body over the exact bytes we send, so receiver-side verification matches without canonicalisation tricks.

Fresh timestamp per attempt

Retries re-sign with a new webhook-timestamp while keeping the same webhook-id — replay-window checks keep working and de-duplication stays trivial.

Off the hot path

Events leave the redirect plane through a queue, so a slow endpoint never slows a visitor — and the event is typically with you in seconds.

Honest semantics

At-least-once delivery: a success that times out on the response may be re-sent. De-dupe on webhook-id and you will never double-count a click.

Frequently asked questions

How do I verify the webhook signature?

Recompute HMAC-SHA256 over "id.timestamp.body" — the webhook-id header, the webhook-timestamp header and the raw request body — keyed with the base64 part of your whsec_ secret, and compare it to the v1 value in webhook-signature. Any Svix verification library accepts the three headers directly, or it's about five lines of standard-library code; the verification docs at docs.xengo.io/api/webhooks/ have copy-paste examples.

What does my endpoint need to return?

Any 2xx status within the delivery timeout counts as delivered. Anything else — a non-2xx, a timeout, a refused connection — schedules the next attempt on the backoff ladder. Do the fast thing in the handler and the slow thing after you've responded.

What happens if my endpoint is down for an hour?

The ladder retries at 30 seconds, 2, 10, 15 and 15 minutes — six attempts covering roughly 45 minutes. An outage longer than that exhausts the ladder for events raised early in it, and each terminal failure is recorded in the delivery log, so you know exactly which events need reconciling from analytics.

Can I see the signing secret again later?

No. The whsec_ secret is shown once, at channel creation, and is never returned by the API afterwards — deliberately, the same way API keys work. If you lose it, create a new webhook channel (which mints a fresh secret) and retire the old one; that's also how you rotate.

Will I ever receive the same event twice?

Rarely, yes — delivery is at-least-once by design. If your endpoint processed an event but the response timed out, the retrier can't tell success from failure and sends it again. The webhook-id is identical on every attempt, so de-duplicating on it makes your processing exactly-once.

More webhook & email

One platform. Everything a link should do.

Smart URLs, dynamic QR codes and a full API — branded on your own domain, safety-screened, and tracked. Build, re-point and measure it all in one place.

Start free trial
1,284 clicks
go.acme.com/spring
1 London 412
2 Manchester 168
3 Bristol 94

Smart URLs

Branded short links on a domain you control — editable, screened, and tracked.

Popular Smart URLs features

  • Your own custom domain, auto-HTTPS
  • Re-point live links without reprinting
  • Every destination safety-screened
  • Honest, bot-filtered analytics

Dynamic QR

Branded QR codes you can re-point any time — never reprint the poster again.

Popular Dynamic QR features

  • Editable destination behind every code
  • Your brand colours + logo
  • Real-time scan alerts
  • Per-code scan analytics
$ curl -X POST api.xengo.io/v1/links
{
  "domain": "go.acme.com",
  "url": "https://acme.com/…"
}
→ 201 go.acme.com/aB3xK

API

Create and manage links + QR at scale, straight from your own tools.

Popular API features

  • REST API with scoped keys
  • Bulk-create thousands of links
  • Programmatic QR generation
  • Webhooks + click / scan alerts

Point the next scan at your own code

14-day free trial, card required, cancel anytime. One POST URL and a Send test between you and your first signed event.