Skip to content
Docs
Task

Receive webhooks

Register an endpoint, verify the signature on every delivery and test the whole path from a laptop through a tunnel.

You have a URL that should hear about orders, customers, events and tickets. This is how to register it, prove each delivery came from Ticketlayer, and try it before an order exists. What arrives, and in what shape, is in Webhooks. A receiver that keeps a copy of your events current from event.updated, event.deleted and the listing webhooks is in Syncing events.

Register an endpoint#

With an organisation API key or a staff token:

curl -X POST https://api.ticketlayer.com/v1/webhook-endpoints \
  -H "Authorization: Bearer tlak_..." \
  -H "X-Ticketlayer-Org: your-org-slug" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "acc_01J...",
    "url": "https://hooks.example.com/ticketlayer",
    "description": "Orders to the CRM",
    "eventTypes": ["order.confirmed", "ticket.issued"]
  }'

The response carries the endpoint and, once only, its secret. Omit eventTypes to receive every type. Endpoints are per account (the organisation's sub-unit that owns the events); the same URL can be registered for several accounts.

Operations: create, list, get, update (URL, description, event types, enabled/disabled), delete and test, which sends a signed sample delivery.

Verify the signature#

The signing secret is returned once when the endpoint is created. The signature is an HMAC-SHA256 of "<t>.<raw body>" with that secret, hex encoded; reject deliveries whose t is more than five minutes from now.

import crypto from 'node:crypto';

export function verifyTicketlayerSignature(rawBody: string, header: string, secret: string, toleranceSeconds = 300): boolean {
  const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=').map((s) => s.trim())));
  const t = Number(parts.t);
  const v1 = parts.v1;
  if (!t || !v1) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSeconds) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  const a = Buffer.from(v1, 'hex');
  const b = Buffer.from(expected, 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Compute the HMAC over the raw request body, before any JSON parsing or re-serialisation.

Express
app.post('/webhooks/ticketlayer', express.raw({ type: 'application/json' }), (req, res) => {
  const raw = req.body.toString('utf8');
  if (!verifyTicketlayerSignature(raw, req.get('Ticketlayer-Signature') ?? '', process.env.TL_WEBHOOK_SECRET!)) {
    return res.status(400).send('bad signature');
  }
  const envelope = JSON.parse(raw);
  if (envelope.type === 'order.confirmed') {
    // envelope.data is the Order payload above
  }
  res.sendStatus(204);
});

Local development#

Run a tunnel (for example ngrok http 3000) and register the tunnel URL as the endpoint. The test operation above is the quickest way to see a delivery without buying a ticket.