Skip to content

Score a match from your system

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 and 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
    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
    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
    { "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
    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
    { "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
    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
    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
    {
    "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.

    periodNumberPeriod
    0Pre-match
    1First half
    2Half time
    3Second half
    4Full time

    Post a PERIOD_STARTED fact with the number of the period that starts. The match goes live and the clock starts.

    Terminal
    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
    { "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
    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
    { "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
    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
    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
    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
    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
    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
    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.

Pick your sport. The tabs stay on your choice across these docs.

nameEventWhodata
PERIOD_STARTEDA period starts——
PERIOD_FINISHEDA period ends——
GOALA goalteamId, playerId{ "shotOnTarget": true }
OWN_GOALAn own goal. Counts for the other teamteamId, playerId—
PENALTY_SCOREDA penalty scoredteamId, playerId—
PENALTY_MISSA penalty missedteamId, playerId{ "shotOnTarget": false }
YELLOW_CARDA yellow cardteamId, playerId—
SECOND_YELLOW_CARDA second yellow cardteamId, playerId—
RED_CARDA red cardteamId, playerId—
SUBSTITUTIONA substitution. Counts in substitutionsteamId, playerIdOptional { "off": 501, "on": 502 }, player ids. Swaps the players in the lineup
PARTIAL_SUBSTITUTIONSwaps two players in the lineup. Does not count in substitutionsteamId{ "off": 501, "on": 502 }, player ids
CORNER_WONA cornerteamId—
OFFSIDEAn offsideteamId, playerId—
FREE_KICK_CONCEDEDA foul that gives a free kick. Counts in foulsteamId, playerId—
PENALTY_CONCEDEDA foul that gives a penalty. Counts in foulsteamId, playerId—
FOULA foul. Does not change fouls: send FREE_KICK_CONCEDED or PENALTY_CONCEDEDteamId, playerId—
SAVEA saveteamId, playerId—
SHOT_ON_TARGET, SHOT_OFF_TARGET, SHOT_BLOCKEDA shotteamId, playerId—
CLOCK_STOPPED, CLOCK_STARTEDStop or start the clock inside a period——
SHOOTOUT_GOAL, SHOOTOUT_MISSA penalty shootout attemptteamId, playerId—

teamId is the id of the team from the competition. playerId is the id of the player in that team. See Teams and 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.

StatusMessageCause
400Could not find period 'N'periodNumber is not a period of this match
400Period has already startedA second PERIOD_STARTED for the current period
400Could not start period too far in the future. Next period is 'N'A PERIOD_STARTED that skips a period
400Invalid lineupA 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
404No fact found with id 'N'The fact does not belong to the match in the path
  • 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.