UFCalendar Fight API โ€” REST reference

Fight data API by UFCalendar โ€” MMA + bare-knuckle boxing: events, full cards, results, per-fight and per-round statistics, fighters with complete multi-promotion careers, official rankings history (UFC 2013โ†’), judges' scorecards (every round, every official, UFC back to 1995), UFCalendar Power Index ratings, AI win probabilities and per-country broadcast rights โ€” for UFC, PFL, OKTAGON, BKFC and RIZIN.

Keys live at ufcalendar.com/account/api โ€” an active plan or the free 1-day trial (100 requests, no card) is required. Plans and quotas: ufcalendar.com/developers.

Built for indie developers, side projects and small teams: fantasy apps, stats sites, Discord bots, analytics projects. Plans are priced for them: self-serve only, no sales-negotiated contracts, and none planned.

Conventions: snake_case JSON in a {"data": ..., "meta": ...} envelope (meta is ALWAYS present, {} at minimum); cursor pagination; UTC ISO-8601 timestamps plus venue IANA tz; errors as {"error": {"code", "message", "request_id"}} (every code is in the ErrorCode schema); monthly-quota headers X-RateLimit-Limit/-Remaining/-Reset and Retry-After on 429. Every GET /v1 200 carries a weak ETag and Cache-Control: private, no-cache; send If-None-Match and an unchanged payload answers 304 with no body. Every response body is described under components.schemas โ€” closed objects, nullable as [T, "null"], every vocabulary an enum โ€” so generated clients get real types.

Odds (2026-09): one UFCalendar consensus line per corner: the mean across the sportsbooks we track (book identities are never exposed; sources is how many backed each point), refreshed every 30 minutes in the 48 hours before a card and every 2 hours otherwise. Current, opening and closing lines ship on every plan (/v1/fights/{id}/odds, /v1/events/{id}/odds, include=odds); the full movement series is Pro and up (/v1/fights/{id}/odds/history). Information only, not betting advice.

Source labels (2026-09-26): source / source_slug fields name official publishers only โ€” ufc-stats, ufc-official, ufc-official-cards, a promotion's own *-official slug, wikipedia, or ufcalendar for our own derivations. Every other publisher now ships as public-records (career records and history rows) or commission-record (judges' scorecards); credential sources list public reference URLs only.

Fighter images are Wikimedia Commons/CC only โ€” the license/artist fields you receive MUST be displayed as a credit. Not affiliated with UFC/Zuffa/TKO or any promotion.

Both official SDKs (pip install ufcalendar, npm install @ufcalendar/sdk) are configured with the base URL https://api.ufcalendar.com/v1 โ€” the same base every code sample on this page uses.

Base URL https://api.ufcalendar.com/v1 ยท auth Authorization: Bearer <key> ยท OpenAPI 3.1 spec ยท llms.txt ยท Pricing & free trial ยท MMA Fight Data API for UFC ยท UFC rankings history ยท UFC fight stats ยท MMA API ยท MCP server for UFC data ยท MMA MCP server

Also on: GitHub (Python client) ยท PyPI: pip install ufcalendar ยท RapidAPI Hub ยท Postman workspace ยท npm: @ufcalendar/sdk ยท GitHub (TypeScript client) ยท Agent skill ยท MCP server (GitHub)

MCP server: https://api.ufcalendar.com/mcp โ€” the same data as a Model Context Protocol server (stateless Streamable HTTP), with 47 tools. Connect with an API key as a bearer token, or sign in with your UFCalendar account; discovery document at /.well-known/oauth-protected-resource. One tool call counts as one request against your plan.

Endpoints (53)

Orgs

GET /v1/orgs โ€” List launch orgs with capability flags

Each org carries flags (stats, rounds, rankings, broadcasts, predictions, scorecards, odds) so clients discover coverage programmatically โ€” plus sport (mma, bare-knuckle-boxing) and country_code (ISO-2). Staged orgs (ONE, KSW, ACA) appear the day they clear the reliability bar. RIZIN publishes no official board, so its rankings flag is false. scorecards is true for UFC, PFL, OKTAGON and RIZIN; no commission covers bare-knuckle, so BKFC is false.

GET /v1/orgs/{slug} โ€” One org

Single org by slug. Same shape as a list row.

GET /v1/orgs/{slug}/divisions/{division} โ€” One division in one promotion

One weight class in one promotion, in one call: division (slug, canonical name, weight_limit_lbs โ€” null where no standard limit applies), rankings (the promotion's current official board for this division โ€” same object as /v1/rankings/{org}?division= โ€” or null when it publishes none), upcoming (up to 50 bouts booked at this weight, soonest first), recent (the 15 latest completed or no-contest bouts, newest first) and roster (up to 30 fighters who have fought at this weight, most recent first, each with last_fought_at). Bouts are the /v1/fights/{id} shape plus their event (id, slug, title, org, starts_at); a bout is matched on its recorded weight class, so catchweights are not counted. When every list is empty meta.note says so. An unknown org or division 404s; the division 404 lists the allowed slugs.

Events

GET /v1/events โ€” List events (schedule + results)

A bare listing is the upcoming calendar: events from the last ~6h onward, soonest first. Ordering defaults: explicit order always wins; else from= or a forward status (announced|scheduled|live|upcoming) โ†’ asc; else (completed|cancelled|postponed|to=) โ†’ desc, newest first. order=desc with no filters browses the full archive newest-first. Live results land within โ‰ค5 minutes (UFC โ‰ค2). Rows are the list shape (no card) โ€” short_title, numbering and the three section clocks (main_card_at, prelims_at, early_prelims_at, null when the org publishes no split) ride along.

GET /v1/events/{idOrSlug} โ€” Event with full card

Full fight card (both corners, results when completed), venue and event-scoped broadcast rows. Every event and card entry carries sport (e.g. mma, bare-knuckle-boxing). Renamed events 308-redirect from stale slugs. include=eta adds eta (ISO timestamp or null) to every card entry: the estimated start of that bout, by the same rule the website's event page uses. These are estimates, not a schedule โ€” each card section anchors on its declared start, and every completed bout re-anchors the rest of its section on its real finish. Cancelled bouts get null. include=odds adds odds to every card entry: the current UFCalendar consensus line (a, b, favourite, fair_probability_a, sources, recorded_at), or null when the bout is unpriced. Information only, not betting advice; the opening/closing lines and movement are on /v1/events/{idOrSlug}/odds.

GET /v1/events/{idOrSlug}/changes โ€” Card-change log

The diff log behind "card updated": fight added/removed, opponent swapped, card order or date moved, or a fighter profile merged (fighter-merged: two of our fighter ids turned out to be one athlete, so a live bout now carries the surviving id โ€” same humans, new ids). Each entry has id, kind, before, after, observed_at, newest first. No-op rows and flaps (a bout scratched and reinstated within 48 h, a start time moved and moved back within 72 h) are filtered out โ€” the same rule the website and the card.changed webhook apply. /v1/changes is the same log across every event.

GET /v1/events/{idOrSlug}/storylines โ€” Storylines of one card

The talking points of one card, computed from the record โ€” the pre-fight read, as of the card's start (a completed card reads as it did walking in). summary: title_fights, ranked_fighters, champions and closest_bout (the bout nearest 50/50). fights[]: each non-cancelled bout with both corners (id, slug, name, rank on the promotion's official board on the event date โ€” 0 = champion, null = unranked โ€” and streak, signed: positive = consecutive wins, negative = consecutive losses), win_probability_a with probability_source (model = UFCalendar model output, UFC bouts only, corner-guarded; power_index = the UFCalendar Power Index curve; null when neither exists) and tags[]: title, eliminator (both corners ranked #1โ€“#4), coin_flip (within 6 points of 50/50), both_streaking (a, b โ‰ฅ 2), finishers (pct, average recent finish rate โ‰ฅ 60%), rematch (a_wins, b_wins) or trilogy_decider (wins, level after two or more meetings). card: avg_age (4+ known ages), tallest, longest_reach, nations (country, count), streak_leaders (3+ wins, top 3), debuts (first bout in this promotion), returns (365+ days out, months, top 3) and fastest_finish (the quickest career round-1 KO/TKO/submission among the fighters). No fan picks, no combined record. Win probabilities are not betting advice.

GET /v1/events/{idOrSlug}/pickem โ€” Pick'em splits for one card

How the UFCalendar community is picking each bout on one card, from the site's own pick'em game: fights[] in card order (non-cancelled bouts), each with fight_id, both corners (fighter_a, fighter_b: id, slug, name), picks_a, picks_b, total and pct_a (the percentage of picks on corner a, 0โ€“100 with one decimal; null when nobody has picked the bout). Counts only โ€” never who picked. Crowd sentiment, not a market and not a forecast. An unknown event 404s.

GET /v1/changes โ€” Card-change feed across events

Every card change across the launch promotions (or one of them), newest first โ€” the same audit trail the card.changed webhook delivers, for callers that poll instead. Kinds: fight-added, fight-cancelled, fight-reinstated, opponent-changed, time-changed, venue-changed, fighter-merged. Each entry carries id, kind, before, after, observed_at and its event (id, slug, title, org, starts_at). No-op rows and flaps (a bout scratched and reinstated within 48 h, a start time moved and moved back within 72 h) are filtered out, exactly as on the per-event log and the webhook. since defaults to 90 days ago. Unknown kind 400s invalid_kind; a bad since 400s invalid_date.

GET /v1/venues โ€” Search venues

Arenas and venues that have hosted (or have booked) a launch-org event, alphabetical. q (at least 2 characters) matches the name or the city, accent-insensitive; country takes an ISO-2 code (US) or an English country name (Brazil). Each row is id, slug, name, city, region, country, tz, capacity โ€” pass the id to /v1/venues/{id}/events for what was fought there. A q under 2 characters 400s invalid_query.

GET /v1/venues/{id}/events โ€” Events at a venue

Every launch-org event held (or booked) at one venue, newest first โ€” upcoming cards on top, then the history. Same filters and row shape as /v1/events (status, from, to, order, cursor pagination); order=asc reverses it. meta.venue names the venue. An unknown venue, or one with no covered event at this venue (a staged-org-only arena), 404s not_found.

GET /v1/venues/{id} โ€” Venue

Venue with city, region, country (+ country_code), coordinates, capacity and IANA tz.

GET /v1/calendar/{org}.ics โ€” ICS calendar feed

Subscribe your calendar app to an org's schedule. Calendar apps can't send headers, so pass the key as ?key=. Refresh hourly at most. Three shapes: {org}.ics is one entry per event (first bell to the estimated last bout, full card and results in the description); {org}-sections.ics splits each card into early prelims, prelims and main card at their declared start times; {org}-fights.ics has one TENTATIVE entry per bout at its estimated start, linking the bout page. No betting odds.

Fights

GET /v1/fights/{id} โ€” One bout

Bout with corners (FighterRef with country_code), sport (bout override, else the org sport), weight_class + division_slug, result (raw method plus method_normalized / finish_detail / round / time / time_seconds / referee) and bonus flags, plus a compact event. include=odds adds odds: the current UFCalendar consensus line (OddsPair), or null when the bout is unpriced โ€” information only, not betting advice; opening, closing and movement are on /v1/fights/{id}/odds.

GET /v1/fights/{id}/stats โ€” Per-fight totals

Both corners: knockdowns, significant/total strikes, takedowns, submission attempts, reversals, control time, and target/position splits (head/body/leg, distance/clinch/ground) where the source provides them. Each line carries corner (a = the bout's fighter_a, b = fighter_b) beside the stored fighter_position (1/2). distance_time_sec, clinch_time_sec, ground_time_sec (integers, nullable) are seconds spent at distance / in the clinch / on the ground, and can appear alongside official strike counts on any org (position time is filled in even where the rest of the row comes from the official stats).

GET /v1/fights/{id}/rounds โ€” Round-by-round stats

Per-round stat lines for both corners โ€” each with round (and the stored round_number twin) and corner โ€” including target/position splits (head/body/leg, distance/clinch/ground) where they are logged per round (BKFC for head/body/distance/clinch; other orgs may additionally carry leg/ground). distance_time_sec, clinch_time_sec, ground_time_sec (integers, nullable) are seconds spent at distance / in the clinch / on the ground, per round.

Fighters

GET /v1/fighters โ€” Roster search

Fighters with at least one launch-org fight (~4.5k). q is accent-insensitive. Rows are identity only (FighterRef plus second_nationality); country_code is ISO-2 derived from nationality. country accepts an ISO-2 code (matched against every name that maps to it) or a country name.

GET /v1/fighters/{idOrSlug} โ€” Fighter profile

Bio, records, stats, UFCalendar Power Index summary, and CC-licensed images with attribution metadata (displaying the credit is a license requirement). Read records and stats (added 2026-09-24): records is a map keyed by a clean name โ€” pro_mma (the career MMA record), amateur_mma, pro_kickboxing, โ€ฆ, and the org slug for a record INSIDE one promotion (ufc, one, bkfc) โ€” each { value, source, scope }; stats is ONE per-minute panel (SLpM, striking accuracy/defense, SApM, takedown average/accuracy/defense, submission average), always the widest sample UFCalendar holds (the pro-mma panel: every promotion), with basis: { bouts, orgs, source, scope } stating exactly which bouts it averages over (e.g. 7 UFC bouts, or 18 across UFC + Oktagon); null when no real panel exists. career_stats is the raw per-source row list these are built from and stays unchanged for existing integrations. next_fight is the fighter's next booked bout (not completed or cancelled, on an event that has not been cancelled and started no more than ~6h ago; result is always null) and last_fight their most recent completed or no-contest bout โ€” both in the same shape as a source: "native" row of /history, or null. ?include=bonuses adds bonuses: the UFC post-fight bonus ledger โ€” counts of Fight / Performance / KO / Submission of the Night (fotn, potn, kotn, sotn), their total, and the fights that earned them, newest first. Fight of the Night is credited to both corners, the other awards to the winner only. Award counts only, no dollar amounts. ?include=credentials adds credentials: the fighter's combat-sports background, researched by UFCalendar from public sources โ€” credentials[] (kind: belt-rank, competition-result, professional-title, national-team, base-style, training-started, athletic-education, sports-club, award, sport-rank, record; discipline, competition, placement, rank, year, detail (kind-specific extras), note) and affiliations[] (kind: gym, coach, training-partner, manager; name, role, status current/former, is_primary, location, gym, discipline, relationship, as_of, period, note). EVERY row carries sources (the public reference URLs behind the claim โ€” Wikipedia, the promotion's own pages, official bodies; may be empty, since only public references are listed) and confidence (confirmed = an official body or 2+ independent sources, reported = one credible outlet); unverified claims are never served. meta.note asks you to verify before republishing. Both lists may be empty. Comma-separate to combine (include=bonuses,credentials). An unknown include value is 400 invalid_include.

GET /v1/fighters/{idOrSlug}/history โ€” Complete career timeline

The fighter's FULL multi-promotion career: native tracked bouts (richest data, source: "native") merged with the wider multi-promotion career record (source: "history"). Spans every promotion they fought in โ€” not just launch orgs. Every key is present on every row (null when unknown), and each row carries status (one vocabulary across both sources: win, loss, draw, no_contest, cancelled, upcoming, unknown), method_normalized, time_seconds and an opponent object ({id, slug, name}, ids only on native rows) beside the raw result / method / time strings. Newest first; meta.truncated is true when the history rows hit their 500-row cap. source_slug names the publisher: ufcalendar on native rows; on history rows an official publisher (ufc-official, wikipedia, a promotion's *-official), else public-records.

GET /v1/fighters/{idOrSlug}/stats โ€” Career statistics

Raw per-source career rows, plus the readable split in meta.records / meta.stats (the same objects the fighter profile carries โ€” read those unless you need the per-source rows). data: per-scope career stats (pro-mma, amateur-mma, โ€ฆ): record, strikes landed/absorbed per minute, accuracy/defense, takedown and submission averages, and updated_at (this route only). Exactly ONE row per scope; source names the publisher it came from: public-records (the complete multi-promotion career record) is preferred, then the promotion's own site (ufc-stats, bkfc-official, one-official, โ€ฆ), then ufcalendar. A public-records record is a point-in-time snapshot advanced by every settled bout UFCalendar tracked after it, so it matches the record on ufcalendar.com. A row's record and its stats cover the same bouts. pro-mma = the career record plus striking/grappling averages UFCalendar computes over EVERY settled bout it holds per-fight totals for, in any promotion (Contender Series included); its stats are null when no such bout exists. Org-scoped rows (ufc-only, bkfc-only, one-only) are the fighter's record INSIDE that promotion, not a career record: ufc-only carries a record AND averages UFCalendar derives from the fighter's tracked UFC bouts only (Contender Series excluded, W-L-D plus (n NC); both null until a UFC bout is settled โ€” a Contender-Series-only fighter has no UFC stats), while bkfc-only / one-only carry the promotion's own published record. ufcstats', OKTAGON's and ACA's header records are CAREER records and ship under pro-mma (the public-records row wins the scope when present, unless ufcstats counts more bouts).

GET /v1/fighters/{idOrSlug}/rankings โ€” Ranking history

Official-board rows over time (org, board in the public vocabulary โ€” official / meta โ€” division slug, snapshot_date, rank, is_champion), newest snapshot first. Rank 0 = champion; a snapshot is valid until superseded. Cursor-paginated since 2026-09-24 (limit 1โ€“500, default 100) โ€” long careers exceed the old silent 500-row cap.

GET /v1/fighters/{idOrSlug}/power-index โ€” Power Index trajectory

Per-bout rating history from the UFCalendar Power Index engine (rating before/after, model win probability, outcome 1 / 0 / 0.5 and its word twin result). The NEWEST 500 points, served oldest-first; meta.truncated is true when an older tail was dropped.

GET /v1/compare โ€” Compare two fighters

The tale of the tape in one call. a and b are fighter slugs or ids (a renamed slug is followed and named in meta.resolved); both are required and must differ (400 invalid_query), and each must be on the roster (404 not_found). Returns both bios (the /v1/fighters/{idOrSlug} bio fields plus age), career_stats (the /v1/fighters/{idOrSlug}/stats rows), aggregates per fighter over every completed bout with per-fight totals (fights_analyzed, average and best significant strikes and takedowns, average control time in seconds, and the strike mix: location_pct head/body/leg and position_pct distance/clinch/ground, whole percents, null when nothing was logged), recent_form (the last five settled entries of the merged multi-promotion timeline, /v1/fighters/{idOrSlug}/history rows), win_streak, head_to_head (their previous meetings, from a's side), common_opponents (up to 5: the shared opponent with each fighter's most recent bout against them), booked_bout (the upcoming or live bout between them in a covered promotion, in its stored corner order, or null), prediction (UFCalendar model win probability for that booked bout, oriented to a โ€” UFC only, served only while its corner stamp matches the bout, else null) and power_index (each fighter's UFCalendar Power Index rating, peak and fights rated, and win_probability_a from the two ratings). Model output is information, not betting advice.

Rankings

GET /v1/rankings/{org} โ€” Official board (point-in-time)

The time machine: pass ?date=YYYY-MM-DD for the board as it stood on any date (UFC official back to 2013, 543 snapshots). A snapshot is valid until superseded. Rank 0 = champion; P4P has no rank 0. Every entry carries movement against the previous snapshot of the same board (previous rank minus current rank: positive moved up, 0 held, null when not on the previous board) and is_new (true when there was a previous snapshot and the fighter was not ranked in that division on it), plus nationality and country_code (ISO-3166 alpha-2). Fighters are matched by division + name, so a fighter moving divisions reads as new. meta.previous_snapshot_date / meta.next_snapshot_date name the neighbouring snapshots (null at either end) โ€” pass one as date to step through history.

GET /v1/rankings/{org}/{division} โ€” One division

Same as the board endpoint, filtered to a division (lightweight, womens-strawweight, pound-for-pound, โ€ฆ) โ€” entries carry the same movement / is_new / nationality / country_code, and meta the neighbouring snapshot dates.

GET /v1/champions โ€” Current champions

Rank-0 rows of each launch org's latest official board, with the champion's nationality and country_code (ISO-3166 alpha-2). org= narrows to one promotion and division= to one weight class; an unknown value 400s (invalid_org / invalid_division) rather than widening the answer.

Scorecards

GET /v1/fights/{id}/scorecards โ€” Judges' scorecards for a bout

The official athletic-commission record: every judge, their score for each round, their card total, the decision type and any point deductions. UFC back to 1995, PFL to 2018, OKTAGON to 2025, RIZIN from March 2026 (Japan has no athletic commission โ€” the record is the one published by the officials body that judges RIZIN). UFC cards are cross-checked against the official scorecards ufc.com publishes (Dana White's Contender Series and Road to UFC included, from March 2022), which also fill bouts the commission record does not cover. source says who published the cards: commission-record, ufc-official-cards, or a promotion's own slug (oktagon-official, pfl-official, aca-official). total_a/total_b and every round are oriented to the bout's fighter_a_id/fighter_b_id, which are repeated on the payload so you never have to fetch the fight to read a card. winner_fighter_id is who THAT judge gave it to. Check scores_known before charting totals. When it is false the commission published only the outcome, and total_a/total_b are a 1-0 / 1-1 / 0-0 placeholder rather than a score (66 fights, nearly all pre-2005); rounds is empty on those cards. A bout with no commission record answers 200, not 404 (2026-09-24): cards: [], decision_type: null, and meta.note says why (a finish, or cards not yet published). 404 is reserved for an unknown fight, so a client can tell the two apart. Some cards carry no judge identity. PFL's US events have no published commission scorecard, so those bouts are served from PFL's own results post: three cards with judge_id/judge_name null, total_a/total_b set and rounds empty. scores_known is still true โ€” the totals are real. Europe/UAE PFL cards and every other org carry the named, round-by-round record. Media-member and fan scorecards are not part of this dataset.

GET /v1/scorecards/splits โ€” Split and majority decisions

Completed bouts the judges decided on a split or majority (including majority and split draws), newest first, across the launch promotions or one of them, optionally in a date range. Each row is the bout (same shape as /v1/fights/{id}) plus its event, the decision_type, official_winner_fighter_id (null on a draw), every judge's card (cards, same objects as /v1/fights/{id}/scorecards) and dissenting_judges โ€” the cards that disagree with the official result (judge_id, judge_name, total_a, total_b, winner_fighter_id, is_draw, scores_known). A card with no readable verdict is not counted as a dissent; an outcome-only card (scores_known: false) is, flagged. Commission records only โ€” facts as filed, no verdict of our own; sources that publish no decision type are not listed. Up to 50 rows a page.

GET /v1/judges โ€” Judge directory

Every official who has scored a launch-org bout (~550), busiest first, with their career shape: fights, rounds scored, rounds scored 10-8 or wider, three-judge cards that came back split, and lone_dissents โ€” cards where this judge alone picked the other corner. Filter with q (name), org, and min_fights (the rates mean little below ~10 fights). meta.league is the whole directory's baseline, never the filtered subset: judges, fights (judge-cards), rounds_scored, and the pooled rates wide_round_rate (wide rounds / rounds), split_rate (split / three-judge cards) and lone_dissent_rate (lone dissents / three-judge cards).

GET /v1/judges/{id} โ€” One judge

A single official's career aggregates. Same shape as a directory row.

GET /v1/judges/{id}/scorecards โ€” Everything a judge has scored

This judge's card for every launch-org bout they worked, newest first โ€” the bout, its event, the decision type, and their own card, with where it sat on the panel: lone_dissent (three known verdicts and this judge alone differs from both others), split (three known verdicts that do not all agree) โ€” both null when the panel is not three judges or a verdict is unknown โ€” and colleagues, the other judges' totals and verdicts (the full round-by-round panel is on /v1/fights/{id}/scorecards). Cursor-paginated.

Odds

GET /v1/events/{idOrSlug}/odds โ€” Consensus odds for one card

The UFCalendar consensus line for every non-cancelled bout on one card, in card order: fights[], each with fight_id, status, both corners (fighter_a, fighter_b: id, slug, name), consensus (the latest point; the closing line once the bout is settled), opening (the first point we recorded), closing (settled bouts only: the last point at or before the event start), movement (opening โ†’ consensus in implied-probability points on corner a, with the direction the market moved), points and updated_at. Every point is a / b (american, decimal, raw implied_probability), favourite, fair_probability_a (margin removed), sources (how many sportsbooks backed it) and recorded_at. A bout nobody has priced stays on the list with nulls and points: 0; meta.priced / meta.unpriced count both. meta.checked_at is when we last checked the market, whether or not the line moved. Consensus is the mean across the sportsbooks we track, refreshed every 30 minutes in the 48 hours before a card and every 2 hours otherwise; book identities are never exposed. Information only, not betting advice. An unknown event 404s.

GET /v1/fights/{id}/odds โ€” Consensus odds for one bout

The UFCalendar consensus line for one bout: consensus (the latest point; the closing line once the bout is settled), opening (the first point we recorded), closing (settled bouts only, completed or no_contest: the last point at or before the event start; null until then), movement (opening โ†’ consensus: delta_points_a in implied-probability points on corner a, direction = the corner the market moved toward, since), points (stored points) and updated_at (when the line last moved). Every point is a / b (american, decimal, raw implied_probability), favourite, fair_probability_a (margin removed) and sources (how many sportsbooks backed it). Consensus is the mean across the sportsbooks we track, in decimal space; book identities are never exposed. meta.checked_at is when we last checked the market, whether or not the line moved; meta.history is the movement series (Pro+). A bout nobody priced answers 200 with nulls and points: 0; 404 is reserved for an unknown fight. Information only, not betting advice.

GET /v1/fights/{id}/odds/history โ€” Consensus line movement (Pro+)

Every stored consensus point for one bout, oldest first: the line-movement series. A point is stored only when the consensus changes, so the timestamps follow real market moves, not our refresh cadence. Each point: id, a / b (american, decimal, raw implied_probability), favourite, fair_probability_a (margin removed), sources (how many sportsbooks backed it) and recorded_at. from / to (inclusive YYYY-MM-DD) narrow the window; limit 1 to 500 (default 100); pass meta.pagination.next_cursor back as cursor for the next page. Requires Pro or higher (403 tier_required, answered before the fight is looked up). Book identities are never exposed. Information only, not betting advice.

Power Index

GET /v1/power-index/{org} โ€” Power Index board

The UFCalendar Power Index (our own rating engine, hourly refresh) for one org, in three views: - current (default): top-rated fighters whose latest bout was in this org. - peaks: the highest ratings ever reached, keyed on the org where the peak was set. - movers: the biggest rating change over the last days (default 365), among fighters with at least two bouts in the window and five rated overall, whose latest bout in the window was in this org. Rows carry start_rating, delta and fights_in_window instead of peak fields. Every row carries division (slug from the fighter's latest bout, null for a catchweight). division= narrows any view; the filter runs before limit. Unknown view 400s invalid_view, unknown division 400s invalid_division.

Matchmaker

GET /v1/matchmaker/{org} โ€” Matchmaker board

UFCalendar's matchmaker for one MMA promotion (ufc, pfl, oktagon, rizin; bkfc 400s unsupported_org โ€” the engine runs on the Power Index, which rates MMA only). Every candidate pair inside a division is scored 0โ€“100 for rank proximity, momentum, competitiveness, readiness and story (rematches, trilogy deciders, eliminators): the promotion's official ladder where one exists plus the strongest active unranked Power Index fighters. UFC pairs are scored by UFCalendar's booking model (a ranker trained on every realized UFC booking), weighted by the stakes term; other promotions use the fitted heuristic. Fighters already booked leave the board, pairs already booked are excluded, and one fighter appears in at most two slips per division. Without division the list is the cross-division top board (at most two slips per division); with it, that division's best pairs. meta.divisions lists the promotion's divisions (an unknown one 400s invalid_division), meta.rankings_date the ladder snapshot used (null when the promotion has no current ladder). Each slip: key (lo-hi fighter ids), a (red corner โ€” the better-credentialed side) and b with rank (0 = champion, null = unranked), power_index, streak (signed: +3 = three straight wins) and booked; score; win_probability_a from the two Power Index ratings; tags (title, eliminator, coinFlip, bothStreaking, finishers, trilogyDecider, unfinishedBusiness, rematch, unrankedGem, bounceBack, steppingUp, bothDue, booked). Computed WITHOUT the website's style-clash (Fight DNA) factor, so scores can differ slightly from ufcalendar.com. A ranking of fights worth making, not a report of bookings; probabilities are model output for information only. Recomputed at most every 10 minutes.

GET /v1/matchmaker/next/{fighter} โ€” Who should this fighter fight next

One fighter's most sensible next opponents by UFCalendar's matchmaker: the same engine and candidate pool as the board (their division's ladder plus the promotion's active Power Index fighters), every pairing scored, best first. The promotion and division come from the fighter's most recent bouts in a covered MMA promotion (404 not_found when there is none, or the division has no pool); a renamed slug 308-redirects. The fighter is scored as free even when booked โ€” the question is who comes after that โ€” and booked_next names the bout already booked (opponent + event), or null. Opponents already booked against them are left out. subject carries the corner fields plus days_since_last; each suggestion has opponent, win_probability_subject (from the two Power Index ratings), score (0โ€“100) and tags (same vocabulary as the board). Computed WITHOUT the website's style-clash (Fight DNA) factor, so scores can differ slightly from ufcalendar.com; probabilities are model output for information only.

Stats

GET /v1/stats/leaders โ€” One stat leaderboard

One board from UFCalendar's Record Book rollup (rebuilt daily) for ONE promotion โ€” org is required; there is no cross-promotion board on the API. metric picks the board; its scope decides what a row is: career rows are fighters, single_fight and round rows are PERFORMANCES (one fighter can hold several; fight names the bout and opponent, round_number the round), event rows are CARDS (event, no fighter), division rows are WEIGHT CLASSES (division, no fighter). value is in meta.metric.unit (rate is a 0โ€“1 fraction; seconds, per_minute, per_15_minutes as named); value_secondary is the denominator or context named by meta.metric.secondary (e.g. attempts behind an accuracy, the round of a fastest finish); sample_n is the bouts (or fighters) behind the value. direction is the ranking order, not a verdict. rank is the rollup's own position; equal values are a tie โ€” display_rank and tied say so. Narrow with division, country (ISO-2, fighters of that nationality; population all only โ€” combining it with population=active 400s invalid_filter) or population=active (a bout in the last ~18 months). Boards need a minimum sample, so a narrow slice can 404. Errors: invalid_org, invalid_metric, invalid_division, invalid_country, invalid_population, invalid_filter.

GET /v1/stats/record-book โ€” The Record Book

Every leaderboard's top rows for ONE promotion in one call โ€” org is required โ€” grouped by category (fights, time, striking, grappling, records): data is [{ category, boards: [{ metric, rows }] }], rows exactly as on /v1/stats/leaders. top rows per board (default 10, max 25). Same division / country / population narrowing as /v1/stats/leaders, plus scope for one kind of board (career, single_fight, round, division, event). A board appears only when it has at least 3 rows (or top, if smaller); inside one division, the division-ranking boards are left out. An empty slice returns [] with meta.note.

GET /v1/stats/years/{year} โ€” A year in review

One calendar year (UTC) of one promotion โ€” or, without org, of every covered promotion together (never a promotion the API does not cover) โ€” in numbers, over completed bouts of non-cancelled events: events_completed, title_fights, methods (ko, sub, dec, other, total), divisions (weight classes with 10+ bouts, highest finish rate first, up to 14), fastest_finishes (up to 8 round-1 KO/TKO/submissions, seconds and time, winner and loser), upsets (up to 8 wins with the lowest pre-fight UFCalendar Power Index probability, win_probability), climbers (up to 8 fighters with the biggest Power Index gain over the year, gain in rating points), busiest (up to 8 fighters by bouts, with wins), orgs (events and completed bouts per promotion), countries (host countries by events, up to 12) and judges (the 5 judges on the most commission-scored bouts). orgs_scope names the promotions counted. year runs from 1993 to the current year; anything else 404s. Unknown org 400s invalid_org. Refreshed every 10 minutes.

Predictions

GET /v1/predictions/upcoming โ€” Model win probabilities (UFC)

UFCalendar model win probabilities for upcoming UFC bouts, published about three weeks ahead of each card and repriced at least weekly. Corner-guarded: a probability is only served while the stored pair matches the bout's current pair โ€” a late opponent swap removes the row and it returns repriced within 30 minutes, rather than mislabeling it. Pass event= to narrow to one card (meta.event then names it; an unknown event 404s). Not betting advice.

Broadcast

GET /v1/events/{idOrSlug}/watch โ€” How to watch one event

Who airs one event, country by country: the promotion's standing rights deals merged with the event's own confirmed broadcast listings. Series-aware โ€” a Dana White's Contender Series or Road to UFC card gets its own grid (series: dwcs / rtufc), never the numbered-card deals; null for an ordinary card. Each country is { country, providers: [{ provider, kind, url, note_key, event_confirmed, rank }] }, WORLD first then ISO order. event_confirmed: true means the event's own listing names that provider (those rows win on a collision and sort first, rank: -1); note_key is a short machine key for a caveat (e.g. main card on PPV). Pass country= (two letters) for that market plus worldwide; anything else 400s invalid_country. Links are raw. On a sub-series card meta.note says an empty grid means no deal is known.

GET /v1/broadcast-rights/{org} โ€” Who airs it, per country

Org-level standing media rights per country (PFL harvested weekly across 212 countries; UFC ~110 curated). WORLD rows apply globally. Without series you get the org-wide deals only; UFC sub-series (dwcs = Dana White's Contender Series, rtufc = Road to UFC) have their OWN grid, which REPLACES the org-wide one for those cards โ€” pass series= to read it. Each row carries series (null for org-wide). For one event, /v1/events/{idOrSlug}/watch picks the right grid for you and overlays the event's own listings. An unknown series 400s invalid_series.

Search

Articles

GET /v1/articles โ€” Search UFCalendar articles

UFCalendar's own editorial archive โ€” previews, recaps, investigations, fighter-pay and technique pieces โ€” newest first. q (at least 2 characters) matches the title, summary or a tag, accent-insensitive (and the translated title and summary when locale is set); tag is an exact tag match; locale is one of the 13 site languages (en default, es, pt, de, fr, tr, ru, ka, ja, ko, pl, sr, zh) โ€” a row serves its translation when one exists and English otherwise, and its locale names the language actually served. Each row: slug, url (the canonical page in the requested language), title, description, author_name, tags, published_at, locale. Pass the slug to /v1/articles/{slug} for the body. A q under 2 characters 400s invalid_query; an unknown locale 400s invalid_locale. Betting-advice editorial (picks, parlays) is not served. Link back when quoting.

GET /v1/articles/{slug} โ€” Get an article

One published UFCalendar article by slug: title, description, author_name, author_slug, tags, published_at, url and the full body_md (Markdown; the site's client-only widgets โ€” fighter cards, embeds and offer blocks โ€” are removed). No cover image is served: its license credit has no place in this payload. locale is the language requested and served_locale the one delivered: the translation when one exists, English otherwise. An unknown slug (or a draft) 404s; betting-advice editorial (picks, parlays) is not served, so such a slug 404s too; an unknown locale 400s invalid_locale. Link back when quoting.

Account

GET /v1/webhook-endpoints โ€” List your webhook endpoints

Pro and above. Counts as one request against your quota; the deliveries themselves do not.

POST /v1/webhook-endpoints โ€” Register a webhook endpoint

Pro+. Body: {"url": "https://...", "events": ["event.announced", "fight.result", "card.changed", "event.completed", "odds.moved"]}. Subscribe to event.announced and you do not need to poll /v1/events to discover new cards โ€” it fires once, within a minute of a card first landing in our data, and carries the same event object the list endpoint returns. fight.result fires once per bout within a minute of the result, as revision 1, and again as the next revision (corrected: true, with the previous result) whenever the winner, method, round, time or status is corrected in the 72 h after it โ€” upsert on fight_id and keep the highest revision. odds.moved fires when the UFCalendar consensus line on an upcoming bout moves at least 5 implied-probability points on corner a, or the favourite flips, measured against the last line we delivered for that bout (its opening line before the first delivery), so a slow drift arrives once, not per refresh: data is {fight_id, event, fighter_a, fighter_b, odds: {consensus, opening, movement, updated_at}, previous, delta_points_a, direction, favourite_flipped, moved_at}. Information only, not betting advice. The response contains the signing secret ONCE. Every delivery body is {"id", "type", "data", "sent_at"} (WebhookDelivery schema) โ€” id is stable across retries, so deduplicate on it. Deliveries carry X-UFCalendar-Signature: t=<unix>,v1=<hmac> where the HMAC-SHA256 input is "<t>.<rawBody>". Verify the timestamp too: reject anything where |now - t| exceeds 300 seconds, otherwise a captured delivery can be replayed at you forever with a valid HMAC. Delivery is at-least-once โ€” expect a retry roughly every 60s until your endpoint returns 2xx. Endpoints auto-disable after 20 consecutive failures, and you may hold at most 3 active endpoints (409 endpoint_limit). Latency: results are dispatched within ~1 minute of the result being recorded. event.announced only fires for cards that have not started yet, so archive backfills never reach you as new-card noise. card.changed is filtered the same way: a change whose before and after read identically (a venue row re-minted for the same arena, a source dropping a sub-time it never knew) is never delivered, and when a source flip-flops a value we deliver the move and its undo once, not the twentieth repeat. Deliveries are free โ€” they never count against your monthly request quota, only the calls you make to manage endpoints do.

DELETE /v1/webhook-endpoints/{id} โ€” Delete a webhook endpoint

Pro and above. Idempotent: deleting an id that is not yours (or no longer exists) still answers 200.

POST /v1/webhook-endpoints/{id}/rotate-secret โ€” Rotate an endpoint signing secret

Pro and above. Issues a fresh whsec_ secret for the endpoint and returns it ONCE, keeping the endpoint id and its delivery history. The previous secret stops verifying the moment this returns, so deploy the new one to your receiver first (or accept both during the cutover). Use this instead of delete-and-recreate when a secret leaks.

GET /v1/usage โ€” Your quota usage

Current-month usage for the calling key: tier, requests used/limit, rpm limit, and the reset as both a Unix timestamp (reset) and ISO-8601 (reset_at); month (YYYYMM) and period (YYYY-MM) are the same month in two spellings. On the free 1-day trial, trial_ends_at is when access stops (null on a paid plan) โ€” plan around it, not reset_at.

Discovery

GET /v1/plans โ€” Plans, quotas and how to connect (no key required)

The only endpoint that answers without a credential. Returns every plan with its monthly request quota, per-minute limit, key limit and a checkout_url; the terms of the free 1-day trial and the URL that starts it; the MCP endpoint with both of its auth doors; and the documentation links. Static per deploy and cached an hour โ€” poll it never, read it once.