Settle any outcome
with one API call.
Send the outcome you need settled. Get back settled, held or voided, a settlement_id and a receipt link. Gaming outcomes from recorded video.
Watch outcome verification. The API and sandbox are live. See status.
Four steps.
One sandbox key.
Production base URLs are issued with access. A sandbox key works on the sandbox base URL provided with the key. The examples use $PLLAY_BASE for that URL. The key below is a placeholder. See the spec for full fields.
Send your sandbox key in the X-Partner-Key header on every call.
export PLLAY_KEY="pllay_sk_sandbox_…" # Every call carries this header: -H "X-Partner-Key: $PLLAY_KEY"
const PLLAY = process.env.PLLAY_BASE!;
async function pllay(path: string, body?: object, headers: Record<string, string> = {}) {
const res = await fetch(PLLAY + path, {
method: body ? 'POST' : 'GET',
headers: { 'X-Partner-Key': process.env.PLLAY_KEY!, 'Content-Type': 'application/json', ...headers },
body: body ? JSON.stringify(body) : undefined,
});
return res.json();
}Send type, path and spec. spec.game_slug names the auto-settle title, such as fortnite. spec.pov_side (home or away) is required for dota2 and rocketleague. Do not send threshold. Send an Idempotency-Key. The same key and the same body within 24 hours returns the original 200 response.
curl -X POST "$PLLAY_BASE/v1/events" \
-H "X-Partner-Key: $PLLAY_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"type":"promo","path":"vod","notional_minor":0,"spec":{"title":"Fortnite","question":"Did the POV player win (Victory Royale)?","game_slug":"fortnite"}}'
# 201: state "open", event_id returned
curl -X POST "$PLLAY_BASE/v1/events/$EVENT_ID/settle" \
-H "X-Partner-Key: $PLLAY_KEY" -H "Content-Type: application/json" \
-d '{}'
# 200. The server sets outcome, confidence and model_version.const event = await pllay('/v1/events', {
type: 'promo',
path: 'vod',
notional_minor: 0,
spec: {
title: 'Fortnite',
question: 'Did the POV player win (Victory Royale)?',
game_slug: 'fortnite'
}
}, { 'Idempotency-Key': crypto.randomUUID() });
// event.state === 'open'
await pllay(`/v1/events/${event.event_id}/settle`, {});Lock the event before settle. The full flow is in the reference below.
Read the event. A settled result carries its settlement_id and a receipt link.
curl "$PLLAY_BASE/v1/events/$EVENT_ID" -H "X-Partner-Key: $PLLAY_KEY"
# "state": "settled", "settlement_id": "7f3ac91e-2b4d-4e60-8a15-c0d9e3f1a762"
# receipt: https://pllay.io/receipt/{event_id}const result = await pllay(`/v1/events/${event.event_id}`);
// result.state: 'settled' | 'held' | 'voided' | …
const receipt = `https://pllay.io/receipt/${result.event_id}`;held is a waiting state, not an error: wait for event.settled or event.voided. Receipts are public for results the partner opts in, and for the browser-game demo. Sandbox receipts are labelled.
Your HTTPS endpoint is registered with your key. Every delivery carries X-Webhook-Event and X-Pllay-Event (the event name), X-Pllay-Delivery (a stable id for this delivery), X-Webhook-Timestamp and X-Pllay-Timestamp (Unix seconds), X-Webhook-Signature and X-Pllay-Signature (t=<unix seconds>,v1=<hex>). v1 is the HMAC-SHA256 of "<t>.<raw body>" with your webhook secret. Reject a t more than 300 seconds from your clock. Verify X-Pllay-Signature. X-Webhook-Signature is deprecated: it signs the body only and does not protect against replay. During a secret rotation X-Pllay-Signature carries two v1 values; accept the delivery when any v1 matches a secret you hold. A retry of a delivery reuses X-Pllay-Delivery and t, so save the delivery id before you act and ignore a repeat.
# X-Pllay-Delivery: <delivery id> (save it, ignore a repeat) # X-Pllay-Event: <event name> # X-Pllay-Timestamp: <unix seconds> # X-Pllay-Signature: t=<unix seconds>,v1=<hex>[,v1=<hex>] printf '%s' "$PLLAY_T.$RAW_BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" # Compare the hex to each v1. Reject a t more than 300 seconds off. # X-Webhook-Signature: sha256=<hex of the raw body> is deprecated: no replay protection. # X-Webhook-Event and X-Webhook-Timestamp are also sent.
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(rawBody: string, header: string, secrets: string[]) {
const parts = header.split(',');
const t = (parts.find((p) => p.startsWith('t=')) || '').slice(2);
const v1s = parts.filter((p) => p.startsWith('v1=')).map((p) => p.slice(3));
const age = Math.abs(Date.now() / 1000 - Number(t));
if (!/^[0-9]+$/.test(t) || v1s.length === 0 || !(age <= 300)) return false;
return secrets.some((secret) => {
const expected = createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex');
return v1s.some((v1) => v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected)));
});
}
// Header: X-Pllay-Signature. Pass every secret you still hold (current, then previous during a rotation).
// Dedupe on X-Pllay-Delivery before side effects. X-Webhook-Signature is deprecated.
// X-Webhook-Event: event.settled | event.held | event.reversed | event.voidedEvents: event.settled, event.held, event.reversed, event.voided. The body is the event object.
A settled result,
as your code sees it.
{
"event_id": "5d0e8a14-3b7c-4f29-a6d1-9c2e47b0f385",
"type": "promo",
"path": "vod",
"state": "settled",
"spec": {
"title": "Fortnite",
"question": "Did the POV player win (Victory Royale)?",
"game_slug": "fortnite"
},
"outcome": "yes",
"confidence": 0.987,
"threshold": 0.9,
"model_version": "vision-vod-2026-09",
"evidence_uri": "https://pllay.io/receipt/5d0e8a14-3b7c-4f29-a6d1-9c2e47b0f385",
"settlement_id": "7f3ac91e-2b4d-4e60-8a15-c0d9e3f1a762",
"reversal_settlement_id": null,
"reviewer_id": null,
"stake_ref": null,
"notional_minor": 0,
"reason": null
}Fortnite.
One question, one clip.
Send a recorded clip of one battle royale match from the player’s own view. Vision reads the end screen and returns WIN or LOSE, or holds when the clip does not show a clear result. Available What Live means
Did the POV player win (Victory Royale)?
WIN. Also when the player was eliminated earlier and is spectating a teammate’s win.
LOSE.
Held. In duos, trios and squads the team can still win, so Vision never calls a loss from ELIMINATED alone.
- Recorded video only, on a public https URL. One match per clip, from the player’s own view.
- Include the end screen, VICTORY ROYALE or YOU PLACED #N, plus about 5 seconds after it appears.
- Don’t stop the clip at ELIMINATED. Let it run on to the YOU PLACED #N screen.
- Keep it short: the end of the match and the end screen are enough.
Use adapter fortnite_result.json and market kind pov with exactly one WIN and one LOSE option. Other labels, such as Team A and Team B, can’t be mapped and the event is voided.
curl -X POST "$PLLAY_PARTNER_API/v1/events" \
-H "X-Partner-Key: $PLLAY_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"video_url":"https://example.com/your-fortnite-clip.mp4","adapter":"fortnite_result.json","game_slug":"fortnite","market":{"kind":"pov","options":["WIN","LOSE"]},"external_market_id":"your-market-123"}'
# 202: event_id returned, status "queued"
curl "$PLLAY_PARTNER_API/v1/events/$EVENT_ID" -H "X-Partner-Key: $PLLAY_KEY"{
"event_id": "…",
"status": "resolved",
"state": "settled",
"external_market_id": "your-market-123",
"resolution": {
"outcome_option_index": 0,
"outcome_label": "WIN",
"settleable": true
}
}Excerpt, illustrative values. Full fields are in the partner API spec.
Submitted, not resolved yet. Keep waiting.
WIN or LOSE, read from agreeing frames of the end screen.
Waiting, not an error. Not settled yet. Most often the clip is missing the end screen.
No usable outcome: an unreadable clip or a market that can’t be mapped. Resubmit a better clip.
Fortnite is available and live today. “Available and live today” means the title is offered now for recorded-video settlement, not live-stream settlement. See /coverage#live. Game names are trademarks of their owners, and a listing is not a partnership, sponsorship or endorsement. See /coverage#titles. When Vision reads a clear end screen, the result settles on its own, without waiting for a person. Anything Vision is unsure of, such as ELIMINATED on its own or a clip with no clear end screen, is held for review rather than guessed: it is checked before it settles, so an unclear result is never settled on a guess. A hold is a waiting state, not an error: build for settled, held and voided. Results are asynchronous: poll the event or take the webhook. What “live today” means. Title names. Webhook format, corrections and disputes.
Sandbox keys return a simulated Fortnite result: the same clip URL, reference and market always give the same answer. It never reads the clip, and it is never a real result. To test the held path, send an external_market_id that starts with sandbox-held: that event always comes back held.
- Run the sandbox in your browser now, with no key.
- Request a sandbox key through the integration form (/enterprise?flow=integration). Self-serve signup is coming soon; until it opens, sandbox keys are issued per partner.
- Build against it, including the held path. Today your HTTPS webhook endpoint is registered for you and the signing secret is shared privately.
- When your integration handles settled, held and voided, a production key and the base URL follow a short review.
API spec · partner-api-v1.yaml · OpenAPI 3.0 spec for the partner API, including the Fortnite example.
Valorant.
One banner, one result.
Send a recorded clip of one Valorant match from the player’s own view. Vision reads the end-of-match banner and returns WIN or LOSE, or holds when the clip does not show a clear result. Human confirmation required
Did the POV player’s team win the match?
WIN. Surrenders and forfeits included: the banner names the outcome, so the match never has to reach the final round.
LOSE.
Held. Round results during the match, such as Round Win or LOST, are never read as the match result. A match that ends without VICTORY or DEFEAT is held, not guessed.
- Recorded video only, on a public https URL. One match per clip, from the player’s own view.
- Include the end-of-match VICTORY or DEFEAT banner, plus a few seconds after it appears.
- Don’t stop the clip on a round result. Let it run to the end-of-match banner.
- Keep it short: the end of the match and the banner are enough.
Use adapter valorant_result.json, game_slug valorant and market kind pov with exactly one WIN and one LOSE option. Other labels, such as Team A and Team B, can’t be mapped and the event is voided.
curl -X POST "$PLLAY_PARTNER_API/v1/events" \
-H "X-Partner-Key: $PLLAY_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"video_url":"https://example.com/your-valorant-clip.mp4","adapter":"valorant_result.json","game_slug":"valorant","market":{"kind":"pov","options":["WIN","LOSE"]},"external_market_id":"your-market-123"}'
# 202: event_id returned, status "queued"
curl "$PLLAY_PARTNER_API/v1/events/$EVENT_ID" -H "X-Partner-Key: $PLLAY_KEY"{
"event_id": "…",
"status": "resolved",
"state": "settled",
"external_market_id": "your-market-123",
"resolution": {
"outcome_option_index": 0,
"outcome_label": "WIN",
"settleable": true
}
}Excerpt, illustrative values. Full fields are in the partner API spec.
Submitted, not resolved yet. Keep waiting.
WIN or LOSE, read from agreeing frames of the end-of-match banner.
Waiting, not an error. Not settled yet. Most often the clip ends before the banner.
No usable outcome: an unreadable clip or a market that can’t be mapped. Resubmit a better clip.
Valorant requires human confirmation while automatic settlement is under review. When Vision reads a clear VICTORY or DEFEAT banner, the result settles on its own, without waiting for a person. Anything Vision is unsure of, such as a clip that ends before the banner, is held for review rather than guessed: it is checked before it settles, so an unclear result is never settled on a guess. A hold is a waiting state, not an error: build for settled, held and voided. Results are asynchronous: poll the event or take the webhook. What “live today” means. Title names. Webhook format, corrections and disputes.
Sandbox keys return a simulated result: the same clip URL, reference and market always give the same answer. It never reads the clip, and it is never a real result. To test the held path, send an external_market_id that starts with sandbox-held: that event always comes back held.
- Run the sandbox in your browser now, with no key.
- Request a sandbox key through the integration form (/enterprise?flow=integration). Self-serve signup is coming soon; until it opens, sandbox keys are issued per partner.
- Build against it, including the held path. Today your HTTPS webhook endpoint is registered for you and the signing secret is shared privately.
- When your integration handles settled, held and voided, a production key and the base URL follow a short review.
API spec · partner-api-v1.yaml · OpenAPI 3.0 spec for the partner API. For Valorant, use the Fortnite example with adapter valorant_result.json and game_slug valorant.
Dota 2.
Name the side, get the result.
Send a recorded clip of one Dota 2 match from the player’s own view, and say which side they played with market.pov_side. Vision reads the victory screen and returns WIN or LOSE, or holds when the clip does not show a clear result. Available What Live means
Did the POV player’s side win the match?
Radiant won. WIN when pov_side is home (the POV player is Radiant), LOSE when it is away.
Dire won. WIN when pov_side is away (the POV player is Dire), LOSE when it is home.
Held. Kill counts and the top bar are never used: Dota is won by destroying the Ancient, so only the victory screen settles. A clip without a clear RADIANT or DIRE VICTORY is held, not guessed.
- Recorded video only, on a public https URL. One match per clip, from the player’s own view.
- Include the RADIANT VICTORY or DIRE VICTORY screen, plus a few seconds after it appears.
- Don’t stop the clip before the Ancient falls. Let it run to the victory screen.
- Keep it short: the end of the match and the victory screen are enough.
Use adapter dota2_victory.json, game_slug dota2 and market kind pov with exactly one WIN and one LOSE option, plus market.pov_side: "home" if the POV player is on Radiant, "away" if on Dire. pov_side is required for Dota 2: the victory screen names the winning side, not the player, so an event sent without it, or with any other value, is rejected with a 400 pov_side_required error that points back to this guide. Nothing is queued. Labels other than WIN and LOSE can’t be mapped and the event is voided.
curl -X POST "$PLLAY_PARTNER_API/v1/events" \
-H "X-Partner-Key: $PLLAY_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"video_url":"https://example.com/your-dota2-clip.mp4","adapter":"dota2_victory.json","game_slug":"dota2","market":{"kind":"pov","options":["WIN","LOSE"],"pov_side":"away"},"external_market_id":"your-market-123"}'
# 202: event_id returned, status "queued"
curl "$PLLAY_PARTNER_API/v1/events/$EVENT_ID" -H "X-Partner-Key: $PLLAY_KEY"{
"event_id": "…",
"status": "resolved",
"state": "settled",
"external_market_id": "your-market-123",
"resolution": {
"outcome_option_index": 0,
"outcome_label": "WIN",
"settleable": true
}
}Excerpt, illustrative values: a DIRE VICTORY with pov_side away is a WIN. Full fields are in the partner API spec.
Submitted, not resolved yet. Keep waiting.
WIN or LOSE, read from agreeing frames of the victory screen and mapped through pov_side.
Waiting, not an error. Not settled yet. Most often the clip ends before the victory screen.
No usable outcome: an unreadable clip or a market that can’t be mapped. Resubmit a better clip.
Dota 2 is available and live today. “Available and live today” means the title is offered now for recorded-video settlement, not live-stream settlement. See /coverage#live. Game names are trademarks of their owners, and a listing is not a partnership, sponsorship or endorsement. See /coverage#titles. When Vision reads a clear RADIANT VICTORY or DIRE VICTORY, the result settles on its own, without waiting for a person. Anything Vision is unsure of, such as a clip that ends before the victory screen, is held for review rather than guessed: it is checked before it settles, so an unclear result is never settled on a guess. A hold is a waiting state, not an error: build for settled, held and voided. Results are asynchronous: poll the event or take the webhook. What “live today” means. Title names. Webhook format, corrections and disputes.
Sandbox keys return a simulated result: the same clip URL, reference and market always give the same answer. It never reads the clip, and it is never a real result. To test the held path, send an external_market_id that starts with sandbox-held: that event always comes back held.
- Run the sandbox in your browser now, with no key.
- Request a sandbox key through the integration form (/enterprise?flow=integration). Self-serve signup is coming soon; until it opens, sandbox keys are issued per partner.
- Build against it, including the held path. Today your HTTPS webhook endpoint is registered for you and the signing secret is shared privately.
- When your integration handles settled, held and voided, a production key and the base URL follow a short review.
API spec · partner-api-v1.yaml · OpenAPI 3.0 spec for the partner API, including a Dota 2 example with market.pov_side.
League of Legends.
One splash, one result.
Send a recorded clip of one League of Legends match from the player’s own view. Vision reads the end-of-game VICTORY or DEFEAT splash and returns WIN or LOSE, or holds when the clip does not show a clear result. Available What Live means
Did the POV player win the match?
WIN. The centre splash names the POV player’s outcome.
LOSE.
Held. In-game kill feeds and scoreboards are never read as the match result. A clip that ends before the VICTORY or DEFEAT splash is held, not guessed.
- Recorded video only, on a public https URL. One match per clip, from the player’s own view.
- Include the end-of-game VICTORY or DEFEAT splash, plus a few seconds after it appears.
- Don’t stop the clip on the scoreboard or the CONTINUE prompt alone. Let the splash land first.
- Keep it short: the end of the match and the splash are enough.
Use adapter lol_result.json, game_slug league-of-legends and market kind pov with exactly one WIN and one LOSE option. Other labels, such as Team A and Team B, can’t be mapped and the event is voided. No market.pov_side: League names the POV outcome directly, unlike Dota 2.
curl -X POST "$PLLAY_PARTNER_API/v1/events" \
-H "X-Partner-Key: $PLLAY_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"video_url":"https://example.com/your-league-clip.mp4","adapter":"lol_result.json","game_slug":"league-of-legends","market":{"kind":"pov","options":["WIN","LOSE"]},"external_market_id":"your-market-123"}'
# 202: event_id returned, status "queued"
curl "$PLLAY_PARTNER_API/v1/events/$EVENT_ID" -H "X-Partner-Key: $PLLAY_KEY"{
"event_id": "…",
"status": "resolved",
"state": "settled",
"external_market_id": "your-market-123",
"resolution": {
"outcome_option_index": 0,
"outcome_label": "WIN",
"settleable": true
}
}Excerpt, illustrative values. Full fields are in the partner API spec.
Submitted, not resolved yet. Keep waiting.
WIN or LOSE, read from agreeing frames of the end-of-game splash.
Waiting, not an error. Not settled yet. Most often the clip ends before the splash.
No usable outcome: an unreadable clip or a market that can’t be mapped. Resubmit a better clip.
League of Legends is available and live today. “Available and live today” means the title is offered now for recorded-video settlement, not live-stream settlement. See /coverage#live. Game names are trademarks of their owners, and a listing is not a partnership, sponsorship or endorsement. See /coverage#titles. When Vision reads a clear VICTORY or DEFEAT splash, the result settles on its own, without waiting for a person. Anything Vision is unsure of, such as a clip that ends before the splash, is held for review rather than guessed: it is checked before it settles, so an unclear result is never settled on a guess. A hold is a waiting state, not an error: build for settled, held and voided. Results are asynchronous: poll the event or take the webhook. What “live today” means. Title names. Webhook format, corrections and disputes.
Sandbox keys return a simulated result: the same clip URL, reference and market always give the same answer. It never reads the clip, and it is never a real result. To test the held path, send an external_market_id that starts with sandbox-held: that event always comes back held.
- Run the sandbox in your browser now, with no key.
- Request a sandbox key through the integration form (/enterprise?flow=integration). Self-serve signup is coming soon; until it opens, sandbox keys are issued per partner.
- Build against it, including the held path. Today your HTTPS webhook endpoint is registered for you and the signing secret is shared privately.
- When your integration handles settled, held and voided, a production key and the base URL follow a short review.
API spec · partner-api-v1.yaml · OpenAPI 3.0 spec for the partner API, including a League of Legends example.
PUBG: BATTLEGROUNDS.
One question, one clip.
Send a recorded clip of one battle royale match from the player’s own view. Vision reads the end screen and returns WIN or LOSE. A clear read settles automatically. When the read is not clear, the result is held for review. Available What Live means
Did the POV player win (Winner Winner Chicken Dinner)?
WIN. Also when the player was eliminated earlier and is spectating a teammate’s win.
LOSE, including a placement after an earlier elimination.
Held. In duos and squads the team can still win, so Vision never calls a loss from an elimination alone.
- Recorded video only, on a public https URL. One match per clip, from the player’s own view.
- Include the end screen, Winner Winner Chicken Dinner or the placement screen, plus a few seconds after it appears.
- Don’t stop the clip at the elimination. Let it run on to the placement screen.
- Keep it short: the end of the match and the end screen are enough.
Use adapter pubg_result.json and market kind pov with exactly one WIN and one LOSE option. No market.pov_side: the end screen names the POV player’s outcome. Other labels can’t be mapped and the event is voided.
curl -X POST "$PLLAY_PARTNER_API/v1/events" \
-H "X-Partner-Key: $PLLAY_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"video_url":"https://example.com/your-pubg-clip.mp4","adapter":"pubg_result.json","game_slug":"pubg","market":{"kind":"pov","options":["WIN","LOSE"]},"external_market_id":"your-market-123"}'
# 202: event_id returned, status "queued"
curl "$PLLAY_PARTNER_API/v1/events/$EVENT_ID" -H "X-Partner-Key: $PLLAY_KEY"{
"event_id": "…",
"status": "resolved",
"state": "settled",
"external_market_id": "your-market-123",
"resolution": {
"outcome_option_index": 0,
"outcome_label": "WIN",
"settleable": true
}
}Excerpt, illustrative values. Full fields are in the partner API spec.
Submitted, not resolved yet. Keep waiting.
WIN or LOSE, read from agreeing frames of the end screen. A clear read settles automatically.
Waiting, not an error. Not settled yet. When the read is not clear, the result is held for review, and so is a clip that is missing the end screen. Do not release rewards on held.
No usable outcome: an unreadable clip or a market that can’t be mapped. Resubmit a better clip.
PUBG: BATTLEGROUNDS is available and live today. “Available and live today” means the title is offered now for recorded-video settlement, not live-stream settlement. See /coverage#live. Game names are trademarks of their owners, and a listing is not a partnership, sponsorship or endorsement. See /coverage#titles. When Vision reads a clear end screen, the result settles on its own. When the read is not clear, the result is held for review rather than guessed. A hold is a waiting state, not an error: build for settled, held and voided. Results are asynchronous: poll the event or take the webhook. The same Idempotency-Key and the same body within 24 hours returns the original 200 response. What “live today” means. Title names. Webhook format, corrections and disputes.
Sandbox keys return a simulated result: the same clip URL, reference and market always give the same answer. It never reads the clip, and it is never a real result. To test the held path, send an external_market_id that starts with sandbox-held: that event always comes back held.
- Run the sandbox in your browser now, with no key.
- Request a sandbox key through the integration form (/enterprise?flow=integration). Self-serve signup is coming soon; until it opens, sandbox keys are issued per partner.
- Build against it, including the held path. Today your HTTPS webhook endpoint is registered for you and the signing secret is shared privately.
- When your integration handles settled, held and voided, a production key and the base URL follow a short review.
API spec · partner-api-v1.yaml · OpenAPI 3.0 spec for the partner API.
Rocket League.
One question, one clip.
Send a recorded clip of one match from the player’s own view, and say which team that player is on. Vision reads the end screen and returns WIN or LOSE. A clear read settles automatically. When the read is not clear, the result is held for review. Available What Live means
Did the POV player’s team win the match?
WIN, when the banner names the side you sent as pov_side.
LOSE.
The end screen is the result.
- Recorded video only, on a public https URL. One match per clip, from the player’s own view.
- Include the end screen, the WINNER banner, plus a few seconds after it appears.
- The clip must show which side won. Do not stop before the WINNER banner.
- Keep it short: the end of the match and the end screen are enough.
Use adapter rocket_league_result.json, market kind pov with exactly one WIN and one LOSE option, and market.pov_side. home means the POV player is on Blue. away means Orange. You are responsible for pov_side: a wrong side settles the wrong team, and the remedy is a dispute, not a silent correction.
curl -X POST "$PLLAY_PARTNER_API/v1/events" \
-H "X-Partner-Key: $PLLAY_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"video_url":"https://example.com/your-rocket-league-clip.mp4","adapter":"rocket_league_result.json","game_slug":"rocketleague","market":{"kind":"pov","options":["WIN","LOSE"],"pov_side":"home"},"external_market_id":"your-market-123"}'
# 202: event_id returned, status "queued"
curl "$PLLAY_PARTNER_API/v1/events/$EVENT_ID" -H "X-Partner-Key: $PLLAY_KEY"{
"event_id": "…",
"status": "resolved",
"state": "settled",
"external_market_id": "your-market-123",
"resolution": {
"outcome_option_index": 0,
"outcome_label": "WIN",
"settleable": true
}
}Excerpt, illustrative values. Full fields are in the partner API spec.
Submitted, not resolved yet. Keep waiting.
WIN or LOSE, read from agreeing frames of the end screen. A clear read settles automatically.
Waiting, not an error. Not settled yet. When the read is not clear, the result is held for review, and so is a clip that is missing the WINNER banner. Do not release rewards on held.
No usable outcome: an unreadable clip or a market that can’t be mapped. Resubmit a better clip.
Rocket League is available and live today. “Available and live today” means the title is offered now for recorded-video settlement, not live-stream settlement. See /coverage#live. Game names are trademarks of their owners, and a listing is not a partnership, sponsorship or endorsement. See /coverage#titles. When Vision reads a clear end screen, the result settles on its own. When the read is not clear, the result is held for review rather than guessed. A hold is a waiting state, not an error: build for settled, held and voided. If a settled result is wrong, including a wrong pov_side, dispute it within 72 hours. A PLLAY reviewer decides. Results are asynchronous: poll the event or take the webhook. The same Idempotency-Key and the same body within 24 hours returns the original 200 response. What “live today” means. Title names. Webhook format, corrections and disputes.
Sandbox keys return a simulated result: the same clip URL, reference and market always give the same answer. It never reads the clip, and it is never a real result. To test the held path, send an external_market_id that starts with sandbox-held: that event always comes back held.
- Run the sandbox in your browser now, with no key.
- Request a sandbox key through the integration form (/enterprise?flow=integration). Self-serve signup is coming soon; until it opens, sandbox keys are issued per partner.
- Build against it, including the held path. Today your HTTPS webhook endpoint is registered for you and the signing secret is shared privately.
- When your integration handles settled, held and voided, a production key and the base URL follow a short review.
API spec · partner-api-v1.yaml · OpenAPI 3.0 spec for the partner API.
Partner API webhooks.
Corrections and disputes.
The title guides use the partner API. Deliveries use the same headers as the settlement contract webhooks in the reference below.
Resolution and dispute events go to an HTTPS URL you send to PLLAY. PLLAY registers that URL and subscribes it to the event types you need; the signing secret is shared privately. Dispute webhooks reach you only when your endpoint is subscribed to dispute.opened, dispute.decided and resolution.corrected, so ask PLLAY to include them.
- The resolution and dispute URL must be https. PLLAY registers it. PATCH /v1/webhooks does not set this URL.
- PATCH /v1/webhooks sets the legacy endpoint for prediction.* events only. webhook_url must start with https://.
- Changing that legacy URL rotates the HMAC signing secret. The new secret is returned once, on that response. Store it then.
- Partner API Idempotency-Key: the same key and the same body within 24 hours returns the original 200 response.
A result is in: state settled, or held when it needs a person.
No outcome: state voided.
The run could not complete: state held.
A dispute was filed on one of your results, with its target acknowledge and decide times.
PLLAY decided the dispute, upheld or overturned, with a written reason.
An overturn issued a new corrected or void result. The original result is kept.
X-Webhook-Event: <event name> X-Webhook-Signature: sha256=<hex of the raw body> X-Webhook-Timestamp: <unix seconds> X-Pllay-Signature: t=<unix_seconds>,v1=<hex> # v1 = HMAC-SHA256 of "<t>.<raw body>" with your endpoint secret. # Recompute and compare it, and reject a t more than 300 seconds from your clock.
{
"schema_version": 1,
"event_id": "…",
"event_type": "resolution.corrected",
"environment": "production",
"occurred_at": "…",
"data": {
"event_id": "…",
"external_market_id": "your-market-123",
"dispute_id": "…",
"correction_id": "…",
"kind": "corrected",
"tier": "…",
"outcome_option_index": 1,
"original": {
"tier": "…",
"outcome_option_index": 0
},
"decision_reason": "…",
"corrected_at": "…"
}
}Illustrative values. The envelope event_id is the webhook event id and is the same on every retry, so de-duplicate on it; data.event_id is the result id. Every field is in the partner API spec.
- Dispute it within 72 hours of the result: POST /v1/events/{id}/disputes with reason_code (wrong_outcome, wrong_market, evidence_unclear or other), reason_text and an optional https evidence_url. Needs the resolution:dispute scope. You can also email support@pllay.io.
- Each result can be disputed once. A second filing returns 409 dispute_already_open while the first is open, or 409 dispute_already_decided after it is decided. After 72 hours the API returns 422 dispute_window_closed.
- PLLAY reviews the evidence and decides, upheld or overturned, with a written reason (decision_reason, sent to you as written). Acknowledge and decide times are targets, not a contractual guarantee. GET /v1/disputes/{id} shows lifecycle_status, ack_due_at and decide_due_at.
- If it is overturned, the original result is kept and a new corrected or void result arrives as resolution.corrected (and as correction on GET /v1/disputes/{id}). Adjust your own users’ positions from it. PLLAY never moves money; you handle any refund on your own rails.
Details: Webhooks, POST /events/{id}/disputes and GET /disputes/{id} in partner-api-v1.yaml. Timeline on /trust.
What PLLAY settles,
title by title.
Live now What Live means
'Live' means PLLAY's integration for the listed title (Fortnite, League of Legends, VALORANT, Dota 2, Rocket League and PUBG: BATTLEGROUNDS) has passed PLLAY's internal regression testing and internal production test settlements and is ready for partner integration. 'Live' does not mean that any partner or customer is using the title, that its results have been independently reviewed or authenticated, or that the title's publisher has approved or endorsed its use. Whether a title is available to a particular partner depends on onboarding, that partner's QA, the partner's own compliance obligations, and any publisher permissions its use requires. PLLAY may suspend or remove a title at any time, including in response to a publisher notice.
Current restriction: VALORANT: human confirmation required while automatic settlement is under review.
| Title | Live now | Coming soon |
|---|---|---|
| Fortnite | Win or loss. VICTORY ROYALE is a win. YOU PLACED #N is a loss, including when the player was eliminated earlier. | Placement · Player identity check · Match details |
| League of Legends | Win or loss. The end-of-game VICTORY or DEFEAT splash. | Score · Player identity check · Match details |
| Dota 2 | Win or loss. RADIANT VICTORY or DIRE VICTORY, mapped to the player through the side you send. Requests must name the player’s side with market.pov_side: home for Radiant, away for Dire. | Score · Player identity check · Match details |
| PUBG: BATTLEGROUNDS | Win or loss. Winner Winner Chicken Dinner is a win. A placement screen is a loss. | Placement · Player identity check · Match details |
| Rocket League | Win or loss. The end-of-match WINNER banner (Blue or Orange), mapped to the player through the side you send. Requests must name the player’s side with market.pov_side: home for Blue, away for Orange. | Goals · Player identity check · Match details |
Human confirmation required
| Title | Human confirmation required | Coming soon |
|---|---|---|
| VALORANT | Win or loss. The end-of-match VICTORY or DEFEAT banner. | Round score · Player identity check · Match details |
Fortnite, League of Legends, Dota 2, PUBG: BATTLEGROUNDS and Rocket League: settled automatically when the read is clear; held for review when the read is not clear. What each title settles, on /coverage →
Sandbox first.
Production when ready.
Test outcomes only. A sandbox settlement is never a real result.
Kept apart from production: a production key cannot see a sandbox event.
Rate-limited to 100 creates a minute per key.
Never billed.
Issued after a short review.
Same API, same calls, same webhooks.
Billed per settled result.
$1 per settled result.
Held and voided are free.
100 settled results free every month
$1 per settled result
No monthly spend cap
Sandbox calls are never billed. Custom agreements and any pool clearance fee are described on /pricing.
Use of the API is governed by the Terms of Service, API Terms, Acceptable Use Policy and DPA.
SDKs.
Generate from the spec.
Official SDK packages are not published yet. Generate a typed client in any language from the OpenAPI files, or copy the curl and TypeScript calls above.
npx @openapitools/openapi-generator-cli generate -i https://www.pllay.io/artifacts/settlement/openapi.yaml -g typescript-fetch -o ./pllay-client
Settlement API · openapi.yaml · Partner API · partner-api-v1.yaml
Clone it.
Run it.
A small app that runs the four steps against the sandbox: create, read the result, verify a webhook.
Create. Lock.
Settle or hold.
Production base URLs are issued with access. A sandbox key works on the sandbox base URL provided with the key. The examples use $PLLAY_API for that URL. Tour fixture: the sandbox page runs in your browser, needs no key and does not call the hosted API. Create sends spec.game_slug and does not send threshold. spec.pov_side (home or away) is required for dota2 and rocketleague. Settle sends an empty body.
Recorded-video verification is built and benchmarked. Which titles are available is on Status. The settlement contract below is separate and carries the decision.
# Vision intake (partner API): submit the clip
curl -X POST "$PLLAY_PARTNER_API/v1/vision/verify" -H "X-Partner-Key: $PARTNER_KEY" -H "Content-Type: application/json" \
-d '{"video_url":"https://example.com/your-fortnite-clip.mp4","adapter":"fortnite_result.json","game_slug":"fortnite","market":{"kind":"pov","options":["WIN","LOSE"]},"external_market_id":"your-reference"}'
# 202: verification_id and event_id returned, status "queued"# 01 Propose: create the event
curl -X POST "$PLLAY_API/v1/events" -H "X-Partner-Key: $PARTNER_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $UNIQUE_KEY" \
-d '{"type":"promo","path":"vod","notional_minor":0,"spec":{"title":"Fortnite","question":"Did the POV player win (Victory Royale)?","game_slug":"fortnite"}}'
# 201: state "open", event_id returned
# 02 Verify: lock it while Vision reads the clip
curl -X POST "$PLLAY_API/v1/events/$EVENT_ID/lock" -H "X-Partner-Key: $PARTNER_KEY" -H "Content-Type: application/json"
# 200: state "locked"
# 03 Settle: empty body. The server sets the outcome.
curl -X POST "$PLLAY_API/v1/events/$EVENT_ID/settle" -H "X-Partner-Key: $PARTNER_KEY" -H "Content-Type: application/json" \
-d '{}'
# 200. The server sets outcome, confidence and model_version.
# 02 Verify: below the threshold, hold instead (hold = no settle)
curl -X POST "$PLLAY_API/v1/events/$EVENT_ID/hold" -H "X-Partner-Key: $PARTNER_KEY" -H "Content-Type: application/json" \
-d '{"reason":"below_threshold"}'
# 200: state "held". Not settled yet.You keep funds, KYC, and media margins. PLLAY returns the settlement_id. Contest per /trust; a reversal is a new entry.
VALORANT: human confirmation required while automatic settlement is under review. Fortnite, League of Legends, Dota 2, PUBG: BATTLEGROUNDS and Rocket League are available and live today. “Available and live today” means the title is offered now for recorded-video settlement, not live-stream settlement. See /coverage#live. Game names are trademarks of their owners, and a listing is not a partnership, sponsorship or endorsement. See /coverage#titles. A clear read on recorded video settles automatically for Fortnite, League of Legends, Dota 2, PUBG: BATTLEGROUNDS and Rocket League. Other titles are in Beta. See /status →
Two requests.
Four webhooks.
Example payloads from the contract. The values are illustrative, not production results.
{
"type": "promo",
"path": "vod",
"notional_minor": 0,
"spec": {
"title": "Fortnite",
"question": "Did the POV player win (Victory Royale)?",
"game_slug": "fortnite"
}
}{}{
"event_id": "3f6c1b2e-8a54-4d1f-9b7a-2c5e0d4a9f10",
"type": "contest",
"path": "vod",
"state": "settled",
"spec": {
"title": "CS2",
"market": "round_14_clutch",
"rule": "last_alive_wins_round"
},
"outcome": "yes",
"confidence": 0.987,
"threshold": 0.9,
"model_version": "vision-vod-2026-09",
"evidence_uri": "https://pllay.io/receipt/3f6c1b2e-8a54-4d1f-9b7a-2c5e0d4a9f10",
"settlement_id": "9b1d7c40-5e2a-4f63-8c1b-7a0e3d2f6b58",
"reversal_settlement_id": null,
"reviewer_id": null,
"stake_ref": "op_contest_55120",
"notional_minor": 0,
"reason": null
}{
"event_id": "c8a24e71-0b36-4f95-a1d2-6e9b5f3c7d04",
"type": "contest",
"path": "vod",
"state": "held",
"spec": {
"title": "CS2",
"market": "round_14_clutch",
"rule": "last_alive_wins_round"
},
"outcome": null,
"confidence": null,
"threshold": 0.9,
"model_version": null,
"evidence_uri": null,
"settlement_id": null,
"reversal_settlement_id": null,
"reviewer_id": null,
"stake_ref": "op_contest_55120",
"notional_minor": 0,
"reason": "below_threshold"
}{
"event_id": "3f6c1b2e-8a54-4d1f-9b7a-2c5e0d4a9f10",
"type": "contest",
"path": "vod",
"state": "reversed",
"spec": {
"title": "CS2",
"market": "round_14_clutch",
"rule": "last_alive_wins_round"
},
"outcome": "yes",
"confidence": 0.987,
"threshold": 0.9,
"model_version": "vision-vod-2026-09",
"evidence_uri": "https://pllay.io/receipt/3f6c1b2e-8a54-4d1f-9b7a-2c5e0d4a9f10",
"settlement_id": "9b1d7c40-5e2a-4f63-8c1b-7a0e3d2f6b58",
"reversal_settlement_id": "a41d02be-7c35-4e18-b9f6-0d83c2e1a574",
"reviewer_id": null,
"stake_ref": "op_contest_55120",
"notional_minor": 0,
"reason": "dispute_overturned"
}{
"event_id": "c8a24e71-0b36-4f95-a1d2-6e9b5f3c7d04",
"type": "contest",
"path": "vod",
"state": "voided",
"spec": {
"title": "CS2",
"market": "round_14_clutch",
"rule": "last_alive_wins_round"
},
"outcome": null,
"confidence": null,
"threshold": 0.9,
"model_version": null,
"evidence_uri": null,
"settlement_id": null,
"reversal_settlement_id": null,
"reviewer_id": null,
"stake_ref": "op_contest_55120",
"notional_minor": 0,
"reason": "unresolvable"
}How webhooks
reach you.
The Event object, no envelope. The event name is in the X-Webhook-Event header.
X-Webhook-Signature: sha256=<hex> of the raw body. Compare it in constant time against the raw body, not a re-serialized JSON object.
X-Webhook-Timestamp is Unix seconds. X-Pllay-Signature is t=<unix seconds>,v1=<hex>. v1 is the HMAC-SHA256 of "<t>.<raw body>" with your webhook secret. Reject a delivery whose t is more than 300 seconds from your clock. The event name is in X-Webhook-Event.
30 s timeout. 3 attempts, 1 s then 4 s apart, only on a 5xx or network error. A 4xx is final.
The API refuses
what we will not do.
| Status | Error | When | Do instead |
|---|---|---|---|
| 401 | unauthorized | X-Partner-Key is missing, unknown, revoked or expired, or lacks the scope the call needs. | Send a valid X-Partner-Key. |
| 422 | validation_failed | Create has an unknown key or is missing type, path or spec, or a settle body field is invalid. | Read details. |
| 422 | server_locked_field | These fields are set by PLLAY; clients must not send them. The server rejects them with 422 server_locked_field, or in transitional mode ignores them and returns a warnings entry. | Omit them. The message is "PLLAY sets outcome, confidence and threshold from its Vision run. Remove these fields." |
| 403 | certification_blocked | The title is not offered for auto-settlement. The message is "Title not offered for auto-settlement". | Send spec.game_slug for a title the server offers, such as fortnite. spec.pov_side (home or away) is required for dota2 and rocketleague. |
| 422 | category_not_offered | spec.category names a category that is not offered yet. Only gaming is. | Leave spec.category out, or use gaming. |
| 422 | live_unreviewed | A live settle has no reviewer_id. reviewer_id is set by PLLAY. Clients must not send it. | Do not send reviewer_id. |
| 422 | below_threshold | Settle with confidence < threshold. | Send hold instead. |
| 422 | promo_stake | A database insert failed, including a threshold outside 0–1 (legacy code name). Not a reused Idempotency-Key, and not related to Pulse or stakes. | Read the message. A same-body replay within 24 hours is a 200, not this error. |
| 409 | illegal_transition | The event exists and the verb is not in the state map. | Read state first; see /settlement. |
| 404 | not_found | A partner event route is called for a missing or not-owned event. The message is "Event not found". | Check the event_id. |
| 403 | reversal_requires_dispute | POST /v1/events/{id}/reverse does not write a reversal. | File a dispute at POST /v1/events/{id}/dispute. The message names that route. |
| 429 | rate_limit_exceeded | More than 100 creates in 60 seconds on one key. | Wait reset_in_seconds. |
One receipt.
Check it yourself.
Each outcome returns the score, the model version and a link to the evidence it was read from. A brand can trace a reward decision back to the settlement record.
- event_id
- 5d0e8a14-3b7c-4f29-a6d1-9c2e47b0f385
- type · path
- promo · vod
- spec
- Fortnite · Victory Royale
- model_version
- vision-vod-2026-09
- decision
- Settled · no person needed
- evidence_uri
- pllay.io/receipt/5d0e8a14-3b7c-4f29-a6d1-9c2e47b0f385Example value · See the example receipt page
- evidence clip
- 00:41.200 → 00:44.800 · 8 frames
- state
- settled
- reviewer_id
- null · vod
- settlement_id
- 7f3ac91e-2b4d-4e60-8a15-c0d9e3f1a762
Get a sandbox key.
Set up with you.
Tell us what you’re building and we’ll set up a sandbox key with you.
Request a key.
Build in the sandbox today.
Keys are issued per partner today: request one and we reply at your work email. The sandbox needs no key. Self-serve signup is coming soon.