How to build a UFC or MMA app
To build a UFC or MMA app you need one data source behind five screens (schedule, fight card, results, fighter profiles, rankings) and a way to know when a result is official. UFCalendar’s MMA Fight Data API covers all of it as JSON over REST for UFC, PFL, OKTAGON, BKFC and RIZIN: full cards back to UFC 1, per-round stats, official rankings history, judges’ scorecards, ICS calendar feeds, and signed webhooks that push each result within minutes of the decision. Try it free for 1 day and 100 requests with no card; paid plans start at $19/mo.
Free 1-day trial · 100 requests · no credit card. Paid plans from $19/mo for 30,000 requests, cancel anytime.
Every screen a fight app needs, and the calls behind it
Each screen is one or two requests. The badge shows which plans can make the call.
1.Schedule and calendar
Upcoming cards with the start time in UTC plus the venue time zone, the main-card time, and PPV and title-card flags. Add who airs each card in the viewer’s country, or hand fans an ICS feed instead of writing calendar sync.
GET /v1/events?org=ufcAll plansMMA schedule + results API: full fight cards, venues, broadcasts, and a card-change diff log for late reshuffles.
Who airs the promotion in a given country: broadcaster, kind (TV, streaming, PPV) and link.
GET /v1/calendar/ufc.icsAll plansICS calendar feeds per organization · subscribe any calendar app to every event, auto-updating as cards change.
2.Fight card
One call returns the whole card in running order: both corners, weight class and title flags. The change log records every bout added, scratched or moved, so a late replacement never leaves a stale card in your app.
GET /v1/events/{slug}All plansOne event with its full fight card (both corners, results when completed), venue and event-level broadcasts.
GET /v1/events/{slug}/changesAll plansThe card-change diff log: fight added or removed, opponent swapped, date or order moved.
GET /v1/events/{slug}/storylinesAll plansA card's talking points from the record: title fights, ranked fighters, win streaks and the closest bout on paper.
3.Results
Winner, method, round and time for every bout, with per-fight totals for both corners. Completed cards come back newest first, each with every result.
Completed cards newest-first, each with the full card and every result.
GET /v1/fights/{id}All plansOne bout with corners, result (winner, method, round, time), referee and bonus flags.
GET /v1/fights/{id}/statsAll plansPer-fight totals for both corners: strikes by target and position, takedowns, control time.
4.Fighter profile
Bio, record, career striking and grappling rates, and the fighter’s whole career across promotions as one timeline.
GET /v1/fighters/{slug}All plansFighter bio, records, career statistics per scope, Power Index and CC-licensed images.
GET /v1/fighters/{id}/historyAll plansRoster search, bios, records · and complete multi-promotion career timelines, merged and deduplicated.
GET /v1/fighters/{slug}/statsAll plansCareer statistics per scope: strike and takedown rates, accuracy, defense, submission average.
5.Rankings
The official board for every division, today or on any past date back to 2013, plus the current champions of every promotion in one call.
GET /v1/rankings/ufc?date=YYYY-MM-DDAll plansOfficial ranking boards, point-in-time back to 2013 for the UFC. Champions across every launch org.
GET /v1/championsAll plansCurrent champions across every launch org in one call.
6.Notifications
On Pro and Business, signed webhooks push results and card changes to your server, which sends the push to your users. On any plan, the change feed lists every card change for apps that poll.
POST /v1/webhook-endpointsPro and upSigned webhooks for fight results, card changes, event completion and consensus odds moves · no polling needed.
GET /v1/changes?org=ufcAll plansEvery card change across every event in one feed, newest first: the webhook's audit trail for callers that poll.
A minimal app backend
The data for all five screens in a handful of requests. Run it on your server: an API key must never ship inside a mobile app or a browser bundle.
// server.js — your app's backend (Node 18+). The key lives here, never in the app.
const API = "https://api.ufcalendar.com/v1";
const headers = { Authorization: `Bearer ${process.env.UFCAL_KEY}` };
const get = (path) => fetch(`${API}${path}`, { headers }).then((r) => r.json());
// Schedule screen: upcoming UFC cards, soonest first
const { data: events } = await get("/events?org=ufc");
for (const ev of events) console.log(ev.starts_at, ev.title, ev.is_ppv ? "PPV" : "");
// Fight card screen: one event with its whole card, in running order
const { data: next } = await get(`/events/${events[0].slug}`);
for (const f of next.card) console.log(f.weight_class, f.fighter_a?.name, "vs", f.fighter_b?.name);
// Results screen: winner, method, round and time once a bout is over
const { data: [last] } = await get("/events?org=ufc&status=completed&limit=1");
const { data: done } = await get(`/events/${last.slug}`);
for (const f of done.card.filter((x) => x.result)) {
const winner = [f.fighter_a, f.fighter_b].find((c) => c?.id === f.result.winner_fighter_id);
console.log(winner?.name ?? "draw / no contest", "by", f.result.method, `R${f.result.round} ${f.result.time}`);
}
// Fighter profile screen
const { data: fighter } = await get("/fighters/islam-makhachev");
console.log(fighter.name, fighter.career_stats.find((s) => s.scope === "pro-mma")?.record);
// Rankings screen: the current board
const { data: board } = await get("/rankings/ufc");
const lw = board.divisions.find((d) => d.division === "lightweight");
console.log("Champion:", lw?.entries.find((e) => e.rank === 0)?.name);# backend.py — your app's backend. The key lives here, never in the app.
import os, requests
API = "https://api.ufcalendar.com/v1"
H = {"Authorization": f"Bearer {os.environ['UFCAL_KEY']}"}
get = lambda path, **params: requests.get(f"{API}{path}", params=params, headers=H).json()["data"]
# Schedule screen: upcoming UFC cards, soonest first
events = get("/events", org="ufc")
for ev in events:
print(ev["starts_at"], ev["title"], "PPV" if ev["is_ppv"] else "")
# Fight card screen: one event with its whole card, in running order
event = get(f"/events/{events[0]['slug']}")
for f in event["card"]:
print(f["weight_class"], (f["fighter_a"] or {}).get("name"), "vs", (f["fighter_b"] or {}).get("name"))
# Results screen: winner, method, round and time once a bout is over
last = get("/events", org="ufc", status="completed", limit=1)[0]
for f in get(f"/events/{last['slug']}")["card"]:
r = f.get("result")
if not r:
continue
corners = {c["id"]: c["name"] for c in (f["fighter_a"], f["fighter_b"]) if c}
print(corners.get(r["winner_fighter_id"], "draw / no contest"), "by", r["method"], f"R{r['round']} {r['time']}")
# Fighter profile screen
fighter = get("/fighters/islam-makhachev")
print(fighter["name"], next((s["record"] for s in fighter["career_stats"] if s["scope"] == "pro-mma"), None))
# Rankings screen: the current board
board = get("/rankings/ufc")
lw = next(d for d in board["divisions"] if d["division"] == "lightweight")
print("Champion:", next((e["name"] for e in lw["entries"] if e["rank"] == 0), None))Push results within minutes
Register one HTTPS endpoint and the API sends it a signed fight.result delivery within minutes of the official decision, plus card.changed when a bout is added, scratched or reshuffled, event.announced for new cards and event.completed when the last bout ends. A corrected result arrives as a new revision, so upsert on the fight id. Deliveries never count against your quota. Webhooks come with Pro and Business.
// Pro and Business: let results come to you instead of polling on fight night
await fetch("https://api.ufcalendar.com/v1/webhook-endpoints", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.UFCAL_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ url: "https://example.com/hooks/ufcal", events: ["fight.result", "card.changed"] }),
});
// Your receiver: one signed delivery per result (X-UFCalendar-Signature header)
app.post("/hooks/ufcal", express.json(), async (req, res) => {
const { type, data } = req.body;
if (type === "fight.result") {
await db.results.upsert(data.fight_id, data.result); // a correction arrives as a new revision
const headline = data.result.winner ? `${data.result.winner} wins by ${data.result.method}` : data.result.method;
await push.toFollowers(data.fight_id, headline);
}
res.sendStatus(200);
});Cache once on your server, serve every user
- Call the API from your backend, never from each phone. One request then serves all your users, and your key stays secret.
- Cache by how fast the data moves: the upcoming schedule for an hour or so, a finished card for days, and historical records for good.
- Every response carries an ETag. Send it back as If-None-Match and an unchanged payload returns 304 with no body.
- On fight night, let webhooks (or the change feed) tell you what changed instead of re-downloading whole cards.
Which plan fits your app
- Hobby · $19/mo
- 30,000 requests a month at 60 per minute. Enough for a side project or a Discord bot that caches on its server and re-checks the card a few times on fight night.
- Pro · $49/mo
- 200,000 requests a month at 300 per minute, plus webhooks. The usual pick for a public app that pushes results to its users.
- Business · $149/mo
- 1,000,000 requests a month at 600 per minute, for apps with heavy traffic or a large archive backfill.
Pricing
Free 1-day trial · 100 requests · no credit card. Paid plans from $19/mo for 30,000 requests, cancel anytime.
Calendar feeds
Each promotion has an ICS feed that Google Calendar, Apple Calendar and Outlook subscribe to and refresh on their own, with card changes flowing through. Fans get every event in their calendar and you write no sync code.
AI features
The same database runs as a hosted MCP server with 47 tools. Connect Claude, ChatGPT or Cursor and fight questions are answered from real records rather than a model’s memory, at https://api.ufcalendar.com/mcp
Who it's for
Built for indie developers, side projects and small teams. Whether you're building a fantasy app, a stats site, a Discord bot or an analytics project, our plans are priced for you. We don't sell enterprise contracts, and we don't plan to.
Questions
- Which API should I use to build a UFC app?
- Pick one that covers every screen you plan to ship and lets you sign up yourself. UFCalendar’s MMA Fight Data API covers schedules, full fight cards, results, per-round stats, fighter careers, official rankings history and judges’ scorecards for UFC, PFL, OKTAGON, BKFC and RIZIN, and pushes results by webhook. The comparison page lists the other self-serve MMA APIs side by side.
- Can I build an MMA app for free?
- You can build and test it free: the trial gives you 1 day and 100 requests with no card. A live app needs a paid plan, from $19 a month for 30,000 requests.
- Can my mobile app call the API directly?
- Not with your API key inside it: anyone can extract a key from an app bundle. Call the API from your own server, cache the responses there, and have the app talk to your server.
- How do I send a push notification when a fight ends?
- On Pro or Business, register a webhook: a signed fight.result delivery reaches your server within minutes of the official decision, and your server sends the push. On Hobby, re-fetch the event every few minutes while the card is running and compare the results.
- Do I have to scrape websites to get fight data?
- No. Scrapers break whenever a site changes its markup and leave you to reconcile spellings, dates and corners yourself. The API returns one cleaned record per fighter and per bout, with stable ids and slugs.
- Which promotions can my app cover?
- UFC, PFL, OKTAGON, BKFC and RIZIN, on one schema with the same endpoints, so adding a promotion is a query parameter rather than a new integration.
More Fight API pages
One page per job: schedules, results, per-round statistics, rankings history, judges' scorecards and consensus odds, each with its own endpoints, sample responses and code.
UFCalendar is an independent publication, not affiliated with UFC/Zuffa/TKO, PFL, OKTAGON MMA, BKFC, RIZIN FF or any promotion. Organization names are used to describe the data. Terms of service · Refund Policy