<!--
  Hand-maintained markdown mirror of the /developers/docs page
  (frontend/app/developers/docs/page.tsx). When editing either one, update the
  other. Shared facts (base URL, rate limit, support email) are pinned in
  frontend/components/api-docs/constants.ts.
-->

# Stats Guy Fantasy API Reference

> Player and pick values for dynasty and redraft fantasy football, derived from real trades across thousands of Sleeper leagues. Free and open: no API key, no authentication, open CORS.

## Getting started

The Stats Guy Fantasy API serves player and pick values derived from real dynasty and redraft trades across thousands of Sleeper leagues. Values are recalculated on a regular run cadence (roughly daily), and historical snapshots are archived per run. See the [methodology](https://statsguyfantasy.com/methodology) for how values are computed.

All endpoints are served over HTTPS from a single base URL. There is no authentication and no API key. Every endpoint is open to everyone, including for commercial use. Publicly distributed products that display the data must include a visible credit linking back to Stats Guy Fantasy; see the terms of service at https://statsguyfantasy.com/terms.

```
Base URL: https://api.statsguyfantasy.com/api/v1
```

Fetch the top of the superflex dynasty board:

```bash
curl "https://api.statsguyfantasy.com/api/v1/rankings?format=sf_dynasty&limit=5"
```

The API root (`GET https://api.statsguyfantasy.com/api/v1`) is a discovery endpoint that lists every endpoint and its parameters as JSON. CORS is open, so you can call the API directly from browser apps.

## Rate limits & caching

Requests are rate limited per IP address, currently **60 requests per minute**. Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` (unix seconds) headers. When you exceed the limit you get a `429` response with a `Retry-After` header. Back off until then.

Responses are gzip-compressed and may be served from a cache for up to an hour, so don't depend on sub-hour freshness. Because values only change when a calculation run completes (roughly daily), polling faster than that gains nothing anyway. For bulk consumers, one `GET /players` sync per day is plenty. If you need more headroom than the per-IP limit, email support@leaguebeat.com.

## League formats

Values are calculated independently for four league formats:

| Format | Description |
| --- | --- |
| `sf_dynasty` | Superflex dynasty (default) |
| `non_sf_dynasty` | Single-QB dynasty |
| `sf_redraft` | Superflex redraft |
| `non_sf_redraft` | Single-QB redraft |

**List endpoints are format-scoped**: `?format=` (or the `format` body field) picks the lens. **Entity endpoints are format-complete**: player detail and picks return `value` (and related fields) as per-format maps, so one call covers every league type.

## Player & pick IDs

Player IDs are [Sleeper](https://docs.sleeper.com/) player IDs, returned by every endpoint as `id` and accepted anywhere a player ID is expected (path parameters, trade-evaluate sides).

```bash
# Fetch a player by Sleeper ID
curl "https://api.statsguyfantasy.com/api/v1/players/7564"
```

Rookie draft picks use structured IDs in three forms:

| Pick ID | Description |
| --- | --- |
| `pick:2027:1` | Round-only: a 2027 1st |
| `pick:2027:1:early` | Early/mid/late variant of a round-only pick |
| `pick:2026:2.08` | Slot-specific (current draft class): 2026 pick 2.08 |

Player metadata (names, teams, positions, ages) is synced from the Sleeper API; position eligibility (`fantasyPositions`) follows Sleeper's classification. Values and rankings are Stats Guy Fantasy's own trade-derived data.

## Dates & history

Dates are always strict `YYYY-MM-DD`; other spellings are rejected with `400`. Historical lookups resolve to the latest snapshot on or before the requested date, looking back at most 14 days; the snapshot date actually used is echoed back as `asOf`.

The `asOf` / `valuesAsOf` fields on current-value responses are the timestamp of the value-calculation run that produced the data (per format), not the request time.

## Errors

Errors are JSON with the shape `{ "error": "...", "message": "..." }`:

| Status | Description |
| --- | --- |
| `400` | Bad input: invalid parameters, malformed dates, or malformed JSON bodies |
| `404` | Not found: unknown player or route |
| `413` | Request body over 100kb |
| `429` | Rate limit exceeded; respect the `Retry-After` header |
| `500` | Internal server error |

---

## GET /rankings

Player rankings for a format, current or historical. Use `/rankings` for display views, slices (position, pagination), and historical boards; use `/players` when you want the complete current dataset in one call.

| Parameter | Description |
| --- | --- |
| `format` | League format (default `sf_dynasty`) |
| `position` | `QB`, `RB`, `WR`, or `TE` |
| `date` | `YYYY-MM-DD`. Historical board (omit for current). Resolves to the latest snapshot on or before this date (14-day lookback); `asOf` is the snapshot date served. |
| `limit` | 1-1000, default 500 |
| `offset` | Pagination offset |

```bash
curl "https://api.statsguyfantasy.com/api/v1/rankings?format=sf_dynasty&position=WR&limit=1"
```

```json
{
  "format": "sf_dynasty",
  "asOf": "2026-07-24T13:23:13.724Z",
  "total": 214,
  "limit": 1,
  "offset": 0,
  "rankings": [
    {
      "rank": 1,
      "id": "7564",
      "name": "Ja'Marr Chase",
      "team": "CIN",
      "position": "WR",
      "positionRank": 1,
      "value": 10874,
      "age": 26.4,
      "yearsExperience": 5
    }
  ]
}
```

With `?date=`, values and ranks come from the historical snapshot, but player metadata (name, team, age) reflects the current player record.

## GET /players

All players with calculated values, as format-complete cards (same shape as `GET /players/:id`), sorted by `sf_dynasty` value. This is the bulk sync endpoint: one call gives you a complete Sleeper-ID-keyed values map for local caching.

```json
{
  "total": 376,
  "valuesAsOf": {
    "sf_dynasty": "2026-07-24T13:23:13.724Z",
    "non_sf_dynasty": "2026-07-24T13:20:41.238Z",
    "sf_redraft": "2026-07-24T13:26:02.881Z",
    "non_sf_redraft": "2026-07-24T13:28:19.454Z"
  },
  "players": [
    {
      "id": "7564",
      "name": "Ja'Marr Chase",
      "team": "CIN",
      "position": "WR",
      "value": {
        "sf_dynasty": 10874,
        "non_sf_dynasty": 10422,
        "sf_redraft": 9100,
        "non_sf_redraft": 8944
      }
    }
  ]
}
```

Use `valuesAsOf` to decide when to resync: if it hasn't changed, neither have the values.

## GET /players/:id

A player card across all formats: values, ranks, and change metrics (7- and 30-day windows) as per-format maps. There is no `format` parameter; one call covers every league type. The `:id` path takes the Sleeper player ID.

```bash
curl "https://api.statsguyfantasy.com/api/v1/players/7564"
```

```json
{
  "id": "7564",
  "name": "Ja'Marr Chase",
  "team": "CIN",
  "position": "WR",
  "fantasyPositions": ["WR"],
  "age": 26.4,
  "yearsExperience": 5,
  "rookieYear": 2021,
  "dataUpdatedAt": "2026-07-24T18:00:03.162Z",
  "value": {
    "sf_dynasty": 10874,
    "non_sf_dynasty": 10422,
    "sf_redraft": 9100,
    "non_sf_redraft": 8944
  },
  "rank": {
    "sf_dynasty": 1,
    "non_sf_dynasty": 1,
    "sf_redraft": 4,
    "non_sf_redraft": 5
  },
  "positionRank": {
    "sf_dynasty": 1,
    "non_sf_dynasty": 1,
    "sf_redraft": 1,
    "non_sf_redraft": 1
  },
  "rankChange": {
    "sf_dynasty": { "days7": 0, "days30": -1 },
    "non_sf_dynasty": { "days7": 0, "days30": 0 },
    "sf_redraft": { "days7": 1, "days30": 2 },
    "non_sf_redraft": { "days7": 0, "days30": 1 }
  },
  "positionRankChange": {
    "sf_dynasty": { "days7": 0, "days30": 0 },
    "non_sf_dynasty": { "days7": 0, "days30": 0 },
    "sf_redraft": { "days7": 0, "days30": 0 },
    "non_sf_redraft": { "days7": 0, "days30": 0 }
  },
  "valueChange": {
    "sf_dynasty": { "days7": 121, "days30": 340 },
    "non_sf_dynasty": { "days7": 98, "days30": 287 },
    "sf_redraft": { "days7": 45, "days30": 122 },
    "non_sf_redraft": { "days7": 51, "days30": 130 }
  }
}
```

## GET /players/:id/value-history

Full value history for a player in one format, newest first.

| Parameter | Description |
| --- | --- |
| `format` | League format (default `sf_dynasty`) |
| `window` | Max number of history entries (default: all) |
| `since` | `YYYY-MM-DD`. Only entries on or after this date (for incremental sync) |

```bash
curl "https://api.statsguyfantasy.com/api/v1/players/7564/value-history?format=sf_dynasty&window=2"
```

```json
{
  "id": "7564",
  "name": "Ja'Marr Chase",
  "format": "sf_dynasty",
  "history": [
    { "date": "2026-07-24", "value": 10874, "rank": 1, "positionRank": 1 },
    { "date": "2026-07-23", "value": 10851, "rank": 1, "positionRank": 1 }
  ]
}
```

## GET /picks

Rookie draft pick values across all formats. Years with slot-specific data (the current draft class) return one entry per slot (`slot`); future years return `early`/`mid`/`late` variants per round (`variant`).

| Parameter | Description |
| --- | --- |
| `year` | Filter by draft year (e.g. 2027) |

```json
{
  "total": 63,
  "picks": [
    {
      "id": "pick:2026:2.08",
      "year": 2026,
      "round": 2,
      "slot": 8,
      "value": {
        "sf_dynasty": 2100,
        "non_sf_dynasty": 1900,
        "sf_redraft": 0,
        "non_sf_redraft": 0
      }
    },
    {
      "id": "pick:2027:1:early",
      "year": 2027,
      "round": 1,
      "variant": "early",
      "value": {
        "sf_dynasty": 5400,
        "non_sf_dynasty": 4800,
        "sf_redraft": 0,
        "non_sf_redraft": 0
      }
    }
  ]
}
```

## POST /trades/evaluate

Compare the total value of two trade sides. Sides accept Sleeper player IDs and pick IDs (any pick ID form), with up to 20 assets per side. Response entries are positional (same order as the request arrays) and echo the canonical `id`.

An optional `date` (`YYYY-MM-DD`) evaluates the trade using that date's values, for retro trade grading. Each asset resolves to its latest snapshot on or before the date (14-day lookback); assets with no snapshot in that window come back `found: false`. Historical entries include `asOf`, the snapshot date actually used. Note: early/mid/late pick variants apply the current variant shape table to the historical base value, an approximation for past dates.

```bash
curl -X POST "https://api.statsguyfantasy.com/api/v1/trades/evaluate" \
  -H "Content-Type: application/json" \
  -d '{
    "format": "sf_dynasty",
    "sideA": ["7564", "pick:2027:1:early"],
    "sideB": ["4984", "pick:2026:2.08"]
  }'
```

```json
{
  "format": "sf_dynasty",
  "sideA": {
    "assets": [
      {
        "id": "7564",
        "type": "player",
        "name": "Ja'Marr Chase",
        "position": "WR",
        "team": "CIN",
        "value": 10874,
        "found": true
      },
      {
        "id": "pick:2027:1:early",
        "type": "pick",
        "name": "2027 Round 1 (early)",
        "value": 5400,
        "found": true
      }
    ],
    "totalValue": 16274
  },
  "sideB": {
    "assets": [
      {
        "id": "4984",
        "type": "player",
        "name": "Josh Allen",
        "position": "QB",
        "team": "BUF",
        "value": 9950,
        "found": true
      },
      {
        "id": "pick:2026:2.08",
        "type": "pick",
        "name": "2026 Pick 2.08",
        "value": 2100,
        "found": true
      }
    ],
    "totalValue": 12050
  },
  "differential": 4224
}
```

Unknown IDs come back with `found: false` and `value: 0` (they still count toward the totals as zero). The response reports side totals and the differential only.

## POST /trades/evaluate/batch

Evaluate up to 25 trades in one request. A batch counts as a single request against the rate limit. The body is `{ "trades": [ ... ] }` where each entry has the same shape as a single evaluate body (its own `format`, `date`, `sideA`, `sideB`). Results return positionally.

```bash
curl -X POST "https://api.statsguyfantasy.com/api/v1/trades/evaluate/batch" \
  -H "Content-Type: application/json" \
  -d '{
    "trades": [
      {
        "format": "sf_dynasty",
        "date": "2026-03-01",
        "sideA": ["7564"],
        "sideB": ["4984", "pick:2027:1"]
      },
      {
        "format": "non_sf_dynasty",
        "sideA": ["pick:2026:1.04"],
        "sideB": ["9509"]
      }
    ]
  }'
```

```json
{
  "total": 2,
  "results": [
    {
      "format": "sf_dynasty",
      "date": "2026-03-01",
      "sideA": { "assets": [ "..." ], "totalValue": 10534 },
      "sideB": { "assets": [ "..." ], "totalValue": 13890 },
      "differential": -3356
    },
    {
      "format": "non_sf_dynasty",
      "sideA": { "assets": [ "..." ], "totalValue": 3450 },
      "sideB": { "assets": [ "..." ], "totalValue": 3120 },
      "differential": 330
    }
  ]
}
```

Validation is atomic: any invalid entry fails the whole request with a `400` that includes the offending `index`. Unresolvable assets within a valid entry still come back per-asset as `found: false`.

## GET /trades/most-traded

Top 10 most-traded players per league type over a trailing window. Aggregate counts only.

| Parameter | Description |
| --- | --- |
| `days` | Trailing window, 1-365 (default 30) |

```json
{
  "days": 30,
  "mostTraded": {
    "dynasty_sf": [
      {
        "id": "7564",
        "name": "Ja'Marr Chase",
        "team": "CIN",
        "position": "WR",
        "tradeCount": 412
      }
    ],
    "dynasty_non_sf": [ "..." ],
    "redraft_sf": [ "..." ],
    "redraft_non_sf": [ "..." ]
  }
}
```

---

## Support

Questions, bug reports, or need higher rate limits? Email support@leaguebeat.com. For update and breaking-change notices, join the update list at https://statsguyfantasy.com/developers#api-updates. To understand what the numbers mean, read the methodology articles at https://statsguyfantasy.com/methodology. API usage is covered by the terms of service at https://statsguyfantasy.com/terms.
