← tournament.guruproducers

for organizers, federations, and sport software

Plug your event into the public.

One JSON POST per leaderboard update. tournament.guru renders the public spectator pages — branded with your logo, your sponsors, your scoring rules — and threads competitors across events for the Trophy Room.

You own the scoring

Producers compute rank locally and POST a pre-ranked leaderboard. No platform-side rules engine to fight.

Sport-agnostic shape

Bag weights, golf strokes, chess ratings, race times — same columns + rows JSON. Add a new sport without touching the platform.

Branded per event

Each tournament carries its own logo, colors, hero image, and sponsor strip. Your producer attribution links to your homepage.

One POST, sport-shaped

The SDK handles auth, retries, idempotency. You describe your columns and stamp ranks; we paint.

import { TournamentGuruClient, assignRanksAscending } from "@tournament-guru/sdk";

const guru = new TournamentGuruClient({ apiKey: process.env.GURU_API_KEY! });

const rows = entries.map((e) => ({
  competitorId: e.userId,
  rank: 0,                       // overwritten below
  displayName: e.golferName,
  values: { thru: e.holes, today: e.today, total: e.total },
}));

const ranked = assignRanksAscending(rows, (r) => r.values.total);

await guru.upsertTournament({
  schemaVersion: "v1",
  id: "highland-invitational-r3",
  producer: { id: "highland-tour", displayName: "Highland Tour" },
  name: "Highland Invitational — Round 3",
  startDate: "2026-05-04",
  location: "Pinehurst No. 2, NC",
  state: "in-progress",
  leaderboard: {
    updatedAt: new Date().toISOString(),
    rankingExplanation: "Lowest total stroke score wins.",
    columns: [
      { id: "thru",  label: "Thru",  format: "integer", align: "right" },
      { id: "today", label: "Today", format: "integer", align: "right" },
      { id: "total", label: "Total", format: "integer", align: "right" },
    ],
    rows: ranked,
  },
});

Fishing

// Fishing — heaviest aggregate, ties broken by big fish.
assignRanksDescending(rows, [
  (r) => r.values.weight ?? 0,
  (r) => r.values.big ?? 0,
]);

Running

// Running — fastest chip time wins.
assignRanksAscending(rows, (r) => r.values.timeSec ?? 0);

v1 endpoints

POST/api/v1/tournamentsCreate or replace a tournament
PATCH/api/v1/tournaments/{id}Partial update
POST/api/v1/tournaments/{id}/leaderboardAtomic leaderboard replace
POST/api/v1/tournaments/{id}/eventsAppend to event log
GET/api/v1/tournaments/{producerId}:{tournamentId}Public read
GET/api/v1/producers/meAuth smoke test

Auth: Authorization: Bearer tg_<producerId>_<secret>. Body capped at 200 KB. Default rate limit 60 req/min per producer.

Want a key?

We're issuing producer keys by hand for now. Email hello@tournament.guru with your event, your sport, and where the data lives.