# Get team placements

```http
GET /api/public/v1/competitions/igp24/leaderboard/teams/{teamId}/placements
```

**Scope**: `competition.read`.

Returns the cursor-paginated placement breakdown for one participant or LMFDB baseline row in the frozen official IGP24 leaderboard. It never includes post-event validation, Discovery-only records, submitted polynomial coefficients, or private member identities.

Before leaderboard publication, the endpoint returns an unpublished empty page without confirming whether `teamId` exists. After publication, `entry` identifies the matching public leaderboard row and holds the consent-controlled team-member projection once, rather than repeating it on every placement.

## Path parameter

| Parameter | Type | Description |
| :--- | :--- | :--- |
| `teamId` | string | Opaque `teamId` from a participant or baseline item returned by [Get leaderboard](./get-leaderboard.md). |

## Query parameters

| Parameter | Type | Required | Default | Description |
| :--- | :--- | :---: | :--- | :--- |
| `cursor` | string | No | — | Opaque cursor returned by the previous page. |
| `limit` | integer | No | `25` | Page size from `1` through `100`. |

Pass `nextCursor` unchanged to the same endpoint with the same `teamId`. Do not decode, modify, construct, or reuse it for another team or request flow.

## Publication and pagination

- `meta.published: false` is an information boundary: `entry` is `null`, `placements` is empty, `nextCursor` is `null`, `totalPlacements` is `0`, and `meta.publishedAt` is `null`.
- When results are published, `entry`, placement scores, discriminant values, scoring inputs, total count, and placement order come from the same frozen official result. Post-event processing cannot change them.
- Placements are ordered by unrounded canonical `points` descending, then `t` and `r` ascending. The response rounds `points` to six decimal places; clients must preserve the returned order rather than sorting by the displayed value.
- A continuation cursor is bound to the published result and `teamId`. Replaying a page returns the same frozen placement rows in the same order.
- Current `entry.teamName`, `entry.teamNumber`, `entry.teamDetails`, and the reference-only `isSolvable` value may be corrected without changing placement order or cursor meaning.

The sum of rounded `placements[].points` may differ slightly from the rounded aggregate `entry.score`. Use the leaderboard's `entry.score` and `entry.rank`; do not reconstruct either value from this response.

The endpoint remains readable while IGP24 is publicly visible, including after its status changes to closed. After publication, a `teamId` that is not a public participant or baseline row returns `404 NOT_FOUND` instead of an indistinguishable empty result.

## Response fields

### Data

| Field | Type | Description |
| :--- | :--- | :--- |
| `entry` | [Leaderboard entry](./get-leaderboard.md#igp24leaderboardentry) \| null | Matching public participant or baseline row. It is `null` only while results are unpublished. |
| `placements` | [`Igp24Placement`](#igp24placement)[] | Current page in frozen official placement order. |
| `nextCursor` | string \| null | Opaque cursor for the next page, or `null` when traversal is complete. |
| `totalPlacements` | integer | Frozen number of official placements held by this leaderboard row across all pages. |
| `meta.published` | boolean | Whether the frozen official leaderboard is public. |
| `meta.publishedAt` | string \| null | ISO 8601 UTC publication time; non-null exactly when `meta.published` is `true`. |

`entry.teamDetails` follows the shared [privacy rules](../../competitions.md#leaderboard-team-details-and-privacy). It is always `null` for the baseline row and may be `null` for a participant row. Placements do not repeat this display projection.

### `Igp24Placement`

| Field | Type | Description |
| :--- | :--- | :--- |
| `t` | integer | Transitive group index. |
| `r` | integer | Real-root count/signature. |
| `label` | string | Transitive group label, equal to `24T{t}`. |
| `points` | number | Frozen official point contribution for this pair, rounded to six decimal places. |
| `kTeams` | integer | Scoring-population size `k` used by the official pair formula. |
| `scoringDiscAbs` | string \| null | This row's scoring value as a decimal string. |
| `minScoringDiscAbs` | string \| null | Minimum participant scoring value for the pair under the same `discSource`. |
| `discSource` | `exact_nfdisc` \| `mixed_disc` \| null | Interpretation of both scoring-discriminant fields. |
| `isSolvable` | boolean \| null | Current reference solvability flag for `label`, or `null` when unavailable. It does not affect the frozen score. |
| `baselineUnlocked` | boolean | Whether the LMFDB reference participated in this pair's official scoring population. |
| `baselineDiscAbs` | string \| null | Exact absolute LMFDB reference discriminant, or `null` when none applies. |

Scoring-value rules:

- `kTeams` counts credited participant teams and, when `baselineUnlocked` is `true`, the LMFDB reference.
- `exact_nfdisc` means the two scoring values are exact absolute field discriminants. `mixed_disc` means they are official scoring upper bounds, not exact field discriminants.
- For participant rows, `scoringDiscAbs`, `minScoringDiscAbs`, and `discSource` are non-null. A baseline reference may use `null` when no value is available.
- Decimal discriminants must remain strings; converting them to JavaScript numbers can lose integer precision.

## Example request

```bash
curl "https://api.sair.foundation/api/public/v1/competitions/igp24/leaderboard/teams/teamv2_a7a863ce80e6447881ad068bab82498e/placements?limit=25" \
  -H "Authorization: Bearer $SAIR_API_KEY"
```

## Published response

```json
{
  "ok": true,
  "data": {
    "entry": {
      "rank": 7,
      "entryKind": "participant",
      "teamId": "teamv2_a7a863ce80e6447881ad068bab82498e",
      "teamNumber": "IGP24-T00007",
      "teamName": "Galois Wranglers",
      "score": 4.7321,
      "scoreablePairs": 12,
      "teamDetails": {
        "members": [
          {
            "name": "Alice Example"
          }
        ]
      }
    },
    "placements": [
      {
        "t": 105,
        "r": 12,
        "label": "24T105",
        "points": 1,
        "kTeams": 2,
        "scoringDiscAbs": "100",
        "minScoringDiscAbs": "100",
        "discSource": "exact_nfdisc",
        "isSolvable": true,
        "baselineUnlocked": true,
        "baselineDiscAbs": "120"
      }
    ],
    "nextCursor": "WzEsInRlYW12Ml9hN2E4NjNjZTgwZTY0NDc4ODFhZDA2OGJhYjgyNDk4ZSIsMSwxMDUsMTJd",
    "totalPlacements": 12,
    "meta": {
      "published": true,
      "publishedAt": "2026-08-18T12:00:00Z"
    }
  }
}
```

## Unpublished response

```json
{
  "ok": true,
  "data": {
    "entry": null,
    "placements": [],
    "nextCursor": null,
    "totalPlacements": 0,
    "meta": {
      "published": false,
      "publishedAt": null
    }
  }
}
```

## Errors

| HTTP | Code | When |
| :---: | :--- | :--- |
| `400` | `MALFORMED_BODY` | `cursor` is invalid or belongs to another result or team, or `limit` is outside `1` through `100`. |
| `404` | `NOT_FOUND` | IGP24 is not publicly visible as active or closed, or published results contain no public participant or baseline row for `teamId`. |
| `503` | `IGP24_SERVICE_UNAVAILABLE` | The frozen official leaderboard service is temporarily unavailable. |

See [Errors](../../../errors.md) for authentication, scope, rate-limit, and standard error-envelope behavior.
