# UFC Stats Scraper: MMA Fights, Fighters, Events, Round-by-Round (`neverempty/ufc-stats-scraper`) Actor

For MMA betting models, fantasy tools and analytics: official ufcstats.com data covering 788 events back to UFC 2 in March 1994. Every fight round by round, with significant strikes split by head, body and leg and by distance, clinch and ground, takedowns, control time and judges' scorecards.

- **URL**: https://apify.com/neverempty/ufc-stats-scraper.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Sports, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $18.25 / 1,000 record returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## UFC Stats Scraper - fights, fighters, events, round by round

**For MMA analytics, betting models, fight previews and fighter scouting**: official UFC fight statistics from **ufcstats.com**, the site the UFC itself publishes its numbers on — events, fights, fighters and **every single round**.

**Most UFC scrapers give you the fight summary: who won, by what method, in which round. This one also opens every round** and returns the significant strikes split by **where they landed** (head, body, leg) and **from what position** (distance, clinch, ground), for both fighters, plus control time and takedowns — the numbers you need to say *how* a fight was won, not just that it was. Judges' scorecards come back per judge, per fighter.

**The whole history, in one request.** UFCStats lists **788 events** — the oldest is UFC 2 on 11 March 1994, the newest the card announced for next week (measured 8 September 2026) — and the entire event list comes back in a single request.

No API key, no login. Export as JSON, CSV or Excel.

### What you get

#### Fights (default)

One row per fight, with:

- **The card**: event name, date, location, weight class, whether it was a title fight
- **Both fighters**: name, UFCStats id, nickname, and their result written out (`win`, `loss`, `draw`, `no contest`)
- **The finish**: method (`KO/TKO`, `Submission`, `Decision - Unanimous`, …), the detail
  (`Punch to Head At Distance`), the exact round and time, the time format, the referee
- **Judges' scorecards** when it went to a decision - each judge with **both fighters' points as numbers**,
  not a single string you have to split yourself
- **Totals per fighter**: knockdowns, significant strikes landed/attempted and accuracy, total strikes,
  takedowns landed/attempted and accuracy, submission attempts, reversals, control time (also in seconds)
- **Significant strikes broken down**: head / body / leg, and distance / clinch / ground - landed and attempted
- **The same numbers for every round**, in a `rounds` array

#### Events

One row per UFC event: name, date, location, and whether it has already happened.
UFCStats lists **788 events** - the oldest is UFC 2, on 11 March 1994 - and the whole list comes back in a single request.

#### Fighters

One row per fighter: height, reach, stance, date of birth, the win-loss-draw record, and the career rates
UFCStats publishes (strikes landed and absorbed per minute, striking accuracy and defence, takedown average,
accuracy and defence, submission average) - plus that fighter's full fight list with opponent, event, date,
method, round and time.

***

### Example output (one fight, shortened)

```json
{
  "recordType": "fight",
  "eventName": "UFC 324: Gaethje vs. Pimblett",
  "eventDate": "2026-01-24",
  "weightClass": "Featherweight",
  "fighter1": "Arnold Allen",  "fighter1Result": "loss",
  "fighter2": "Jean Silva",    "fighter2Result": "win",
  "winner": "Jean Silva",
  "method": "Decision - Unanimous",
  "endRound": 3, "endTime": "5:00", "referee": "Chris Tognoni",
  "judges": [
    { "judge": "Ron McCarthy", "fighter1Score": 27, "fighter2Score": 30 },
    { "judge": "Chris Lee",    "fighter1Score": 28, "fighter2Score": 29 },
    { "judge": "Mike Bell",    "fighter1Score": 28, "fighter2Score": 29 }
  ],
  "fighter1SigStrikesLanded": 56, "fighter1HeadLanded": 37, "fighter1BodyLanded": 11, "fighter1LegLanded": 8,
  "fighter1DistanceLanded": 53,   "fighter1ClinchLanded": 3, "fighter1GroundLanded": 0,
  "fighter1ControlTimeSeconds": 54,
  "fighter2SigStrikesLanded": 74, "fighter2HeadLanded": 43, "fighter2TakedownsLanded": 4,
  "rounds": [
    { "round": 1, "fighter1": { "headLanded": 12, "bodyLanded": 4, "legLanded": 6, "…": "…" }, "fighter2": { "…": "…" } },
    { "round": 2, "…": "…" },
    { "round": 3, "…": "…" }
  ]
}
```

***

### Input

| Field | Key | What it does |
|---|---|---|
| **What to collect** | `mode` | `fights` (default), `events` or `fighters` |
| **How many events to read** | `maxEvents` | Events are read newest first; each holds roughly 10-14 fights |
| **Maximum records** | `maxRecords` | The most rows this run may return and charge for |
| **Open each fight page** | `fetchFightDetails` | On (default) gives the round-by-round breakdown, the referee and the scorecards. Off uses only the totals on the event page - fewer requests |
| **Include upcoming events** | `includeUpcoming` | Off by default, see below |
| **UFCStats URLs** | `startUrls` | Point the run at specific event, fight or fighter pages |
| **Fighter last-name letters** | `letters` | Which letters of the alphabet index to walk in `fighters` mode |
| **Weight classes** | `weightClasses` | Keep only fights in these weight classes; case-insensitive and partial |
| **Fighter names** | `fighterNames` | Keep only records involving these fighters |
| **Keywords / match / exclude** | `keywords`, `keywordMatch`, `excludeKeywords` | Text filters over event name, bout, weight class, fighter names, location and method |
| **Only return new records** | `monitoringMode` | For a schedule that watches for new fights |
| **Forget what was returned** | `resetMonitoringState` | Clears that memory once |
| **Maximum requests** | `maxRequests` | A hard ceiling on how many pages this run fetches |
| **Use a proxy if asked** | `useProxy` | Off by default; this Actor clears the site's browser check itself |

***

### Things this Actor is careful about

**The "completed" list is not all completed.** UFCStats puts the **next** event at the top of its
completed-events list. Its result columns are empty because the fights have not happened yet. This Actor
marks that event `isUpcoming: true` and skips it by default, so an empty card never arrives looking like a
finished one. Turn on *Include upcoming events* to get the announced card on purpose.

**Empty is not zero.** When UFCStats has no number for something - an unfought bout, a fighter whose
striking accuracy is blank - the field comes back `null`, never `0`. A `0` in this dataset means the site
really printed a zero.

**"Could not read" is never reported as "no data".** If a page fails, if the site's markup changes shape, or
if its browser check is not cleared, you get a row that says exactly which of those happened, with the URL.
Those rows are **not charged**. The same is true for a run where filters matched nothing, or where your
maximum cut the result short - each is its own labelled, free row.

**The limit does not delete whole events.** When *Maximum records* cuts the result, rows are taken evenly
from every event that was read, so you never get three events in full and a fourth silently missing. A free
row tells you how many were left out and from where.

**Repeat runs remember what was already returned.** In monitoring mode, filtering happens *before* the
new-only step, so a fight you filtered out is not held back from a later run.

***

### Frequently asked

**Where does the data come from?** ufcstats.com, the UFC's own statistics site. No login, no third-party
data broker. The site has no robots.txt.

**How far back does it go?** The event list holds 788 events, from UFC 2 on 11 March 1994 to the card announced for next week (measured 8 September 2026).

**Can I watch for new fights?** Yes - schedule the Actor with *Only return records that are new since the
last run* turned on. Each run then returns only fights it has not returned before.

**Why is `eventDate` empty on some fight rows?** A fight page does not carry its own date; the date comes
from the event. If you point the Actor straight at a fight URL, run it in `events` mode too, or read the
fight through its event, to get the date.

**Does it handle draws and no contests?** Yes. `isDraw` and `isNoContest` are set from the fight's own
result flags, and `winner` stays empty rather than naming whoever happens to be listed first.

***

### Pricing

Pay per event: you are charged for each **record returned**. Rows that explain a problem - an unreadable
page, an empty answer, a filter that matched nothing, a result cut by your maximum - are never charged.

# Actor input Schema

## `mode` (type: `string`):

fights = one row per fight, with the round-by-round strike breakdown (the default). events = one row per UFC event. fighters = one row per fighter, with career stats and fight history.

## `maxEvents` (type: `integer`):

Events are read newest first. Each event holds roughly 10-14 fights. Ignored when you give your own URLs below.

## `maxRecords` (type: `integer`):

The most rows this run may return and charge for. When the limit cuts the result, rows are taken evenly from every event, so no event disappears completely, and a free row tells you what was left out.

## `fetchFightDetails` (type: `boolean`):

On: one extra request per fight, and you get per-round significant strikes split by target (head/body/leg) and position (distance/clinch/ground), control time, referee and the judges' scorecards. Off: only the totals shown on the event page (faster and cheaper in platform usage).

## `includeUpcoming` (type: `boolean`):

UFCStats puts the next event at the top of its completed-events list. It has no results yet, so it is skipped by default. Turn this on to get the announced card (fighter names and weight class, with empty result columns).

## `startUrls` (type: `array`):

Optional. Event, fight or fighter URLs from ufcstats.com (for example http://ufcstats.com/event-details/2144954270be834d). When set, only these are read and the event count above is ignored.

## `letters` (type: `array`):

Fighters mode only. Which letters of the alphabet index to walk. Empty means the letter A.

## `weightClasses` (type: `array`):

Keep only fights in these weight classes (for example Lightweight, Women's Flyweight). Matching is case-insensitive and partial.

## `fighterNames` (type: `array`):

Keep only records that involve one of these fighters. Matching is case-insensitive and partial.

## `keywords` (type: `array`):

Keep only records whose event name, bout, weight class, fighter names, location or method contain these words.

## `keywordMatch` (type: `string`):

any = at least one keyword must appear. all = every keyword must appear.

## `excludeKeywords` (type: `array`):

Drop records containing any of these words. Applied to the same fields as Keywords above, and also to the fighter and event names.

## `monitoringMode` (type: `boolean`):

Remembers the ids this Actor has already returned and skips them next time. Filtering happens before this, so a record you filtered out is not held back for later.

## `resetMonitoringState` (type: `boolean`):

Clears that memory once, so the next run returns everything again.

## `maxRequests` (type: `integer`):

A hard ceiling on how many pages this run may fetch from ufcstats.com. It protects the site (and your platform usage) even when a filter matches almost nothing.

## `useProxy` (type: `boolean`):

Off by default: ufcstats.com answers a plain connection, and this Actor solves its browser check itself. Turn this on only if you see the browser check failing.

## Actor input object example

```json
{
  "mode": "fights",
  "maxEvents": 3,
  "maxRecords": 100,
  "fetchFightDetails": true,
  "includeUpcoming": false,
  "letters": [
    "a"
  ],
  "keywordMatch": "any",
  "monitoringMode": false,
  "resetMonitoringState": false,
  "maxRequests": 300,
  "useProxy": false
}
```

# Actor output Schema

## `results` (type: `string`):

In fights mode, one row per fight: the event and its date and location, both fighters with their UFCStats ids and nicknames, who won, the method and the exact round and time it ended, the referee, the judges' scorecards when it went to a decision, and for each fighter the knockdowns, significant strikes landed and attempted, total strikes, takedowns, submission attempts, reversals and control time - plus the same numbers again for every single round, split by target (head, body, leg) and by position (distance, clinch, ground). In events mode, one row per event with its date, location and whether it has happened yet. In fighters mode, one row per fighter with height, reach, stance, date of birth, the win-loss-draw record and the career rates UFCStats publishes, plus the full list of that fighter's fights. Pages that could not be read, a browser check that was not solved, an empty answer from the site, filters that matched nothing and rows left out by your maximum each come back as their own row and are not charged.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "mode": "fights",
    "maxEvents": 3,
    "maxRecords": 100,
    "fetchFightDetails": true,
    "includeUpcoming": false,
    "letters": [
        "a"
    ],
    "maxRequests": 300
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/ufc-stats-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "mode": "fights",
    "maxEvents": 3,
    "maxRecords": 100,
    "fetchFightDetails": True,
    "includeUpcoming": False,
    "letters": ["a"],
    "maxRequests": 300,
}

# Run the Actor and wait for it to finish
run = client.actor("neverempty/ufc-stats-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "mode": "fights",
  "maxEvents": 3,
  "maxRecords": 100,
  "fetchFightDetails": true,
  "includeUpcoming": false,
  "letters": [
    "a"
  ],
  "maxRequests": 300
}' |
apify call neverempty/ufc-stats-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/ufc-stats-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/eDEMQVlVJ7lb3k1zq/builds/PhK8NUgao3d6Bcrmb/openapi.json
