# Match results The confirmed, server-computed outcome of every match your players play — the only signal you may move value on. ## The call ``` GET /partner/results?cursor=&limit=50 Authorization: Bearer 200 { "rows": [ { "matchId": "m_7f3a…", "playerId": "u_8813", "gameId": "streakpeaks", "state": "ended", "outcome": "win", "endedAt": 1785000420, "tierId": "usd_10", "prizeUsdCents": 1000, "stakeUsdCents": 583, "feeUsdMicros": 250000, "played": true } ], "cursor": "…", "hasMore": false } ``` It authenticates with your API key, not a session token, and returns only that key’s environment’s rows. ## `limit` counts matches, not rows `limit` defaults to **50**, capped at **200**, bounding **matches**, not `rows`. A match produces one row per player of yours in it — [a paid match seats two players of the same operator](/docs/paid-play.md), so a decided one produces two rows. `limit=1` can return 2 rows; `limit=200`, up to **400**. Size buffers on the row count, not `limit`. > The cursor is a position in the match history; a page always ends on a match boundary, never splitting one player's row from its opponent's. Read `rows.length` for what arrived and `hasMore` for whether to continue — never compare `rows.length` with `limit` to decide you’re done. ## The three states | `state` | What happened | Your move | | --- | --- | --- | | `started` | Your player is seated — on a paid match, at payment, not their first move | Committed — take it once, if not already | | `ended` | The match is over; `outcome` is final | Credit the result, once | | `no_contest` | Nobody joined | Return the entry, once | > **If you already took the entry when you seated the match, a `started` row is a confirmation, not a second charge — the one mistake on this feed that costs real money.** The seating call moves the money ([Paid matches](/docs/paid-play.md) step 4); this row only confirms it. Key your ledger on `(matchId, playerId)` and apply each movement once. > **Neither a `draw` nor a `no_contest` carries a fee:** a draw returns both players their entry in full, an unjoined match returns the one that was paid, and neither accrues. The model is on [the integration page](/integration). `outcome` is `win`, `loss` or `draw`, appearing only once the state is `ended`; it and `endedAt` are omitted entirely until then — never sent as `null`. Timestamps are Unix epoch seconds. ## The money on a row The last four fields appear on every partner-seated match — on `started` as well as `ended`, since *debit the stake, once* needs a number. | Field | What it is | | --- | --- | | `tierId` | Which tier the seat was sold at. | | `prizeUsdCents` | What the winner takes. Ours, fixed per tier. | | `stakeUsdCents` | What *you* charged this player, sent on the seating call — for a priced tier, your account’s set price. We compute nothing from it; it matches your ledger to ours. | | `feeUsdMicros` | What this seat costs you, under the agreed split, in **micro-USD** — millionths of a dollar: `10000` is one cent, `250000` is 25 c. Quoted before play, not after. | > `feeUsdMicros` is always present; `0` and `null` differ. `0` means **nothing is owed** (a `draw` or a `no_contest` — you may close the accrual); `null` means **we have not said**. A falsy check collapses the two. | Field | What it is | | --- | --- | | `played` | Whether this player made a server-accepted move, present only on `ended`: `true` — they played; `false` — they did not (neither player opened reports `false` on both rows); **absent** — not recorded, not the same as `false`. **The only way to tell a no-show from a real tie on a paid match:** two who paid and left report the same `draw` as two who tied. Branch on `played`, not outcome. | Each row carries **only that seat’s** money — two of your players meeting yields two rows, each with what was charged, which may differ. ## Four rules that decide whether your ledger is right - **Key every move on `(matchId, playerId)`, never on `matchId` alone**: two of your own players yield two rows sharing a matchId, and keying on the match alone drops one side's credit. - **Expect rows to repeat.** Once caught up, the cursor is held a few seconds behind the clock, so your next poll re-returns that window, catching any same-second result. Applying each `(matchId, playerId)` exactly once is therefore not optional. - **You will often see `ended` without ever seeing `started`** — a short match can begin and finish inside one poll interval. Debit and credit in one step; this is ordinary, not an edge case. - **No row means nothing moved:** a player who opens a game without starting a match never produces one, so there is no reservation to time out. ## How long a match can wait A match that never finds an opponent ends as `no_contest` after **72 hours** — the longest your reservation ever stands. One with an opponent runs on its own clock, unaffected. ## Paging, and catching up Pass the `cursor` to get the next page; omit it to start from the beginning of your history. Follow `hasMore` — not whether `rows` came back non-empty. > **Your first call has history behind it:** omitting the cursor starts at the ACCOUNT’s history, not your integration’s, so a fresh build may re-pay months-old matches. On first start: page to the end, keep only the final cursor, settle what arrives after. > **If a cursor is refused** (`400 invalid_cursor`), **stop and raise it** — a valid cursor never expires, so refusal means corruption, truncation, or the wrong environment. Restarting re-reads the whole history, safe only if your ledger is keyed on `(matchId, playerId)`. **An empty `"rows": []` does not mean you are done, and it can happen mid-catch-up, two ways:** a page can land entirely on matches with no row while `hasMore` stays true; or, when what’s left is an exact multiple of `limit`, the last full page still says `hasMore: true` before an empty one — neither is an error. Store your last cursor to catch up after downtime. It’s opaque, valid only for its partner and environment; a foreign or edited one answers `400 invalid_cursor`, never an empty page. ## What a row does not contain Almost nothing: a row carries only what’s tied to **price, payout or identification**. It omits the opponent’s uid, name, the deal, the seed and the move history — except on a paid match with both seats yours, where the two rows share a `matchId` and reveal who your player faced. It also omits your own uid, either side’s score, and the engine version. Your uid is derivable from `playerId`: exactly `ptr::` (or `ptr:test::` in test) — the value [session minting](/docs/players.md) returned. Scores drive no payment: your player sees their own inside your frame; `outcome` plus `played` are what your ledger needs. For a count of matches played rather than their outcomes, use the usage meter in [the portal](/docs/portal.md), not this endpoint.