Guides: task walkthroughs: scoring, streams, webhooks and theme versions # Guides > Task-based walkthroughs for scoring, streams, webhooks, theme versions, code graphics, and native Rive graphics. A guide takes you from an empty terminal to a working result, with `curl` at every step. The [REST reference](/rest/) documents every endpoint. A guide tells you which ones to call, in which order, and what to keep from each answer. | Guide | What it covers | | ------------------------------------------------------------ | ---------------------------------------------------------------------------------- | | [Score a match from your system](/guides/score-a-match/) | Create a match, post the facts as the match runs, and close it | | [Automate a stream](/guides/automate-a-stream/) | Preview, go live, switch the overlay, pause and stop | | [Configure a stream](/guides/configure-a-stream/) | Create an input, save a destination, create the stream of a match and link them | | [Receive webhooks](/guides/receive-webhooks/) | Stand up an endpoint, verify the signature, answer fast, and read the delivery log | | [Publish a theme version](/guides/publish-a-theme-version/) | Read the theme, pin graphic versions, publish, and activate. Beta | | [Build a code graphic](/graphics-sdk/quick-start/) | From one HTML file to a graphic on an overlay. In the Graphics SDK section. Beta | | [Import a Rive graphic](/rive-graphics/import-through-rest/) | From a source project to a native preset on an overlay. Beta | Every guide uses a write key and `https://api.ligr.live/rest`. Set `LIGR_API_KEY` in your shell before you start. See [Your first request](/get-started/first-request/) if you have no key yet. Two shorter tasks have their own reference pages. * Fire a graphic on an overlay: [Graphics commands](/control-room/graphics-commands/) and [Presets](/control-room/presets/). * Push a spreadsheet or JSON into a theme: [External data sources](/control-room/data-sources/). # Automate a stream > Find the stream of a match, go live with or without destinations, switch the overlay, stop, and read the state at every step. A stream has an input, destinations and an overlay. Configure it in the dashboard, or over REST with [Configure a stream](/guides/configure-a-stream/). This guide drives a configured stream for one match. It needs a write key and a match that already has a stream. The endpoints are under [Streams](/rest/operations/tags/streams/) in the REST reference. 1. **Find the stream of the match.** Terminal ```bash curl 'https://api.ligr.live/rest/v2/matches/1188213?include=s,o' \ -H "Authorization: Bearer $LIGR_API_KEY" ``` 200 OK (excerpt) ```json { "id": 1188213, "streams": [{ "id": 90412, "name": "Main feed", "destinations": [{ "id": 3301, "name": "YouTube", "type": "youtube" }] }], "overlays": [{ "id": 2300003, "name": "Broadcast", "key": "…" }] } ``` `streams[].id` is the `streamSettingsId` every stream route takes. `overlays[].id` is the overlay you can switch to in step 4. 2. **Read the state.** Terminal ```bash curl 'https://api.ligr.live/rest/v1/streams/90412/state' \ -H "Authorization: Bearer $LIGR_API_KEY" ``` 200 OK ```json { "streamSettingsId": 90412, "state": "IDLE", "matchId": 1188213 } ``` An idle stream is ready for `GoLive`. 3. **Go live.** Terminal ```bash curl -X POST 'https://api.ligr.live/rest/v1/streams/90412/manage' \ -H "Authorization: Bearer $LIGR_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "command": "GoLive" }' ``` 200 OK ```json { "streamSettingsId": 90412, "state": "CREATING", "matchId": 1188213, "transitioning": true } ``` `GoLive` starts the stream. Without `startDestinationsImmediately` it publishes nowhere: the picture runs, and an operator starts the destinations from the dashboard when it is right. That is the preview. To publish at once, list the destination ids to start: Go live and publish to one destination at once ```json { "command": "GoLive", "startDestinationsImmediately": [3301] } ``` Poll `GET …/state` every 5 seconds until `state` reads `ONLIVE`. The transition takes tens of seconds. `ERROR` means the start failed. Open the stream in the dashboard to read the cause. 4. **Switch the overlay.** Terminal ```bash curl -X POST 'https://api.ligr.live/rest/v1/streams/90412/overlay/change' \ -H "Authorization: Bearer $LIGR_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "overlayId": 2300003 }' ``` Send `"overlayId": null` to unlink the overlay. `…/overlay/show` and `…/overlay/hide` toggle the linked overlay without unlinking it. `…/overlay/refresh` restarts it. 5. **Stop.** Terminal ```bash curl -X POST 'https://api.ligr.live/rest/v1/streams/90412/manage' \ -H "Authorization: Bearer $LIGR_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "command": "StopStream" }' ``` 200 OK ```json { "streamSettingsId": 90412, "state": "TRANSIT_TO_STOPPED", "matchId": 1188213, "transitioning": true } ``` Poll `GET …/state` until `state` reads `IDLE`. The stream is then ready for the next `GoLive`. ## States [Section titled “States”](#states) `GET …/state` returns one of these values. | `state` | Meaning | | -------------------- | -------------------------------------------------------------- | | `IDLE` | No stream is running. `GoLive` is allowed | | `CREATING` | `GoLive` was accepted and the infrastructure is starting | | `ONLIVE` | The stream is live. `StopStream` is allowed | | `TRANSIT_TO_STOPPED` | `StopStream` was accepted and the infrastructure is stopping | | `ERROR` | The start or the stop failed. Open the stream in the dashboard | The manage answer adds `transitioning`. It is `true` for `CREATING` and `TRANSIT_TO_STOPPED`. A command sent while the stream is transitioning is refused with 400. The state endpoint does not carry `transitioning`. Poll on `state`. ## Commands [Section titled “Commands”](#commands) | `command` | Effect | | ------------ | ----------------------------------------------------------------------------------------------------------------- | | `GoLive` | Start the stream. Allowed from `IDLE`. `startDestinationsImmediately` picks the destinations that publish at once | | `StopStream` | Stop the stream. Allowed from `ONLIVE` | There is no pause. Stop the stream and go live again. There is no separate preview command. `GoLive` without destinations is the preview. ## Polling [Section titled “Polling”](#polling) Poll `GET …/state` every 5 seconds while a transition is in progress. Stop polling when the stream reaches the state you wait for. See [Rate limits](/get-started/rate-limits/). ## Errors [Section titled “Errors”](#errors) | Status | Cause | | ------ | ---------------------------------------------------------------------------------- | | 400 | The command is not allowed from the current state, or the stream is transitioning | | 403 | A read key on `…/manage` or an overlay route. Use a write key | | 404 | The `streamSettingsId` belongs to another organization, or the match has no stream | See [Streams](/rest/operations/tags/streams/) in the REST reference. # Configure a stream > Create an input, save a destination, create the stream of a match with both linked, read it back, and change it. Everything the stream settings page does, over REST. A stream needs three things: an input LIGR reads the picture from, one or more destinations it publishes to, and the match it belongs to. This guide creates all three over REST and links them. It needs a write key with the `streams` scope, and a match id. The endpoints are under [Streams](/rest/operations/tags/streams/) in the REST reference. 1. **Create an input.** LIGR generates the host, the application name and the stream key. Send your picture to `rtmpUrl` with `streamKey` as the stream key, or to `srtUrl`. Terminal ```bash curl -X POST 'https://api.ligr.live/rest/v1/streams/inputs' \ -H "Authorization: Bearer $LIGR_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "OB van 1", "latencyMs": 1000 }' ``` 201 Created ```json { "id": 412, "name": "OB van 1", "type": "default", "host": "a1b2c3d4.stream.ligr.live", "applicationName": "en123", "streamKey": "e5f6a7b8c9d0e1f2", "rtmpUrl": "rtmp://a1b2c3d4.stream.ligr.live/en123", "srtUrl": "srt://a1b2c3d4.stream.ligr.live:10080?streamid=#!::r=en123/e5f6a7b8c9d0e1f2,m=publish", "sourceUrl": null, "latencyMs": 1000, "createdAt": "2026-09-12T08:00:00.000Z", "updatedAt": "2026-09-12T08:00:00.000Z" } ``` `latencyMs` is the receive latency of the input. It absorbs network jitter and adds the same delay to the picture. It is one of 20, 50, 100, 150, 250, 350, 500, 1000, 1500, 2000, 2500 or 3000. For a source LIGR pulls instead, send `"type": "pull"` and `sourceUrl`. An input is reusable. Create one per camera feed, not one per match. 2. **Save a destination.** Terminal ```bash curl -X POST 'https://api.ligr.live/rest/v1/streams/destinations' \ -H "Authorization: Bearer $LIGR_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "YouTube main", "type": "youtube", "url": "rtmp://a.rtmp.youtube.com/live2", "key": "abcd-efgh-ijkl-mnop" }' ``` 201 Created ```json { "id": 3301, "name": "YouTube main", "type": "youtube", "transportDirection": "push", "transportProtocol": "rtmp", "feedType": "program", "url": "rtmp://a.rtmp.youtube.com/live2", "key": "abcd-efgh-ijkl-mnop", "pullUrl": null, "singleUse": false, "createdAt": "2026-09-12T08:01:00.000Z", "updatedAt": "2026-09-12T08:01:00.000Z" } ``` `type` is one of `youtube`, `facebook`, `twitter`, `linkedin`, `tiktok`, `twitch`, `custom` and the partner platforms. `custom` takes any RTMP or SRT server. An `srt://` url makes the protocol SRT. `feedType` is `program` with graphics, or `clean` without. A pull destination has no url and key. Send `"transportDirection": "pull"` and LIGR answers with `pullUrl`, the URL a player or a relay pulls from. A destination is reusable too. `GET /v1/streams/destinations` lists what the organization has, one-off destinations included. 3. **Create the stream.** Terminal ```bash curl -X POST 'https://api.ligr.live/rest/v1/streams' \ -H "Authorization: Bearer $LIGR_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "matchId": 1188213, "name": "Main feed", "resolution": "1920 * 1080", "fps": "50", "region": "ap-southeast-2", "inputId": 412, "destinationIds": [3301], "overlayId": 2300003 }' ``` 201 Created (excerpt) ```json { "id": 90412, "name": "Main feed", "matchId": 1188213, "competitionId": 2311, "overlayId": 2300003, "resolution": "1920 * 1080", "fps": "50", "region": "ap-southeast-2", "size": "regular", "inputId": 412, "inputSelection": "specific", "destinationSelection": "specific", "destinations": [{ "id": 3301, "name": "YouTube main", "type": "youtube", "url": "rtmp://a.rtmp.youtube.com/live2", "key": "abcd-efgh-ijkl-mnop" }], "hideOverlay": false, "autoStream": false, "generateHighlights": false, "archiveFullFeed": false } ``` The stream takes its competition from the match. `matchId`, `resolution`, `fps` and `region` are required. Everything else has a default. `region` is the AWS region the stream runs in. Pick the one nearest the input, for example `ap-southeast-2` for Sydney or `eu-west-2` for London. `overlayId` is an overlay of the same match, from `GET /v2/matches/{id}?include=o`. The `id` in the answer is the `streamSettingsId` every lifecycle route takes. Start the stream with `POST /v1/streams/90412/start`. See [Automate a stream](/guides/automate-a-stream/). 4. **Read it back, and change it.** Terminal ```bash curl 'https://api.ligr.live/rest/v1/streams?matchId=1188213' -H "Authorization: Bearer $LIGR_API_KEY" curl 'https://api.ligr.live/rest/v1/streams/90412' -H "Authorization: Bearer $LIGR_API_KEY" ``` An update sends only the fields that change. Add a second destination and turn on highlights: Terminal ```bash curl -X POST 'https://api.ligr.live/rest/v1/streams/90412' \ -H "Authorization: Bearer $LIGR_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "destinationIds": [3301, 3302], "generateHighlights": true, "archiveFullFeed": true }' ``` While the stream runs, only these fields can change: `autoStream`, `autoStreamConfig`, the three times, `generateHighlights`, `publishHighlights` and `destinationIds`. Turning `generateHighlights` on while the stream runs needs `archiveFullFeed` already on. Any other change answers 400. Stop the stream first. 5. **Delete what you no longer need.** Terminal ```bash curl -X DELETE 'https://api.ligr.live/rest/v1/streams/90412' -H "Authorization: Bearer $LIGR_API_KEY" ``` A running stream, an input in use by a running stream, and a destination linked to a running stream all refuse to delete. Stop the stream first. ## Automatic input and destinations [Section titled “Automatic input and destinations”](#automatic-input-and-destinations) A stream can pick its input and destinations from the match instead of fixed ids. Send `"inputSelection": "home"` to use the input assigned to the home team, or `"venue"` for the one assigned to the venue of the match. `destinationSelection` works the same way. Assign inputs and destinations to teams and venues in the dashboard. A stream created with fixed ids uses `specific`, and an update that sends `inputId` or `destinationIds` moves it back to `specific`. ## Automation [Section titled “Automation”](#automation) | Field | Meaning | | ------------------------ | ------------------------------------------------------------------------------- | | `autoStream` | Start and stop at the automation times of the match | | `autoStreamConfig` | Per trigger, `preview`, `goLive` and `stop`, a `mode` of `schedule` or `venues` | | `streamPreviewStartTime` | Minutes relative to the scheduled start of the match | | `streamLiveStartTime` | Minutes relative to the scheduled start of the match | | `streamAutoStopTime` | Minutes relative to the time the match stopped | ## Recording and highlights [Section titled “Recording and highlights”](#recording-and-highlights) | Field | Meaning | | ---------------------- | -------------------------------------------------------------- | | `archiveFullFeed` | Keep the recording of the program feed | | `archiveCleanFeed` | Keep the recording of the clean feed | | `autoConvertRecording` | Convert the recording after the match. Needs `archiveFullFeed` | | `generateHighlights` | Cut highlights from the facts of the match | | `publishHighlights` | Publish the highlights as they are cut | ## Errors [Section titled “Errors”](#errors) | Status | Cause | | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | A change the running stream does not allow, a push destination without url and key, a pull input without `sourceUrl`, a latency outside the list, or a delete of something in use | | 403 | A key without the `streams:write` scope | | 404 | The match, stream, input or destination belongs to another organization | # Publish a theme version > Read the theme, pick the graphic versions the new theme version pins, publish it, and activate it at the right moment. Beta Graphics creation is in beta. See [Beta status](/graphics-sdk/#beta-status). A theme version is a frozen list of graphic versions. An overlay loads the active theme version when its page loads. This guide publishes a theme version, inspects its contents, and activates that exact version. It needs a write key and a theme id. 1. **Read the theme.** Terminal ```bash curl 'https://api.ligr.live/rest/v2/themes/203' \ -H "Authorization: Bearer $LIGR_API_KEY" ``` 200 OK ```json { "id": 203, "name": "Match Day", "activeVersion": 11, "versions": [9, 10, 11], "graphics": [ { "graphicId": "8efdf988-1f4d-43ac-873b-06b9b4f3e379", "name": "Scorebug", "type": "code", "workingVersion": 4, "publishedVersions": [1, 2, 3] }, { "graphicId": "c1e2a9d0-5b7e-4f60-9c3a-2d1f0e8b7a64", "name": "Lower third", "type": "rive", "workingVersion": 7, "publishedVersions": [5, 6] } ] } ``` `activeVersion` is what an overlay loads on its next page load. Each graphic lists its `publishedVersions`. The `workingVersion` is the editable copy. A theme version can pin published versions only. 2. **Decide what the new version pins.** The request pins every graphic you list at the version you give. Every graphic you do not list is pinned at its latest published version. A graphic with no published version is left out. Publish a graphic version first if the change you want is still in a working copy. For a code graphic, see [Push and publish](/graphics-sdk/push-and-publish/). For a Rive graphic, use [Import through REST](/rive-graphics/import-through-rest/#6-publish-snapshot-and-activate). 3. **Publish the theme version.** Terminal ```bash curl -X POST 'https://api.ligr.live/rest/v2/themes/203/versions' \ -H "Authorization: Bearer $LIGR_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "graphics": [ { "graphicId": "c1e2a9d0-5b7e-4f60-9c3a-2d1f0e8b7a64", "version": 5 } ] }' ``` 201 Created ```json { "version": 12, "activeVersion": 11 } ``` This pins the lower third at version 5 and the scorebug at its latest, version 3. The active version is still 11. Nothing changed on air. With the CLI, run `npx ligr-graphic theme publish --theme 203`. It sends no `graphics` list, so every graphic takes its latest published version. Add `--dry-run` to print the selection first, and `--activate` to set the new version active. 4. **Inspect the published version.** Terminal ```bash curl 'https://api.ligr.live/rest/v2/themes/203/versions/12' \ -H "Authorization: Bearer $LIGR_API_KEY" ``` The response lists the frozen graphic version selections. It includes the lower third at v5, regardless of later graphic publications. With the CLI, run `npx ligr-graphic theme inspect --theme 203 --version 12`. 5. **Activate that version.** Select the existing version when the broadcast allows it. This request does not publish a new version or change its contents. Terminal ```bash curl -X PUT 'https://api.ligr.live/rest/v2/themes/203/active-version' \ -H "Authorization: Bearer $LIGR_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "version": 12 }' ``` 200 OK ```json { "activeVersion": 12 } ``` With the CLI, run `npx ligr-graphic theme activate --theme 203 --version 12`. All competitions using this theme share its active version. Existing overlay pages keep their loaded version until reloaded. Reload the preview overlay and check the result before reloading a broadcast browser source. Caution Activate versions and reload broadcast sources only when the operator is ready for the change. The customer dashboard does not expose theme version activation; use the CLI or REST. ## Roll back [Section titled “Roll back”](#roll-back) Read the theme’s `versions` list, then inspect the old version with `GET /v2/themes/203/versions/11`. Activate that existing snapshot to restore its exact graphic selection, including the absence of graphics added later. Terminal ```bash npx ligr-graphic theme inspect --theme 203 --version 11 npx ligr-graphic theme activate --theme 203 --version 11 ``` Reload the preview overlay to verify the rollback. Reload broadcast sources when the operator is ready. Presets for graphics absent from the selected snapshot are hidden. They return if you activate a snapshot containing those graphics. ## Errors [Section titled “Errors”](#errors) | Status | Cause | | ------ | ---------------------------------------------------------------------------------------- | | 400 | A `version` is not a published version of that graphic, or `version` is under 1 | | 403 | A read key. Use a write key | | 404 | The theme belongs to another organization, or the requested theme version does not exist | | 409 | A graphic in the theme is locked. `holder.userName` names the editor. Wait, then retry | See [Themes](/rest/operations/tags/themes/) in the REST reference. # Receive webhooks > Stand up an endpoint, verify every delivery, answer within the timeout, process after you answer, and handle retries. 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. 1. **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. 2. **Write the receiver.** Read the raw body before anything parses it. The signature covers the bytes LIGR sent. server.ts ```ts import { createHmac, timingSafeEqual } from 'node:crypto' import express from 'express' const SECRET = process.env.LIGR_WEBHOOK_SECRET! const FIVE_MINUTES = 5 * 60_000 const seen = new Set() function verify(rawBody: string, ts: string | undefined, sig: string | undefined): boolean { if (!ts || !sig) return false if (Math.abs(Date.now() - Number(ts)) > FIVE_MINUTES) return false const expected = createHmac('sha256', SECRET).update(`v0:${ts}:${rawBody}`).digest('hex') if (sig.length !== expected.length) return false return 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. 3. **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](/webhooks/events/). 4. **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](/guides/score-a-match/). Your receiver logs `fact create`, then `fact delete`, and a `summary update` between them. 5. **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 `0` status means your endpoint sent no answer. A `401` from the receiver above means the signature failed. See [Delivery health & logs](/webhooks/delivery-health/). ## Retries [Section titled “Retries”](#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](/rest/) 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”](#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](/webhooks/verify-signatures/). # Score a match from your system > Create a match, set the lineup, start the periods, post goals and cards as they happen, correct a mistake, and close the match. Football, with curl at every step. This guide connects a scoring system to LIGR for one football match. Your system creates the match, posts a fact for every event, and LIGR computes the score, the clock, the statistics and the graphics from those facts. It needs a write key and about half an hour. The endpoints are under [Matches](/rest/operations/tags/matches/) and [Facts](/rest/operations/tags/facts/) in the REST reference. 1. **Find the ids you need.** A match belongs to a competition, takes place at a venue, and has two teams. Read them once and keep the ids. Terminal ```bash curl 'https://api.ligr.live/rest/v1/competitions' -H "Authorization: Bearer $LIGR_API_KEY" curl 'https://api.ligr.live/rest/v1/competitions/2311/teams' -H "Authorization: Bearer $LIGR_API_KEY" curl 'https://api.ligr.live/rest/v1/venues' -H "Authorization: Bearer $LIGR_API_KEY" ``` 2. **Create the match.** Terminal ```bash curl -X POST 'https://api.ligr.live/rest/v2/matches' \ -H "Authorization: Bearer $LIGR_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "Sydney FC v Melbourne City", "competitionId": 2311, "venueId": 764, "date": "2026-09-12T09:30:00.000Z", "competitorsType": "teams", "competitors": [ { "entityId": 1, "entityType": "team", "meta": { "isHome": true } }, { "entityId": 2, "entityType": "team", "meta": { "isHome": false } } ] }' ``` 201 Created ```json { "id": 1188213, "overlays": [], "streams": [] } ``` `competitorsType` is the match format: `singles`, `doubles`, `teams` or `group`. `entityType` is the kind of each row in `competitors`: `team` or `player`. For example, a doubles match has `competitorsType: doubles` and four `entityType: player` rows. A group match also has `player` rows. Keep the `id`. To add an overlay or a stream to the match, use `POST /v1/overlays` or `POST /v1/streams`. 3. **Set the lineup.** Send the starting players and the substitutes of each team. The graphics use the lineup to show the teams, list the scorers and fill the player variables. Without a lineup, these stay empty. Each lineup player must be on the roster of the team. Read the roster with `GET /v1/teams/{teamId}/players`. If a player is not on the roster, add the player first. This call needs the `teams:write` scope. Terminal ```bash curl -X POST 'https://api.ligr.live/rest/v1/teams/1/players' \ -H "Authorization: Bearer $LIGR_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "firstName": "Adam", "lastName": "Le Fondre", "number": "9", "position": "Forward", "positionType": "FW" }' ``` 201 Created ```json { "id": 501, "firstName": "Adam", "lastName": "Le Fondre", "gfxFirstName": null, "gfxLastName": null, "position": "Forward", "positionType": "FW", "number": "9", "captain": false, "starter": false, "bench": false } ``` Keep the `id`. It is the `playerId` in the lineup. Do not use a player of another team as a substitute for a missing player. Add the missing player to the roster. If a name is too long for the graphics, set shorter graphics names on the roster entry. Use `PATCH /v1/teams/{teamId}/players/{playerId}` with the `teams:write` scope. The same call changes the squad number, the position and the default starter flags. Terminal ```bash curl -X PATCH 'https://api.ligr.live/rest/v1/teams/1/players/501' \ -H "Authorization: Bearer $LIGR_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "gfxFirstName": "Bul", "gfxLastName": "Juach" }' ``` If a data provider supplies the player, the next sync replaces the names, the number and the position. The sync does not change `gfxFirstName` and `gfxLastName`. Then send the lineup. Terminal ```bash curl -X PUT 'https://api.ligr.live/rest/v2/matches/1188213/lineup' \ -H "Authorization: Bearer $LIGR_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "teams": [ { "teamId": 1, "starting": [{ "playerId": 501, "captain": true }, { "playerId": 503, "number": "14" }], "bench": [{ "playerId": 502 }] }, { "teamId": 2, "starting": [{ "playerId": 617 }], "bench": [] } ] }' ``` 200 OK ```json { "lineup": [ { "id": 501, "teamId": 1, "firstName": "Adam", "lastName": "Le Fondre", "number": "9", "position": "Forward", "positionType": "FW", "starter": true, "captain": true, "formationNumber": null, "lineupPosition": null } ], "count": 4 } ``` The example answer shows one of the four players. A field you leave out, such as `number`, takes the roster value. The call replaces the full lineup of each team you send. LIGR removes a player you leave out. A team you leave out keeps its lineup. If you send the same body again, nothing changes. Open overlays show the new lineup immediately. 4. **Start the first half.** A football match has these periods. Period `0` exists before kick-off. | `periodNumber` | Period | | -------------- | ----------- | | `0` | Pre-match | | `1` | First half | | `2` | Half time | | `3` | Second half | | `4` | Full time | Post a `PERIOD_STARTED` fact with the number of the period that starts. The match goes live and the clock starts. Terminal ```bash curl -X POST 'https://api.ligr.live/rest/v2/matches/1188213/facts' \ -H "Authorization: Bearer $LIGR_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "PERIOD_STARTED", "periodNumber": 1 }' ``` 201 Created ```json { "id": 56148731, "matchId": 1188213, "name": "PERIOD_STARTED", "periodNumber": 1, "createdAt": "2026-09-12T09:30:02.114Z" } ``` Periods start in order. A `PERIOD_STARTED` for a period that is not the next one returns 400. 5. **Post the events.** Every event is one fact. `name` is the event, `periodNumber` is the period it happened in, `teamId` and `playerId` name who did it, and `data` carries the sport-specific detail. A goal ```bash curl -X POST 'https://api.ligr.live/rest/v2/matches/1188213/facts' \ -H "Authorization: Bearer $LIGR_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "GOAL", "periodNumber": 1, "teamId": 1, "playerId": 501, "data": { "shotOnTarget": true } }' ``` 201 Created ```json { "id": 56148902, "matchId": 1188213, "name": "GOAL", "periodNumber": 1, "createdAt": "2026-09-12T09:52:17.640Z" } ``` Keep the `id` of every fact you post. You need it to correct or delete the fact later. A yellow card ```bash curl -X POST 'https://api.ligr.live/rest/v2/matches/1188213/facts' \ -H "Authorization: Bearer $LIGR_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "YELLOW_CARD", "periodNumber": 1, "teamId": 2, "playerId": 617 }' ``` `date` defaults to the time LIGR receives the fact. Send it when your system timestamps the event, so the match minute is right. LIGR derives the minute from `date` and the period start. 6. **End the half, start the second.** Terminal ```bash curl -X POST 'https://api.ligr.live/rest/v2/matches/1188213/facts' \ -H "Authorization: Bearer $LIGR_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "PERIOD_FINISHED", "periodNumber": 1 }' curl -X POST 'https://api.ligr.live/rest/v2/matches/1188213/facts' \ -H "Authorization: Bearer $LIGR_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "PERIOD_STARTED", "periodNumber": 3 }' ``` `PERIOD_FINISHED` takes the number of the period that ends. It stops the clock and moves the match into half time, period `2`. `PERIOD_STARTED` with `3` starts the second half. 7. **Correct a mistake.** Update a fact to change who scored or when. Delete a fact that never happened. LIGR recomputes the score and the statistics. Use the `id` from the answer of step 5, here the goal. The goal was scored by another player ```bash curl -X POST 'https://api.ligr.live/rest/v2/matches/1188213/facts/56148902' \ -H "Authorization: Bearer $LIGR_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "playerId": 502 }' ``` The goal was disallowed ```bash curl -X DELETE 'https://api.ligr.live/rest/v2/matches/1188213/facts/56148902' \ -H "Authorization: Bearer $LIGR_API_KEY" ``` An update changes only the fields you send. A period fact can be deleted only while it is the most recent one. 8. **Close the match.** Post `PERIOD_FINISHED` for the last period. The match moves to full time. To end a match early, call `POST /v2/matches/{matchId}/finish`. To cancel or abandon it, call `…/cancel` or `…/abandon`. Terminal ```bash curl -X POST 'https://api.ligr.live/rest/v2/matches/1188213/facts' \ -H "Authorization: Bearer $LIGR_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "PERIOD_FINISHED", "periodNumber": 3 }' ``` 9. **Read back the result.** Terminal ```bash curl 'https://api.ligr.live/rest/v2/matches/1188213/summary' \ -H "Authorization: Bearer $LIGR_API_KEY" ``` The summary is the score, the periods, the clock and the statistics that LIGR folded from your facts. Compare it to your own state after every match. ## Fact names by sport [Section titled “Fact names by sport”](#fact-names-by-sport) Pick your sport. The tabs stay on your choice across these docs. * Football | `name` | Event | Who | `data` | | --------------------------------------------------- | -------------------------------------------------------------------------------- | -------------------- | --------------------------------------------------------------------------------- | | `PERIOD_STARTED` | A period starts | — | — | | `PERIOD_FINISHED` | A period ends | — | — | | `GOAL` | A goal | `teamId`, `playerId` | `{ "shotOnTarget": true }` | | `OWN_GOAL` | An own goal. Counts for the other team | `teamId`, `playerId` | — | | `PENALTY_SCORED` | A penalty scored | `teamId`, `playerId` | — | | `PENALTY_MISS` | A penalty missed | `teamId`, `playerId` | `{ "shotOnTarget": false }` | | `YELLOW_CARD` | A yellow card | `teamId`, `playerId` | — | | `SECOND_YELLOW_CARD` | A second yellow card | `teamId`, `playerId` | — | | `RED_CARD` | A red card | `teamId`, `playerId` | — | | `SUBSTITUTION` | A substitution. Counts in `substitutions` | `teamId`, `playerId` | Optional `{ "off": 501, "on": 502 }`, player ids. Swaps the players in the lineup | | `PARTIAL_SUBSTITUTION` | Swaps two players in the lineup. Does not count in `substitutions` | `teamId` | `{ "off": 501, "on": 502 }`, player ids | | `CORNER_WON` | A corner | `teamId` | — | | `OFFSIDE` | An offside | `teamId`, `playerId` | — | | `FREE_KICK_CONCEDED` | A foul that gives a free kick. Counts in `fouls` | `teamId`, `playerId` | — | | `PENALTY_CONCEDED` | A foul that gives a penalty. Counts in `fouls` | `teamId`, `playerId` | — | | `FOUL` | A foul. Does not change `fouls`: send `FREE_KICK_CONCEDED` or `PENALTY_CONCEDED` | `teamId`, `playerId` | — | | `SAVE` | A save | `teamId`, `playerId` | — | | `SHOT_ON_TARGET`, `SHOT_OFF_TARGET`, `SHOT_BLOCKED` | A shot | `teamId`, `playerId` | — | | `CLOCK_STOPPED`, `CLOCK_STARTED` | Stop or start the clock inside a period | — | — | | `SHOOTOUT_GOAL`, `SHOOTOUT_MISS` | A penalty shootout attempt | `teamId`, `playerId` | — | `teamId` is the id of the team from the competition. `playerId` is the id of the player in that team. See [Teams](/rest/operations/tags/teams/) and [Players](/rest/operations/tags/players/). `fouls` is the sum of free kicks conceded and penalties conceded. Possession, passes, tackles and other Opta-only statistics come only from a data provider feed. No fact sets them. The endpoint does not check `name` against this list. A name LIGR does not know is stored and drives nothing: no score, no statistic, no graphic. Check your spelling against the table. * Tennis **Coming soon.** The Tennis fact name reference is not written yet. Until then, ask your LIGR contact for the fact names of Tennis. A name LIGR does not know is stored and drives nothing. * Basketball **Coming soon.** The Basketball fact name reference is not written yet. Until then, ask your LIGR contact for the fact names of Basketball. A name LIGR does not know is stored and drives nothing. * Australian Rules **Coming soon.** The Australian Rules fact name reference is not written yet. Until then, ask your LIGR contact for the fact names of Australian Rules. A name LIGR does not know is stored and drives nothing. * Rugby League **Coming soon.** The Rugby League fact name reference is not written yet. Until then, ask your LIGR contact for the fact names of Rugby League. A name LIGR does not know is stored and drives nothing. * Rugby Union **Coming soon.** The Rugby Union fact name reference is not written yet. Until then, ask your LIGR contact for the fact names of Rugby Union. A name LIGR does not know is stored and drives nothing. * Cricket **Coming soon.** The Cricket fact name reference is not written yet. Until then, ask your LIGR contact for the fact names of Cricket. A name LIGR does not know is stored and drives nothing. * Netball **Coming soon.** The Netball fact name reference is not written yet. Until then, ask your LIGR contact for the fact names of Netball. A name LIGR does not know is stored and drives nothing. * More sports **Coming soon.** The fact name reference for these sports is not written yet. The sport key is in brackets. * Baseball (`baseball`) * Field hockey (`fieldHockey`) * Futsal (`futsal`) * American Football (`gridiron`) * Handball (`handBall`) * Ice hockey (`iceHockey`) * Lacrosse (`lacrosse`) * Rugby Sevens (`rugbySevens`) * Touch football (`touchFootball`) * Volleyball (`volleyball`) * Water polo (`waterPolo`) Ask your LIGR contact for the fact names of these sports. ## Errors [Section titled “Errors”](#errors) | Status | Message | Cause | | ------ | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- | | 400 | `Could not find period 'N'` | `periodNumber` is not a period of this match | | 400 | `Period has already started` | A second `PERIOD_STARTED` for the current period | | 400 | `Could not start period too far in the future. Next period is 'N'` | A `PERIOD_STARTED` that skips a period | | 400 | `Invalid lineup` | A lineup player is not on the team roster, or a team does not play in the match. `details` names each bad value | | 403 | — | A read key. Use a write key | | 404 | — | The match belongs to another organization | | 404 | `No fact found with id 'N'` | The fact does not belong to the match in the path | ## Keep LIGR in sync [Section titled “Keep LIGR in sync”](#keep-ligr-in-sync) * Post every fact once. LIGR does not deduplicate facts. * Post facts in the order they happened. The summary is folded in fact order. * Subscribe a webhook to `fact` and `summary` events to confirm what LIGR computed. See [Receive webhooks](/guides/receive-webhooks/).