The Off the Upright API

What the API is

The Off the Upright API gives approved key holders doink data as JSON the moment it is added to the Off the Upright catalog: doinks, kickers and franchises, with the calculated fields the site uses, such as stakes tags and career records. A change feed lists what was added, updated or removed, so removals reach you too. A keyed CSV export is also available. Bulk data and the R package will be published on GitHub Releases (link coming).

Getting a key

Request a key at /developers/request. You'll give a project name and a contact email, and you can say what you plan to build. I read every request and approve keys by hand. I send approved keys to the contact email you gave. A key is shown once, when it's issued, and can't be retrieved afterward, so save it when it arrives. The request form asks you to accept the terms on this page.

Authentication

Send your key in an Authorization header on every request, exactly like this:

Authorization: Bearer otu_live_<your 40 character key>
curl -H "Authorization: Bearer $OTU_KEY" "https://offtheupright.com/api/v1/doinks/2025"

All requests are GET (or HEAD) over https://. Plain http:// requests put your key on the wire in the clear, so don't use them.

Never put the key in the URL. A request that carries api_key=, key=, token= or access_token= in the query string is refused with a 400 and the key is not checked. URLs are written to server logs, so if a key was ever put in one, treat it as exposed: write to contact@offtheupright.com and ask for a replacement.

You may call the API from a browser page, and cross-origin requests are allowed. A key in a public page is visible to everyone who views the page, so see the terms below.

Endpoints

Every path is under https://offtheupright.com/api/v1/. A season is the NFL season as it is stored, not the calendar year, from 1974 to the current calendar year plus one. Decades are written 1970s, 1980s and so on, up to the current decade. Every parameter and value is checked, and an unknown one returns a 400.

PathFiltersReturns
doinksseason, week, franchise (slug), kicker (slug or id), play_type (FG or XP), outcome (good or no-good), official (1)Every doink, as doink rows. Filters combine with AND.
doinks/{season}noneThat season's doink rows and a summary block.
doinks/decade/{decade}noneThat decade's doink rows, with seasons and leaders.
kickersnoneEvery kicker, with career records for all doinks and for official games only.
kickers/{season}noneKickers with a doink that season: doink count, games played, rate and team.
franchisesnoneEvery franchise with a doink, with all-time records for all doinks and for official games only.
franchises/{season}noneOne row per franchise for that season.
franchises/decade/{decade}noneOne row per franchise, summed over the decade's seasons.
changessince (required)Doinks added, updated and removed since a time. See below.
exportnoneThe whole dataset as CSV.

Every JSON response uses one envelope. data holds the results, count is the number of items, and next is always null for now. It is reserved for paging if a response ever outgrows one body. Season and decade endpoints add summary, seasons or leaders next to data.

Example responses are below. The values are placeholders, not records, and several are shortened.

/api/v1/doinks
{
    "api_version": "v1",
    "generated_at": "2025-11-03T14:00:00Z",
    "count": 1,
    "next": null,
    "data": [
        {
            "doink_id": 1234,
            "kicker": {
                "kicker_id": 57,
                "name": "Example Kicker",
                "slug": "example-kicker",
                "gsis_id": null,
                "is_position_kicker": true
            },
            "kicking_team": "PHI",
            "kicking_franchise": "philadelphia-eagles",
            "play_type": "FG",
            "distance": 48,
            "quarter": "4",
            "game_time": "0:03",
            "outcome": "No Good",
            "stakes": "losing",
            "margin": -2,
            "is_official": true,
            "career_doink_no": 7,
            "game": {
                "season": 2025,
                "week": "9",
                "is_postseason": false,
                "game_date": "2025-11-02",
                "away": "PHI",
                "home": "DAL",
                "away_score": 21,
                "home_score": 23,
                "stadium": "Example Stadium",
                "nflverse_game_id": null,
                "url": "https://offtheupright.com/game/2025-w9-phi-dal"
            },
            "created_at": "2025-11-02T23:41:07Z",
            "updated_at": "2025-11-03T01:12:50Z"
        }
    ]
}
/api/v1/doinks/2025
{
    "api_version": "v1",
    "generated_at": "2025-11-03T14:00:00Z",
    "count": 1,
    "next": null,
    "summary": {
        "doinks": 31,
        "made": 12,
        "no_good": 19,
        "made_pct": 38.7,
        "fg": 26,
        "xp": 5,
        "official": {
            "doinks": 30,
            "made": 12,
            "no_good": 18,
            "fg": 25,
            "xp": 5
        }
    },
    "data": [
        {
            "doink_id": 1234,
            "kicker": {
                "kicker_id": 57,
                "name": "Example Kicker",
                "slug": "example-kicker",
                "gsis_id": null,
                "is_position_kicker": true
            },
            "kicking_team": "PHI",
            "kicking_franchise": "philadelphia-eagles",
            "play_type": "FG",
            "distance": 48,
            "quarter": "4",
            "game_time": "0:03",
            "outcome": "No Good",
            "stakes": "losing",
            "margin": -2,
            "is_official": true,
            "career_doink_no": 7,
            "game": {
                "season": 2025,
                "week": "9",
                "is_postseason": false,
                "game_date": "2025-11-02",
                "away": "PHI",
                "home": "DAL",
                "away_score": 21,
                "home_score": 23,
                "stadium": "Example Stadium",
                "nflverse_game_id": null,
                "url": "https://offtheupright.com/game/2025-w9-phi-dal"
            },
            "created_at": "2025-11-02T23:41:07Z",
            "updated_at": "2025-11-03T01:12:50Z"
        }
    ]
}
/api/v1/kickers
{
    "api_version": "v1",
    "generated_at": "2025-11-03T14:00:00Z",
    "count": 1,
    "next": null,
    "data": [
        {
            "kicker_id": 57,
            "name": "Example Kicker",
            "slug": "example-kicker",
            "url": "https://offtheupright.com/kicker/example-kicker",
            "career": {
                "all": {
                    "total": 12,
                    "made": 5,
                    "nogood": 7,
                    "made_pct": 41.7,
                    "fg_count": 11,
                    "fg_made": 5,
                    "xp_count": 1,
                    "xp_made": 0,
                    "avg_fg_dist": 46.2,
                    "longest_dist": 53,
                    "longest_outcome": "No Good",
                    "wins": 6,
                    "losses": 6,
                    "ties": 0,
                    "total_games": 12,
                    "win_pct": 50,
                    "seasons": [
                        2023,
                        2024,
                        2025
                    ]
                },
                "official": {
                    "total": 12,
                    "made": 5,
                    "nogood": 7,
                    "made_pct": 41.7,
                    "fg_count": 11,
                    "fg_made": 5,
                    "xp_count": 1,
                    "xp_made": 0,
                    "avg_fg_dist": 46.2,
                    "longest_dist": 53,
                    "longest_outcome": "No Good",
                    "wins": 6,
                    "losses": 6,
                    "ties": 0,
                    "total_games": 12,
                    "win_pct": 50,
                    "seasons": [
                        2023,
                        2024,
                        2025
                    ]
                }
            },
            "career_doink_rate": 0.08
        }
    ]
}
/api/v1/franchises
{
    "api_version": "v1",
    "generated_at": "2025-11-03T14:00:00Z",
    "count": 1,
    "next": null,
    "data": [
        {
            "franchise_id": 21,
            "slug": "philadelphia-eagles",
            "display_name": "Philadelphia Eagles",
            "url": "https://offtheupright.com/franchise/philadelphia-eagles",
            "abbreviations": [
                {
                    "abbreviation": "PHI",
                    "first_year": 1933,
                    "last_year": null
                }
            ],
            "all_time": {
                "all": {
                    "doinks_for_count": 40,
                    "doinks_against_count": 38,
                    "total_doinks": 78
                },
                "official": {
                    "doinks_for_count": 39,
                    "doinks_against_count": 38,
                    "total_doinks": 77
                }
            }
        }
    ]
}
/api/v1/changes?since=2025-11-02T00:00:00Z
{
    "api_version": "v1",
    "generated_at": "2025-11-03T14:00:00Z",
    "next": null,
    "data": {
        "since": "2025-11-02T00:00:00Z",
        "until": "2025-11-03T14:00:00Z",
        "added": [
            {
                "doink_id": 1234,
                "kicker": {
                    "kicker_id": 57,
                    "name": "Example Kicker",
                    "slug": "example-kicker",
                    "gsis_id": null,
                    "is_position_kicker": true
                },
                "kicking_team": "PHI",
                "kicking_franchise": "philadelphia-eagles",
                "play_type": "FG",
                "distance": 48,
                "quarter": "4",
                "game_time": "0:03",
                "outcome": "No Good",
                "stakes": "losing",
                "margin": -2,
                "is_official": true,
                "career_doink_no": 7,
                "game": {
                    "season": 2025,
                    "week": "9",
                    "is_postseason": false,
                    "game_date": "2025-11-02",
                    "away": "PHI",
                    "home": "DAL",
                    "away_score": 21,
                    "home_score": 23,
                    "stadium": "Example Stadium",
                    "nflverse_game_id": null,
                    "url": "https://offtheupright.com/game/2025-w9-phi-dal"
                },
                "created_at": "2025-11-02T23:41:07Z",
                "updated_at": "2025-11-03T01:12:50Z"
            }
        ],
        "updated": [],
        "removed": [
            {
                "doink_id": 999,
                "removed_at": "2025-11-03T09:30:00Z",
                "reason": "Duplicate entry",
                "snapshot": {
                    "season": 2025,
                    "week": "9",
                    "game_date": "2025-11-02",
                    "away": "PHI",
                    "home": "DAL",
                    "kicker_name": "Example Kicker",
                    "kicking_team": "PHI",
                    "play_type": "FG",
                    "distance": 48,
                    "quarter": "4",
                    "game_time": "0:03",
                    "outcome": "No Good"
                }
            }
        ]
    }
}

kickers/{season}, franchises/{season} and the decade endpoints follow the same envelope with the fields described in their rows. export returns text/csv with one header row and a download filename of the form offtheupright-doinks-YYYY-MM-DD.csv (the date is UTC).

The doink row

Every endpoint that returns doinks uses the same row.

Editorial notes and video links are not included.

Conditional requests and caching

Responses are live: a new doink appears the moment it is added. Every cacheable response carries an ETag and a Last-Modified header and Cache-Control: private, max-age=60. Send the ETag back as If-None-Match (or the date as If-Modified-Since) and an unchanged response comes back as 304 Not Modified with no body. The ETag changes whenever the catalog does, so a 304 is the cheapest way to poll. The changes endpoint is never cached. Send Accept-Encoding: gzip to get compressed bodies.

Rate limits

Each key has a bucket of 60 requests that refills at one request per second, which is 3,600 requests an hour sustained. Every keyed response includes X-RateLimit-Limit (the bucket size) and X-RateLimit-Remaining (what is left after this request). When the bucket is empty the response is 429 with a Retry-After header in seconds. Wait that long before the next request. Rate limits count requests, including ones answered with a 304.

Changes feed

changes?since= takes a UTC time, either 2025-11-02T00:00:00Z or just a date, 2025-11-02. It returns three lists for the window from since up to the until time in the response:

Keep the until value and use it as your next since. The window can be at most 400 days. An older since is a 400 error. Use doinks or export for a full copy.

Terms of use (version 1)

By requesting a key, you agree to these terms.

  1. Credit the source. When you publicly display data from the API, name Off the Upright and link to offtheupright.com or the game page. Every doink row includes the game's URL.
  2. One key per project. Use a separate key for each project, and don't share yours. You may put a key in a public web page or app, but then it is public: if it leaks or is misused, it may be revoked.
  3. Stay within the rate limits. They are published on this page. A key that keeps exceeding them may be suspended.
  4. Keys can be revoked. Off the Upright may revoke a key at any time and will send the reason to your contact email. Revocation is not a ban: you can request a new key the same way, though approval remains at Off the Upright's discretion.
  5. The data is provided as is, as it appears in the Off the Upright catalog. You may cache and store it to run your project, but don't republish it as a standalone dataset or offer it through your own API without asking first. The API is read-only, so report errors through the site's correction forms.
  6. Your email is for your key only. Off the Upright uses it to issue, warn about or revoke your key, and will delete it when you ask or when your key is deleted.

Errors

Errors are JSON, with the same leading keys as a success and then error:

{"api_version":"v1","error":{"code":"bad_request","message":"Unknown parameter: seasons"}}
Statuserror.codeWhen
400bad_requestUnknown parameter, malformed value, a missing or unreadable since, a season or decade out of range, or a key sent in the URL.
401unauthorizedNo Authorization header, not a Bearer key, or a key that is unknown, revoked or expired. The message never says which.
404not_foundA path under /api/v1/ that no endpoint serves.
405method_not_allowedAny method except GET or HEAD. The Allow header lists them.
429rate_limitedYour bucket is empty. Wait for Retry-After seconds.
500server_errorSomething failed on our side. The message is fixed text.