loader

Guardian Partner API

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.

Authentication

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"
Server side only. A partner key carries your organization's entitlement and can read every roster you own. It must never be shipped in a browser bundle, a game client, or anything else an entrant can read.

Two optional controls are set per key when it is issued:

  • IP allowlist — when set, requests from any other address are rejected.
  • Rate limit — requests per minute, per calling IP. Exceeding it returns 429.

Scopes

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.

ScopeGrants
rosters:readList rosters and read their clearance.
rosters:writeCreate, re-check, lock and delete rosters.
reputation:readLook up a single player's standing.
attestations:readVerify a match attestation token.
attestations:consumeRedeem an attestation, one time only.
receipts:readVerify a signed match receipt.
reports:writeSubmit an integrity report.

Errors & limits

StatusMeaning
401Missing key, unknown key, wrong scope, or an IP outside your allowlist. These are deliberately indistinguishable.
402A plan ceiling, not a permission problem — see error for which one. The response carries your current entitlement.
404No such roster owned by you. Another organizer's event also returns 404: whether it exists is not something we confirm.
409The roster is locked. Reopen it before changing the line-up.
422Validation failed. Laravel's standard errors object is returned.
429Over your per-minute rate limit.
503Guardian is disabled on this deployment.

Quota errors

A 402 distinguishes a billing ceiling from a permission failure, so your integration can surface "upgrade" rather than "access denied". Three can occur:

errorCause
roster_size_exceededMore entries than your tier allows on one roster. Nothing is written.
roster_quota_exceededYou already hold your tier's maximum number of rosters.
seat_quota_exceededThis month's seat allowance is spent.
What a seat is. One player checked on one roster, once, counts as one seat. Re-checking a ten-player roster costs ten seats. Duplicate handles within a submission are collapsed before billing, so the same player listed twice is charged once.

Verdicts & reason codes

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.

VerdictMeans
clearNothing outstanding. Eligible.
reviewSomething is worth a look. Playable at your discretion — this is a judgement call we hand back to you, not a refusal.
blockedAn active ban or equivalent. Not eligible.
unknownGuardian 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.

Reason codes

CodeVerdictMeaning
no_guardian_recordunknownNo player and no handle-level history.
ban:{source}:{severity}blockedAn active ban. Repeated once per distinct ban.
player_status_bannedblockedThe player record itself is banned.
player_status_flaggedreviewFlagged by scan history.
trust_below_thresholdreviewTrust score under the configured bar.
upheld_reportsreviewEnough community reports have been upheld against this player.
no_passing_attestationreviewNever produced a passing scan.
attestation_stalereviewTheir 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".

Create or replace a roster

POST /api/guardian/rosters rosters:write

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.

FieldTypeNotes
event_refstring, requiredYour identifier. A–Z a–z 0–9 . _ : -, max 120.
labelstringHuman-readable event name.
game_profilestringDefaults to general. Scores prefer an attestation for this title.
starts_atdateWhen the event begins.
entries[]array, requiredAt least one entry.
entries[].handlestring, requiredThe player's in-game handle.
entries[].platformstringe.g. steam, psn, xbox.
entries[].team_refstringYour 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"
    }
  ]
}

Read clearance

GET /api/guardian/rosters/{event_ref} rosters:read

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-check a roster

POST /api/guardian/rosters/{event_ref}/recheck rosters:write

Re-evaluates every entry against current standing and returns the updated roster. This is the billable operation: it consumes one seat per entry.

If you have webhooks, you do not need to poll this. We re-check on your behalf and push changes. Use 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"

Lock & reopen

POST /api/guardian/rosters/{event_ref}/status rosters:write
DELETE /api/guardian/rosters/{event_ref} rosters:write

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.

List rosters

GET /api/guardian/rosters rosters:read

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
  }
}

Player reputation

GET /api/guardian/reputation reputation:read

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"]
}

Match attestations

GET /api/guardian/attestation/{token} attestations:read
POST /api/guardian/attestation/{token}/consume attestations:consume

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
}

Match receipts

GET /api/guardian/receipts/{receipt} receipts:read

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.

Submit a report

POST /api/guardian/report reports:write

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 }

Webhooks

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.

EventFires when
guardian.roster.entry.changedAn entry's verdict moves. Carries direction: regression or improvement.
guardian.roster.clearedA roster reaches fully cleared. Fires on the transition only.
match.receipt.issuedA match receipt is created.
match.receipt.finalizedA receipt is finalized.
match.receipt.revokedA receipt is revoked.
Page on regressions, not on everything. A player moving 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 }
  }
}
Roster events are delivered only to the partner that owns the roster. Your entrants and their standing are never sent to another organizer.

Verifying signatures

Every delivery carries three headers:

HeaderValue
X-Guardian-Event-IdUnique per event. Use it to make your handler idempotent — retries reuse the same id.
X-Guardian-TimestampUnix seconds at signing.
X-Guardian-Signaturev1= followed by the hex HMAC-SHA256 of timestamp + "." + raw_body, keyed with your webhook's signing secret.
Sign over the raw request body, exactly as received. Parsing the JSON and re-encoding it will change the bytes and the signature will not match. Compare digests with a constant-time function, and reject timestamps far from your own clock so an old delivery cannot be replayed.

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.

Getting a key

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.