Integrity verification for tournament organizers, leagues and publishers. Clear a roster before the event, check a player's standing on demand, and get told the moment a cleared player stops being clear.
Every partner request carries your key in the X-Guardian-Partner-Key
header. Keys are prefixed gpk_, are shown once when issued, and are
stored here only as a hash — we cannot recover one for you, so treat it like a
password and rotate it if it leaks.
Request
curl https://redeyed.com/api/guardian/reputation?handle=examplePlayer \
-H "X-Guardian-Partner-Key: gpk_your_key_here"
Two optional controls are set per key when it is issued:
429.
A key only reaches the endpoints its scopes allow. Ask for the narrowest set
that does your job — if your bracket software only reads clearance, it does not
need rosters:write.
| Scope | Grants |
|---|---|
rosters:read | List rosters and read their clearance. |
rosters:write | Create, re-check, lock and delete rosters. |
reputation:read | Look up a single player's standing. |
attestations:read | Verify a match attestation token. |
attestations:consume | Redeem an attestation, one time only. |
receipts:read | Verify a signed match receipt. |
reports:write | Submit an integrity report. |
| Status | Meaning |
|---|---|
| 401 | Missing key, unknown key, wrong scope, or an IP outside your allowlist. These are deliberately indistinguishable. |
| 402 | A plan ceiling, not a permission problem — see error for which one. The response carries your current entitlement. |
| 404 | No such roster owned by you. Another organizer's event also returns 404: whether it exists is not something we confirm. |
| 409 | The roster is locked. Reopen it before changing the line-up. |
| 422 | Validation failed. Laravel's standard errors object is returned. |
| 429 | Over your per-minute rate limit. |
| 503 | Guardian is disabled on this deployment. |
A 402 distinguishes a billing ceiling from a permission failure, so
your integration can surface "upgrade" rather than "access denied". Three can occur:
error | Cause |
|---|---|
roster_size_exceeded | More entries than your tier allows on one roster. Nothing is written. |
roster_quota_exceeded | You already hold your tier's maximum number of rosters. |
seat_quota_exceeded | This month's seat allowance is spent. |
Every entry resolves to one of four verdicts, and every verdict carries the reason codes behind it. Show the reasons to whoever makes the call — a disqualification you cannot explain to the player is one you will have to defend without evidence.
| Verdict | Means |
|---|---|
| clear | Nothing outstanding. Eligible. |
| review | Something is worth a look. Playable at your discretion — this is a judgement call we hand back to you, not a refusal. |
| blocked | An active ban or equivalent. Not eligible. |
| unknown | Guardian has no record of this player. |
unknown is not clear. A player we have
never seen has produced no evidence, which is not the same as producing good
evidence. Roster roll-ups count unknown alongside blocked
for exactly this reason — otherwise the cheapest way to pass a roster check would
be to never install Guardian at all. If your rules admit unverified entrants, read
the per-entry verdict rather than summary.cleared.
| Code | Verdict | Meaning |
|---|---|---|
no_guardian_record | unknown | No player and no handle-level history. |
ban:{source}:{severity} | blocked | An active ban. Repeated once per distinct ban. |
player_status_banned | blocked | The player record itself is banned. |
player_status_flagged | review | Flagged by scan history. |
trust_below_threshold | review | Trust score under the configured bar. |
upheld_reports | review | Enough community reports have been upheld against this player. |
no_passing_attestation | review | Never produced a passing scan. |
attestation_stale | review | Their last passing scan is outside the freshness window. A pass describes the machine that produced it, and machines change. |
no_passing_attestation and attestation_stale are
different problems. The first is a player who has never verified; the second is a
player who did, a while ago. The first usually means "ask them to run Guardian",
the second "ask them to run it again".
Idempotent on your own event_ref. Submitting the same reference again
replaces the line-up in place rather than creating a second roster, so you can PUT
your current entrants on a schedule without tracking our identifiers. Entries that
survive a resubmission keep their previous verdict, so a substitution does not read
as every player having just changed status.
The roster is evaluated immediately and the response is the clearance.
| Field | Type | Notes |
|---|---|---|
event_ref | string, required | Your identifier. A–Z a–z 0–9 . _ : -, max 120. |
label | string | Human-readable event name. |
game_profile | string | Defaults to general. Scores prefer an attestation for this title. |
starts_at | date | When the event begins. |
entries[] | array, required | At least one entry. |
entries[].handle | string, required | The player's in-game handle. |
entries[].platform | string | e.g. steam, psn, xbox. |
entries[].team_ref | string | Your own team identifier, echoed back. |
Request
curl -X POST https://redeyed.com/api/guardian/rosters \
-H "X-Guardian-Partner-Key: gpk_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"event_ref": "spring-cup-2026",
"label": "Spring Cup 2026 — Main Bracket",
"game_profile": "warzone",
"starts_at": "2026-09-12T18:00:00Z",
"entries": [
{ "handle": "alpha_one", "platform": "steam", "team_ref": "team-alpha" },
{ "handle": "alpha_two", "platform": "steam", "team_ref": "team-alpha" }
]
}'
201 Created
{
"event_ref": "spring-cup-2026",
"label": "Spring Cup 2026 — Main Bracket",
"game_profile": "warzone",
"status": "open",
"starts_at": "2026-09-12T18:00:00+00:00",
"last_checked_at": "2026-08-18T21:40:11+00:00",
"summary": { "total": 2, "clear": 1, "review": 1, "blocked": 0, "cleared": false },
"entries": [
{
"handle": "alpha_one",
"platform": "steam",
"team_ref": "team-alpha",
"verdict": "clear",
"reasons": [],
"trust_score": 100,
"checked_at": "2026-08-18T21:40:11+00:00"
},
{
"handle": "alpha_two",
"platform": "steam",
"team_ref": "team-alpha",
"verdict": "review",
"reasons": ["attestation_stale"],
"trust_score": 88,
"checked_at": "2026-08-18T21:40:11+00:00"
}
]
}
Returns the roster in the shape above. This is a read of the last evaluation — it does not re-check and does not consume seats, so it is safe to poll for a dashboard.
Re-evaluates every entry against current standing and returns the updated roster. This is the billable operation: it consumes one seat per entry.
recheck for an on-demand answer — the morning of an
event, or when an organizer clicks refresh.
Request
curl -X POST https://redeyed.com/api/guardian/rosters/spring-cup-2026/recheck \
-H "X-Guardian-Partner-Key: gpk_your_key_here"
Send {"status": "locked"} once the line-up is final. Locking stops the
entrants changing; it does not stop clearance refreshing, which is the
point — catching a player going bad between lock and kickoff is the reason this
product exists. Accepted values are open, locked and
archived.
Submitting a new line-up to a locked roster returns 409.
Your 100 most recent rosters with their summaries, plus your current entitlement — tier, seat allowance, seats used this month, and whether webhooks are included. Useful for a usage widget in your own admin.
200 OK
{
"rosters": [
{
"event_ref": "spring-cup-2026",
"label": "Spring Cup 2026 — Main Bracket",
"game_profile": "warzone",
"status": "open",
"starts_at": "2026-09-12T18:00:00+00:00",
"summary": { "total": 2, "clear": 1, "review": 1, "blocked": 0, "cleared": false },
"last_checked_at": "2026-08-18T21:40:11+00:00"
}
],
"entitlement": {
"tier": "league",
"name": "Guardian Roster — League",
"seats_per_month": 40000,
"seats_used": 1260,
"max_rosters": 250,
"max_entries_per_roster": 1000,
"webhooks": true
}
}
A single player's standing, without creating a roster. Takes handle
and an optional platform. Where no player record exists, handle-level
bans and upheld reports are still returned — a ban is not shed by uninstalling.
200 OK
{
"found": true,
"alias": "examplePlayer",
"handle": "examplePlayer",
"platform": "steam",
"status": "clean",
"trust_score": 96,
"flagged": false,
"banned": false,
"active_bans": [],
"verified_accounts": ["steam:examplePlayer"]
}
An attestation is a player's proof, produced by the Guardian client, that their machine passed at a moment in time. Where roster clearance answers "is this player eligible this week", an attestation answers "did this specific machine pass just now" — use it at check-in for a match, not for eligibility across a season.
Reading is idempotent. Consuming is not: it redeems a one-use token and returns a consumption receipt. A second consume of the same token fails validation, which is what stops one pass being shared across a team.
200 OK — verify
{
"valid": true,
"verdict": "clean",
"assurance_level": "content_verified",
"decision_code": "pass",
"game_profile": "warzone",
"competition": {
"organizer": "Spring Cup",
"tournament_ref": "spring-cup-2026",
"match_ref": "qf-3",
"organizer_verified": true,
"platform": "steam",
"account_handle": "alpha_one",
"account_verified": true
},
"player": { "alias": "alpha_one", "status": "clean", "trust_score": 96 },
"expires_at": "2026-09-12T19:00:00+00:00",
"one_use": true,
"consumed": false
}
A signed, after-the-fact record that a match was played under Guardian. Verify one
to settle a dispute weeks later. The response reports
signature_valid separately from admission_valid: a
receipt can be cryptographically genuine and still expired or revoked, and for an
appeal you usually want to know both.
Send an integrity report from your own moderation flow. Reports feed the same
reputation the roster verdicts read from, so an organizer upholding a case makes
the whole network better at catching that player. Fields:
subject_handle (required), subject_platform,
category, description, evidence_url.
200 OK
{ "ok": true, "report_id": 4821 }
Polling tells you a player went bad the next time you ask. Webhooks tell you when it happens, which on the Friday before a Saturday event is the entire difference. Available on paid tiers; configured per partner.
| Event | Fires when |
|---|---|
guardian.roster.entry.changed | An entry's verdict moves. Carries direction: regression or improvement. |
guardian.roster.cleared | A roster reaches fully cleared. Fires on the transition only. |
match.receipt.issued | A match receipt is created. |
match.receipt.finalized | A receipt is finalized. |
match.receipt.revoked | A receipt is revoked. |
review → clear is good news that can wait for someone to look at a
dashboard. clear → blocked the night before a final is not. The
direction field exists so you do not have to re-derive which you got.
Delivery body
{
"id": "3f6c1b02-9a1e-4a1f-9a0c-2d9a7f5f1b22",
"type": "guardian.roster.entry.changed",
"created_at": "2026-08-18T21:44:02+00:00",
"data": {
"event_ref": "spring-cup-2026",
"game_profile": "warzone",
"starts_at": "2026-09-12T18:00:00+00:00",
"entry": {
"handle": "alpha_two",
"platform": "steam",
"team_ref": "team-alpha",
"verdict": "blocked",
"previous_verdict": "clear",
"direction": "regression",
"reasons": ["ban:guardian:permanent"],
"trust_score": 0,
"checked_at": "2026-08-18T21:44:02+00:00"
},
"summary": { "total": 2, "clear": 1, "review": 0, "blocked": 1, "cleared": false }
}
}
Every delivery carries three headers:
| Header | Value |
|---|---|
X-Guardian-Event-Id | Unique per event. Use it to make your handler idempotent — retries reuse the same id. |
X-Guardian-Timestamp | Unix seconds at signing. |
X-Guardian-Signature | v1= followed by the hex HMAC-SHA256 of timestamp + "." + raw_body, keyed with your webhook's signing secret. |
PHP
$raw = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_GUARDIAN_TIMESTAMP'] ?? '';
$provided = $_SERVER['HTTP_X_GUARDIAN_SIGNATURE'] ?? '';
if (abs(time() - (int) $timestamp) > 300) {
http_response_code(400); exit; // Too old — possible replay.
}
$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $raw, $signingSecret);
if (! hash_equals($expected, $provided)) {
http_response_code(400); exit;
}
Node.js
const crypto = require('crypto');
// express.raw({ type: 'application/json' }) — req.body must stay a Buffer.
function verify(req, signingSecret) {
const timestamp = req.get('X-Guardian-Timestamp') || '';
const provided = req.get('X-Guardian-Signature') || '';
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = 'v1=' + crypto
.createHmac('sha256', signingSecret)
.update(timestamp + '.' + req.body.toString('utf8'))
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(provided);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Python
import hashlib, hmac, time
def verify(raw_body: bytes, timestamp: str, provided: str, signing_secret: str) -> bool:
if abs(time.time() - int(timestamp)) > 300:
return False
expected = "v1=" + hmac.new(
signing_secret.encode(),
f"{timestamp}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, provided)
Respond 2xx once you have stored the event. Failed deliveries are
retried, so handlers must tolerate seeing the same
X-Guardian-Event-Id more than once.
Partner credentials are issued per organization, with the scopes and tier agreed up front. Talk to us about your competition and we will get you a sandbox key to build against.