Receive webhooks
This guide builds a webhook receiver in Node and connects it to a competition. It needs a public HTTPS URL, the signing secret of your organization, and about half an hour.
The contract in one line: LIGR sends one signed POST per change, waits 10 seconds, and retries
a failed delivery with up to a minute of retry delays. Your endpoint must verify, answer 2xx at once, and
process afterwards.
-
Get the secret.
Open Developers → Webhooks in the dashboard and copy the signing secret. Your organization has one secret, and every webhook of the organization uses it. Put it in an environment variable on your server. Never put it in a browser.
-
Write the receiver.
Read the raw body before anything parses it. The signature covers the bytes LIGR sent.
server.ts import { createHmac, timingSafeEqual } from 'node:crypto'import express from 'express'const SECRET = process.env.LIGR_WEBHOOK_SECRET!const FIVE_MINUTES = 5 * 60_000const seen = new Set<string>()function verify(rawBody: string, ts: string | undefined, sig: string | undefined): boolean {if (!ts || !sig) return falseif (Math.abs(Date.now() - Number(ts)) > FIVE_MINUTES) return falseconst expected = createHmac('sha256', SECRET).update(`v0:${ts}:${rawBody}`).digest('hex')if (sig.length !== expected.length) return falsereturn timingSafeEqual(Buffer.from(sig), Buffer.from(expected))}const app = express()app.post('/ligr', express.raw({ type: 'application/json' }), (req, res) => {const rawBody = req.body.toString('utf8')const ts = req.header('x-ligr-webhook-timestamp')const sig = req.header('x-ligr-webhook-sig')if (!verify(rawBody, ts, sig)) return res.status(401).end()const event = JSON.parse(rawBody)const key = `${event.entity}:${event.data.id}:${event.date}`if (seen.has(key)) return res.status(200).end()seen.add(key)res.status(200).end()setImmediate(() => handle(event))})function handle(event: { type: string; entity: string; competitionId: number; date: string; data: any }) {console.log(event.entity, event.type, event.data.id)}app.listen(3000)The handler answers before it does any work. A slow database write inside the request is the usual cause of a timeout.
-
Expose it over HTTPS and register it.
Deploy the receiver, or tunnel it for a test. Then in Developers → Webhooks, add the URL, select the competitions and the events you want, and save. Subscribe only to the events you handle. See Events.
-
Trigger a delivery.
Change something in a subscribed competition. A fact is the quickest: post one to a test match with
POST /v2/matches/{matchId}/facts, then delete it. See Score a match. Your receiver logsfact create, thenfact delete, and asummary updatebetween them. -
Read the log.
Open Developers → Webhooks, open the webhook, and select Event Logs. The log keeps the newest 50 attempts per webhook, with the status, the response body and the payload. A
0status means your endpoint sent no answer. A401from the receiver above means the signature failed. See Delivery health & logs.
Retries
Section titled “Retries”LIGR retries a delivery that gets no answer, or a 5xx, 408 or 429 answer. A fact or summary
event retries at once, then after 2, 4, 8 and 16 seconds. Any other event retries after 15 and
45 seconds. A restart or a deploy on your side is covered. An outage longer than a minute loses the
events of that window.
Read from the REST API once, then continue from the webhooks, when you start late or lost events:
| Situation | Read once |
|---|---|
| Your receiver comes up mid-match, or the webhook was enabled after kick-off | GET /v2/matches/{matchId} and GET /v2/matches/{matchId}/summary |
| Your endpoint was down for longer than a minute | The matches and summaries of every live competition |
The date on every event and the updatedAt on every entity tell you which side is newer.
Delivery is at least once. A retried delivery keeps the date of the event, so the seen set in
the receiver above drops a duplicate. It carries the entity as it is at retry time, and it can arrive after a newer event for
the same entity. Keep the delivery with the newest date.
Signature failures
Section titled “Signature failures”| Symptom | Cause | Fix |
|---|---|---|
| Every delivery fails verification | The body was parsed and re-serialized before the check | Read the raw body. Mount the raw parser before any JSON parser |
| Every delivery fails after a deploy | The secret changed, or the environment variable is missing | Copy the secret from the dashboard again |
| Some deliveries fail | Clock skew over five minutes | Sync the server clock. The timestamp is Unix time in milliseconds |
The full verification contract, with a Python version, is on Verify signatures.