Overview
Get notified on your server when a purchase completes, whichever way you sell.
Webhooks tell your server that something happened in Jetron Ticket. When an
attendee completes a purchase, Jetron sends a signed POST to a URL you
register, so you can fulfil, record, or notify without polling the API.
Webhooks work the same way for both integration paths. Whether the sale came through the checkout embed or the API, you receive the same event. For the embed, where the parent page never learns that a purchase happened, webhooks are the only way to react on your side.
Registering an endpoint
Add your endpoint in the Jetron Ticket dashboard, on the event's Integrations tab. There is no signing secret to copy: Jetron signs deliveries with your private API key, which you already have.
A few constraints apply:
- The URL must use HTTPS.
- Each event may have up to 3 enabled subscriptions.
- A URL can only be registered once across the platform, so use a distinct
path per event if you want one service to receive several, for example
/api/webhooks/jetron/my-event. - Private, loopback, and link-local addresses are rejected, so you cannot point
a subscription at
localhost. To develop locally, use a tunnel such as ngrok or Cloudflare Tunnel.
Rotating your private key rotates the signature
Deliveries are signed with your private API key. If you rotate that key, every webhook receiver has to be updated with the new value or signatures will stop verifying.
What a delivery looks like
POST /api/webhooks/jetron HTTP/1.1
Content-Type: application/json
User-Agent: Jetron-Webhook-Dispatcher/1.0
X-Jetron-Signature: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08...
{ "eventType": "TICKET_PURCHASED", "data": { "...": "..." } }Every body shares one envelope: a discriminating eventType and a data
object. See Event types for the payloads, and
Verifying signatures before you trust any of it.
Delivery behaviour
Your endpoint should acknowledge quickly with a 2xx and do real work
afterwards.
| Behaviour | Detail |
|---|---|
| Retries | Up to 5 attempts, with backoff of 1s, 2s, 4s, then 8s |
| Overall budget | The whole delivery is capped at about 30 seconds, so in practice expect roughly 3 attempts |
| Timeout | 10 seconds per attempt |
| Success | Any status from 200 to 399 |
| Redirects | Not followed. A 3xx counts as delivered, so never redirect your webhook URL |
| Ordering | Not guaranteed. Concurrent purchases can arrive out of order |
| Duplicates | Possible. Retries reuse the same body and signature |
Be idempotent
There is no server-side deduplication and no replay protection. Treat
data.reference as the idempotency key: if you have already processed that
reference, acknowledge the delivery and do nothing else.
Only genuine attendee purchases trigger TICKET_PURCHASED. Complimentary and
admin-generated orders do not.
A minimal receiver
export const runtime = 'nodejs' // HMAC needs the Node runtime
export const dynamic = 'force-dynamic'
export async function POST(req: Request) {
const raw = await req.text() // read the raw body before parsing
const signature = req.headers.get('x-jetron-signature')
if (!verifySignature(raw, signature)) {
return Response.json({ error: 'invalid signature' }, { status: 401 })
}
const event = JSON.parse(raw)
if (event.eventType === 'TICKET_PURCHASED') {
await fulfil(event.data) // idempotent on event.data.reference
}
return Response.json({ ok: true })
}verifySignature is on the next page. The one thing to get right here is
reading the raw body first: parsing and re-serializing changes the bytes and
the signature will never match.
Live example
boba.jtr.rsvp/webhooks shows a working receiver with a live feed of verified deliveries, including retry counts. The receiver is open source at jetronmall/boba-event, and Example site explains it.

