How do I build a UFC fight-night Discord bot?
Poll the live UFC card from the MMA Fight Data API and post each new result to a Discord channel webhook, or answer a slash command with discord.js. The Python block posts every result with the winner, method, round and time, checking once a minute while a card is live and every 15 minutes otherwise. On Pro, the fight.result webhook can push results to your bot instead.
Free plan: 1,000 requests a month, no card. Paid plans from $19/mo.
Endpoints used
| Method & path | What it returns | Plan |
|---|---|---|
GET /v1/events?org=ufc&include=headline | MMA schedule + results API: full fight cards, venues, broadcasts, and a card-change diff log for late reshuffles. | All plans |
GET /v1/events/{slug} | One event with its full fight card (both corners, results when completed), venue and event-level broadcasts. | All plans |
POST /v1/webhook-endpoints | Signed webhooks for fight results, card changes, event completion and consensus odds moves · no polling needed. | Pro and up |
The code
curl shows the call behind /nextufc. The Python block posts results through a Discord channel webhook, with no bot account needed; the JavaScript block is a discord.js bot with a /nextufc slash command.
Every block runs as shown with your API key in the UFCAL_API_KEY environment variable. Python uses pip install ufcalendar and JavaScript or TypeScript uses npm install @ufcalendar/sdk; any HTTP client works too.
curl "https://api.ufcalendar.com/v1/events?org=ufc&limit=1&include=headline" \
-H "Authorization: Bearer $UFCAL_API_KEY"# pip install ufcalendar requests
import os
import time
import requests
from ufcalendar import FightAPI
api = FightAPI() # reads UFCAL_API_KEY from the environment
DISCORD_WEBHOOK = os.environ["DISCORD_WEBHOOK_URL"] # Channel settings -> Integrations -> Webhooks
watching, posted = set(), set()
def post_new_results():
watching.update(ev["slug"] for ev in api.events("ufc", status="live"))
for slug in list(watching):
event = api.event(slug)
for f in event["card"]:
r = f["result"]
if not r or f["id"] in posted:
continue
posted.add(f["id"])
names = {c["id"]: c["name"] for c in (f["fighter_a"], f["fighter_b"])}
winner = names.get(r["winner_fighter_id"], "No winner")
msg = (f"**{f['fighter_a']['name']} vs {f['fighter_b']['name']}**\n"
f"{winner} - {r['method']}, round {r['round']} at {r['time']}")
requests.post(DISCORD_WEBHOOK, json={"content": msg}, timeout=10).raise_for_status()
if event["status"] != "live": # one last pass after the card ends
watching.discard(slug)
while True:
post_new_results()
time.sleep(60 if watching else 15 * 60) # fast on fight night, slow otherwise// npm install discord.js @ufcalendar/sdk
import { Client, Events, GatewayIntentBits, REST, Routes, SlashCommandBuilder } from "discord.js";
import { FightAPI } from "@ufcalendar/sdk";
const api = new FightAPI(); // reads UFCAL_API_KEY from the environment
const { DISCORD_TOKEN, DISCORD_APP_ID } = process.env;
// Register /nextufc once (global commands can take a few minutes to appear)
const command = new SlashCommandBuilder().setName("nextufc").setDescription("The next UFC card and its main event");
await new REST().setToken(DISCORD_TOKEN).put(Routes.applicationCommands(DISCORD_APP_ID), { body: [command.toJSON()] });
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand() || interaction.commandName !== "nextufc") return;
for await (const ev of api.events({ org: "ufc", limit: 1, include: ["headline"] })) {
const h = ev.headline;
const unix = Math.floor(new Date(ev.main_card_at ?? ev.starts_at).getTime() / 1000);
// <t:...:F> renders in each reader's own time zone
await interaction.reply(`**${ev.title}**\n${h ? `${h.fighter_a.name} vs ${h.fighter_b.name}\n` : ""}Main card <t:${unix}:F> (<t:${unix}:R>)`);
return;
}
await interaction.reply("No UFC card is scheduled right now.");
});
await client.login(DISCORD_TOKEN);Sample response
A real response from the live API, trimmed for length. … marks removed fields.
{
"data": [
{
"id": 2805,
"slug": "ufc-fight-night-2026-10-31",
"title": "UFC Fight Night: Moicano vs Nolan",
"short_title": "Fight Night",
"numbering": "Fight Night",
"org": "ufc",
"sport": "mma",
"starts_at": "2026-10-31T21:00:00.000Z",
"main_card_at": "2026-11-01T00:00:00.000Z",
"prelims_at": "2026-10-31T21:00:00.000Z",
"early_prelims_at": null,
"tz": "America/Los_Angeles",
"status": "announced",
"is_ppv": false,
"is_title_card": false,
"headline": {
"fight_id": 93077,
"fighter_a": { "id": 589, "slug": "renato-moicano", "name": "Renato Moicano" },
"fighter_b": { "id": 92, "slug": "tom-nolan", "name": "Tom Nolan" },
"weight_class": "Lightweight",
"is_title": false,
"winner_fighter_id": null,
"method": null,
"round": null,
"time": null
}
}
],
"meta": {
"pagination": { "next_cursor": "eyJpZCI6MjgwNX0", "has_more": true, "limit": 1 }
}
}Notes and pitfalls
- Discord's timestamp markup shows the start time in each member's own time zone, so send the Unix time rather than a formatted date.
- Budget: a poll every 15 minutes is about 3,000 requests a month, and a six-hour card polled every minute adds about 720 (a listing call and a card call each minute). That fits the Hobby plan; the free plan suits the slash command on its own.
- Keep the posted bout ids in a file or database so a restart does not repost the card, and fetch each card once more after it flips to completed so the main event is not missed.
- The sample registers /nextufc on every start to stay short. In production, register it once at deploy time, and expect a global command to take a few minutes to appear.
Related pages
More code guides
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