RIZIN API in Python: Japanese MMA schedule, results and stats (2026)
A runnable Python guide to RIZIN fight data: the Japanese schedule in local time, results within minutes, kickboxing bouts and round-by-round stats through the UFCalendar Fight API.

Ask for a RIZIN API in an English-speaking developer forum and you get silence, or a link to a scraper that died when jp.rizinff.com was redesigned. Japan's biggest promotion runs numbered shows at the Saitama Super Arena, a LANDMARK series that tours the country, a New Year's Eve card that fills a network prime-time slot, and mixed cards where a kickboxing bout sits next to a submission specialist's title defense. None of the Western sports-data vendors track any of it.
We do, and as of this week it is part of the UFCalendar Fight API: every RIZIN card since the December 2015 debut, results within minutes of the verdict on fight night, and per-fight plus per-round statistics from 2023 on. This is the same guide I wrote for the UFC side of the API, rewritten for the parts that are different about RIZIN. It costs money after a one day trial, and I say so up front.

Step 0: a key and the setup
Sign in at ufcalendar.com/account/api and click Start free trial: one day, 100 requests, every endpoint, no card. Keep the key in an environment variable.
pip install requests pandas
export UFCAL_KEY=ufcalendar_...
import os, requests
API = "https://api.ufcalendar.com/v1"
H = {"Authorization": f"Bearer {os.environ['UFCAL_KEY']}"}
def get(path, **params):
r = requests.get(f"{API}/{path}", params=params, headers=H, timeout=30)
r.raise_for_status()
return r.json()
Before pulling anything, ask the API what RIZIN actually offers. Every launch organization carries capability flags, and RIZIN's are honest about one gap:
print(get("orgs/rizin")["data"]["capabilities"])
# {'stats': True, 'rounds': True, 'rankings': False,
# 'broadcasts': True, 'predictions': False, 'scorecards': False}
RIZIN publishes no official rankings board, so /v1/rankings/rizin returns a 404 rather than a made-up list. Stats and rounds are there, and so are the broadcasters. There is a pip install ufcalendar client (version 0.1.2, source on GitHub) if you want typed methods, but the raw requests are short enough to show here.
Step 1: the schedule in Japanese time
RIZIN cards start in the afternoon in Tokyo, which is the middle of the night on the American East Coast. Every event row carries a UTC starts_at and the venue's IANA zone in tz, so convert once and stop guessing.
from datetime import datetime
from zoneinfo import ZoneInfo
for ev in get("events", org="rizin")["data"]:
start = datetime.fromisoformat(ev["starts_at"].replace("Z", "+00:00"))
local = start.astimezone(ZoneInfo(ev["tz"]))
print(local.strftime("%a %d %b %H:%M %Z"), ev["short_title"])
At the time of writing that prints four cards: Super RIZIN 5 at 16:00 JST on September 10, RIZIN LANDMARK 16 in Nagasaki on October 3, RIZIN LANDMARK 17 in Chiba on November 8, and the New Year's Eve card on December 31, the last three all at 14:00 JST. short_title is the name you would put in a push notification; title keeps the full Japanese event name, "Super RIZIN 5: Naniwa No Chou Fukkatsu Matsuri" in this case.
Step 2: a full card, with the kickboxing bouts marked
The thing that breaks naive MMA code on a RIZIN card is that not every bout is MMA. Super RIZIN 5 has Noah Bey against Hide Meison Usami under kickboxing rules, three rounds of three minutes, on the same card as Mikuru Asakura against Shinya Aoki. Every fight row carries a sport field, so filter on it instead of assuming.
card = get("events/rizin-2026-09-10")["data"]
print(card["title"], "at", card["venue"] and card["venue"]["name"])
for f in card["card"]:
tag = "" if f["sport"] == "mma" else f"[{f['sport']}]"
a, b = f["fighter_a"]["name"], f["fighter_b"]["name"]
r = f.get("result")
verdict = f"{r['method']} R{r['round']} {r['time']}" if r else f["status"]
print(f"{a} vs {b} {tag}: {verdict}")
A completed bout's result carries winner_fighter_id, method, round and time. The winner is an id because names collide across promotions; map it back through fighter_a and fighter_b. On fight night the row flips within minutes of the verdict, and the same change fans out as a fight.result webhook on Pro plans, so nobody has to poll a Japanese card at 4 a.m.
Cancelled bouts stay on the card with status: "cancelled" rather than vanishing, which is the difference between a bot that says "this fight was pulled" and one that silently forgets it was ever booked. The card-change log behind that is its own endpoint, /v1/events/{id}/changes, documented with the rest at api.ufcalendar.com/docs.
Step 3: round-by-round stats into pandas
RIZIN 54 at Toyota Arena Tokyo is a good test card: ten bouts fought, every one with a result and a stat line. The main event went the distance, Kleber Koike Erbst losing a decision to Kyoma Akimoto, and the numbers explain why better than the scorecard does.
import pandas as pd
card = get("events/rizin-2026-08-11")["data"]
main = next(f for f in card["card"] if f["is_main"])
rows = get(f"fights/{main['id']}/rounds")["data"]
df = pd.DataFrame(rows)
cols = ["round_number", "fighter_position", "sig_strikes_landed",
"head_landed", "leg_landed", "takedowns_attempted", "control_time_sec"]
print(df[cols].to_string(index=False))
Six rows come back, one per fighter per round. Across the bout Koike landed 64 significant strikes to Akimoto's 56, but 28 of his were leg kicks and only 27 went to the head, against 50 head strikes for Akimoto. Koike shot 13 takedowns and landed none. That is a decision loss you can see coming from the DataFrame.
One honesty note about the RIZIN stat lines: they are landed-only. sig_strikes_attempted, head_attempted and the other attempt columns come back null for RIZIN, because the tracker behind these cards does not publish attempts. The UFC lines in the same API carry attempts, so if you are computing accuracy across promotions, check for null before you divide. The position-time columns (distance_time_sec, clinch_time_sec, ground_time_sec) are populated and are the more interesting RIZIN-specific signal anyway.
Step 4: careers that cross promotions
RIZIN's roster is the most cross-pollinated in the sport: Bellator champions, KSW imports, ONE veterans, UFC alumni. A fighter's /history stitches all of it into one timeline, each row carrying org_slug, the opponent's slug, the result and the sport of that bout.
hist = get("fighters/kleber-koike-erbst/history")["data"]
by_org = pd.Series([h["org_slug"] for h in hist if h["org_slug"]]).value_counts()
print(by_org)
For Koike that shows 16 RIZIN bouts, 7 in KSW, one in ONE and one in Bellator, plus the older regional fights that carry no promotion slug. If you have ever tried to assemble that from Tapology and Sherdog by hand, you know why the endpoint exists.
Step 5: who airs it, per country
The broadcasts field on each event carries the confirmed outlets for that specific card, and /v1/broadcast-rights/rizin?country=JP carries the standing rights: ABEMA, U-NEXT and SKY PerfecTV! at home, RIZIN.TV and TrillerTV for the rest of the world. Building a "where do I watch" screen means one call per country code.
for r in get("broadcast-rights/rizin", country="JP")["data"]:
print(r["country"], r["provider"], r["kind"], r["url"])
What is and is not in there
The whole RIZIN catalog is 85 events, 904 fights and 635 fighters, with 782 per-fight stat lines and 1,666 round rows concentrated from 2023 onward. Older cards have full results but no statistics, because nobody logged them at the time. No odds anywhere in the API, by design. No official rankings, because RIZIN does not publish one. Everything else the UFC coverage has, RIZIN has under the same schema and the same key.
If you build something with it, a Discord bot that posts Saitama results at a civilized European hour would be a good start. Nobody has done that yet either.