P
TrustPadel
P
TrustPadel

Weekly · free · unsubscribe anytime

Sign up to the Padel Feed

One email each Thursday: matches worth watching, new venues + courts, equipment drops, integrity-feed updates. No spam, no marketing partners — just padel.

TrustPadel

The world's padel directory. Find courts, improve your game, connect with venues.

App Store

Coming soon

Google Play

Coming soon

Play

  • Open games
  • Tournaments
  • Leagues
  • Events
  • Quests

Compete

  • Rankings
  • Records
  • Head-to-head
  • How Crest works
  • Get your first Crest

Find

  • All venues
  • Coaches
  • Shops + gear
  • Academy
  • Junior + youth

Business

  • Club dashboard
  • Claim a venue
  • Insights marketplace
  • Developer portal
  • Data licence

Company

  • About
  • Ethos
  • Transparency
  • Integrity ladder
  • Contact

© 2026 TrustPadel by BuiltByGo. All rights reserved.

PrivacyTermsCookies
Developer portal · Phase 4K

Build on the Crest standard

Two surfaces: the keyless Padel Decoder (open data — venues, tournaments, coaches) and the keyed Crest API for player ratings, history, webhooks. This page covers the keyed surface. The same buyer licence applies whenever you consume Crest data downstream.

Quickstart

  1. Sign up + create a key in /account/api-keys. Keys start in Free tier; upgrade by emailing [email protected].
  2. Copy the plaintext key shown once. Format is crst_xxxxxxxx.<secret> — the prefix is visible in logs; the secret half is bcrypt-hashed in storage.
  3. Authenticate with Authorization: Bearer crst_xxxxxxxx.<secret>.
  4. Test against the sandbox players before pointing at production handles. The minor + private fixtures specifically verify your code handles the 404-indistinguishable invariant.

Code samples

Three minimal clients hitting the public Crest endpoint. Each handles the 404-indistinguishable invariant correctly (treat “not found” as “no public Crest”, never as “account doesn't exist”).

curl

KEY="crst_xxxxxxxx.your_secret"
curl -sS \
  -H "Authorization: Bearer $KEY" \
  https://trustpadel.com/api/v1/players/sandbox-gold/rating
# 200 → { "handle": "sandbox-gold", "crest": { ... } }
# 404 → opaque; could be missing, minor, or private

JavaScript / TypeScript

async function getCrest(handle: string) {
  const res = await fetch(
    `https://trustpadel.com/api/v1/players/${encodeURIComponent(handle)}/rating`,
    {
      headers: { Authorization: `Bearer ${process.env.TRUSTPADEL_KEY}` },
    },
  )
  if (res.status === 404) return null      // missing OR minor OR private — same UI
  if (res.status === 429) {
    const retry = Number(res.headers.get('Retry-After') ?? 60)
    throw new Error(`Rate limited; retry in ${retry}s`)
  }
  if (!res.ok) throw new Error(`Crest API error: ${res.status}`)
  return res.json()
}

Python

import os, httpx

def get_crest(handle: str) -> dict | None:
    r = httpx.get(
        f"https://trustpadel.com/api/v1/players/{handle}/rating",
        headers={"Authorization": f"Bearer {os.environ['TRUSTPADEL_KEY']}"},
    )
    if r.status_code == 404:
        return None  # missing | minor | private — opaque by design
    r.raise_for_status()
    return r.json()

Webhook signature verification

import crypto from 'node:crypto'

export function verifyCrestWebhook(req, signingSecret) {
  const sig = req.headers['x-crest-signature']
  if (!sig) return false
  const [ts, hex] = sig.split(',')
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false  // ±5min
  const expected = crypto
    .createHmac('sha256', signingSecret)
    .update(`${ts}.${req.rawBody}`)
    .digest('hex')
  return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(hex, 'hex'))
}

Endpoints

  • GET /api/v1/players/{handle}/rating

    Resolve a public handle to its top-discipline Crest + tier. 404 for non-public players (minors, private, non-L2) with no distinction.

  • GET /api/v1/players/{handle}/crest/{discipline}

    Per-discipline Crest: crest_number, crest_tier, provisional, confidence_band, as_of. Same 404 invariant.

  • GET /api/v1/players/{handle}/history

    Last N integrity-clean rating events (mu / sigma / crest deltas). Disputed + sandbagging-flagged events drop until resolved. history:read scope (Developer+ tier).

  • GET /api/v1/players/{handle}/badge

    Embeddable SVG badge — tier colour-keyed. Response is image/svg+xml; query params theme (light/dark), size (small/medium), discipline. Cacheable (5 min TTL). Returns a neutral “Unrated” badge for non-public players (no 404 leak).

  • GET /api/v1/venues/search

    Open + keyless. Use for venue discovery — see /decoder for full docs.

All keyed routes count toward your tier rate limit. Keyless /decoder routes don't. Deprecation + Sunset headers signal scheduled changes per RFC 8594 with a 6-month minimum window. Machine-readable spec (OpenAPI 3.1).

Rate limits + tiers

TierReq/minReq/monthNotes
Free6010,000No card; sandbox-suitable.
Developer6001,000,000Production integrations + small SaaS volumes.
Pro3,00010,000,000Federation + media + analytics tooling.
EnterprisenegotiatednegotiatedBespoke SLA + dedicated support. Talk to us.

Tier upgrades route through billing — email [email protected] with your prefix + intended use.

Scopes

Keys carry a scope set. Write scopes (results:write, webhooks:manage) require Developer tier or above. Scope a key tightly: a credential lost from a read-only integration can't mutate data.

  • player:readResolve handles + read public Crest data per discipline.
  • history:readRead a player's confirmed match history (above the integrity gate).
  • discovery:readSearch rankings, leaderboards, recent activity.
  • badge:readRender the embeddable Crest badge SVG / PNG.
  • results:writeSubmit match results from a verified ingest partner. Developer+ tier.
  • webhooks:manageCreate + manage webhook endpoints. Developer+ tier.

Webhooks

Configure endpoints per key in /account/api-keys. Endpoints must be HTTPS. Delivery is at-least-once with exponential backoff. After repeated failures the endpoint pauses automatically; you resume it from the same page.

Signing

Each request carries an X-Crest-Signature header containing an HMAC-SHA256 over <timestamp>.<raw-body> keyed by the signing secret returned at endpoint creation. Reject requests where the timestamp is more than ±5 minutes from your clock to prevent replays. Always verify the signature before processing the payload.

# Pseudocode
sig_header = req.headers['X-Crest-Signature']
ts, sig = sig_header.split(',')
if abs(now() - int(ts)) > 300: reject
expected = hmac_sha256(signing_secret, f"{ts}.{raw_body}")
if not const_time_eq(expected, sig): reject

Events

  • crest.updatedA player's Crest number / tier changed after a confirmed match.
  • result.confirmedA match transitioned to confirmed (all parties agreed, above integrity gate).
  • result.disputedA confirmed match entered dispute and is quarantined from feeds.
  • player.handle_changedA player renamed; their old handle 301s in handle_history.
  • identity.mergedTwo records were resolved into one through the identity-resolution queue.
  • integrity.flag_raisedA Layer-2 or Layer-3 integrity signal fired against a player or match.
  • subscription.entitlement_changedA dataset entitlement was issued / renewed / revoked.

Sandbox players

Six fixture accounts cover the locked invariants. Hit these against your integration before pointing at production. The sandbox-minor and sandbox-private fixtures specifically verify the 404 invariant — your code must render the same UI for both as for a truly absent player.

  • sandbox-gold

    200 OK — adult, Gold tier, ~1750 Crest, public.

  • sandbox-diamond

    200 OK — adult, Diamond tier, ~2400 Crest, public + Trust+ active.

  • sandbox-provisional

    200 OK — provisional flag true, confidence band wide.

  • sandbox-minor

    404 — minor account. NEVER 200. Use this to verify your integration honours the indistinguishable-404 invariant.

  • sandbox-private

    404 — adult with rating_public=false. Same 404 shape as the minor case (the absence-is-indistinguishable rule).

  • sandbox-disputed

    200 OK — but the most recent match is disputed; the matching result.disputed webhook fires on test events.

Locked invariants

These are non-negotiable across every endpoint, every tier, every release. If your integration depends on the inverse of any of these, please choose a different integration.

  1. Minor + private return indistinguishable 404. No status code, no field, no metadata distinguishes a missing player from a minor or private one. Your UI must not infer existence from latency, header, or response shape.
  2. Article-9 tags never appear in feeds. Identity, condition, and preference tags are protected and absent from every keyed response. If a field that looks Art-9 ever appears, please report it immediately.
  3. Provisional Crests are clearly marked. The provisional boolean + confidence_band let you avoid presenting unstable ratings as firm.
  4. Integrity-gated commercial feeds. Records below the provenance or integrity threshold are absent from the licensed feeds, not flagged in them. Re-pulls after disputes resolve will surface the corrected record.

Questions? [email protected] · Security: [email protected] · Integrity reports: [email protected].