← Dashboard Login with iRacing

HFR iRacing API

Complete documentation for the Half Fast Racing League iRacing API integration. Covers OAuth 2.0 + PKCE authentication, all available data endpoints, database schema, caching strategy, and deployment on cPanel.

Introduction

This integration connects the Half Fast Racing League website to the iRacing data API, allowing us to pull member stats, race results, league standings, and more. It uses OAuth 2.0 Authorization Code flow with PKCE (RFC 7636), which is the authentication method mandated by iRacing for third-party integrations.

Key credentials:

  • Client ID: half-fast
  • Client Type: server-side
  • Redirect URIs: https://league.halffast.racing/oauth/callback and https://staging.halffast.racing/oauth/callback
  • Audience: data-server
  • Test Environment: https://test.halffast.racing

⚠️ Security: The OAuth secret (IRACING_CLIENT_SECRET in config.php) must NEVER be exposed in client-side JavaScript, committed to public repositories, or included in HTML source. It exists only in server-side PHP.

Architecture

The system involves three domains working together:

  1. test.halffast.racing — The main test application. Users visit this, initiate login, and see data.
  2. league.halffast.racing/oauth/callback — The approved OAuth redirect URI. iRacing sends the user here after they log in. The callback validates state, exchanges the code for tokens, and redirects back to the test app.
  3. oauth.iracing.com — iRacing's OAuth server. Handles authentication and issues tokens.

All three subdomains are on the same cPanel account, sharing the same MySQL database (half7175_iracing), which is how the callback can save tokens that the test app then reads.

File Structure

test.halffast.racing/ (document root)
├── config.php — credentials, constants, DB config
├── db.php — DB connection, token/cache/state helpers
├── pkce.php — PKCE code verifier/challenge generation
├── login.php — initiates OAuth flow, redirects to iRacing
├── logout.php — clears tokens and session
├── index.php — dashboard, DB status, quick links
├── oauth/
│ └── callback.php — ← also deployed to league.halffast.racing/oauth/
├── api/
│ └── iracing.php — iRacingAPI class, all endpoint methods
├── test/
│ ├── data_explorer.php — interactive endpoint tester
│ ├── member_test.php — member info, career stats, recent races
│ ├── league_test.php — league info, seasons, standings, roster
│ ├── results_test.php — subsession results and lap data
│ └── api_log.php — view/clear API call history
└── docs/
└── index.php — this documentation page

Note on subdomain routing: The callback.php file must also be placed at league.halffast.racing/oauth/callback.php. Since both subdomains share the same cPanel account, you can either copy the file or create a symlink. The callback shares the same DB, so tokens written there are immediately readable by the test app.

OAuth 2.0 + PKCE Overview

OAuth 2.0 lets our application request permission to access a user's iRacing data without ever knowing their password. The complete flow:

  1. User visits login.php and clicks "Login with iRacing"
  2. We generate a random code_verifier and derive a code_challenge from it (PKCE)
  3. We store the code_verifier in the DB + session for later
  4. We redirect the browser to oauth.iracing.com/oauth2/auth with our client_id, redirect_uri, code_challenge, and a random state value
  5. The user logs into iRacing and approves access
  6. iRacing redirects to league.halffast.racing/oauth/callback with a one-time authorization code and the state value
  7. The callback validates the state (CSRF protection), then POSTs the code + code_verifier to oauth.iracing.com/oauth2/token
  8. iRacing validates the PKCE pair and returns an access_token (and possibly refresh_token)
  9. We store the token in MySQL and redirect back to the dashboard

PKCE Explained

PKCE (Proof Key for Code Exchange, RFC 7636) prevents authorization code interception attacks. Here's how it works:

// Step 1: Generate a random code_verifier (43–128 chars)
$verifier = base64url(random_bytes(64));
// e.g. "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"

// Step 2: Hash it to get the code_challenge
$challenge = base64url(sha256($verifier));
// e.g. "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"

// Step 3: Send challenge in /authorize (keep verifier secret)
// Step 4: Send verifier in /token — iRacing verifies it matches

This means even if someone intercepts the authorization code, they can't use it because they don't have the code_verifier.

The /authorize Endpoint

URL: https://oauth.iracing.com/oauth2/auth (GET — browser redirect)

ParameterRequiredValue
client_idhalf-fast
redirect_urihttps://league.halffast.racing/oauth/callback
response_typecode
scopeopenid
audiencedata-server
state✓ (recommended)Random 32-char hex string — returned in callback for CSRF validation
code_challenge✓ (PKCE)BASE64URL(SHA256(code_verifier))
code_challenge_method✓ (PKCE)S256
promptoptionalverify — forces re-auth even if session exists

On success, iRacing returns HTTP 302 to your redirect_uri with ?code=AUTH_CODE&state=YOUR_STATE. The code is single-use and expires quickly (typically 60 seconds).

The /token Endpoint

URL: https://oauth.iracing.com/oauth2/token (POST — server-to-server, application/x-www-form-urlencoded)

⚠️ Client Secret Masking Required — iRacing does NOT accept the raw client_secret. It must be masked before sending. Sending the plain value will silently fail with an auth error. See Client Secret Masking below.

Authorization Code Grant

Trade the authorization code received at your redirect URI for an access token. This is the final step of the OAuth flow.

ParameterRequiredValue
grant_typeauthorization_code
client_idhalf-fast
client_secretMasked secret — see masking section below
codeThe one-time authorization code from the callback querystring
redirect_uriMust exactly match what was used in /authorize
code_verifier✓ (PKCE)The original random verifier generated at login (NOT the challenge)

Refresh Token Grant

If a refresh_token was issued with the initial token response, it can be traded for a new access token without requiring user interaction. Each refresh token is single-use — the response will include a new refresh token.

POST https://oauth.iracing.com/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&client_id=half-fast
&client_secret=MASKED_SECRET
&refresh_token=STORED_REFRESH_TOKEN

Password Limited Grant

For server-side headless clients that run unattended. Only the registered user (the developer) can use this grant — it is not available to the general public. Both the client_secret and password must be masked before sending.

Rate limited: Expect calls to take 2+ seconds. This grant is intended for one-time use at startup only. Use the Refresh Token Grant to maintain the token afterward. Exceeding rate limits causes a temporary unauthorized_client lockout.

POST https://oauth.iracing.com/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=password_limited
&client_id=half-fast
&client_secret=MASKED_SECRET             // masked with client_id
&username=your%40email.com
&password=MASKED_PASSWORD             // masked with username
&scope=iracing.auth

Token Response (all grant types)

{
  "access_token":             "eyJhbGciOi...",  // use as Bearer token
  "token_type":               "Bearer",
  "expires_in":               600,               // seconds (10 minutes)
  "refresh_token":            "...",             // may be omitted
  "refresh_token_expires_in": 3600,              // may be omitted
  "scope":                    "iracing.auth"    // may be omitted
            

Note on expires_in: The docs show 600 seconds (10 minutes), not 3600 as commonly assumed. Our saveTokens() function uses the actual expires_in value from the response, so this is handled correctly. The refresh_token and refresh_token_expires_in fields may be omitted at iRacing's discretion.

Client Secret Masking

iRacing requires all secrets and passwords to be masked (hashed) before transmission. This is a one-way pre-transmission transform — the server hashes the masked value again before storing it. Sending the raw secret will result in authentication failure.

Algorithm

For client_secret: BASE64( SHA256( secret + lowercase(trim(client_id)) ) )
For password: BASE64( SHA256( password + lowercase(trim(username)) ) )

PHP Implementation (used in this project)

function maskSecret(string $secret, string $clientId): string {
    $normalizedId = strtolower(trim($clientId));
    $combined     = $secret . $normalizedId;
    return base64_encode(hash('sha256', $combined, true)); // raw=true → binary → base64

// Usage in callback.php:
$maskedSecret = maskSecret(IRACING_CLIENT_SECRET, IRACING_CLIENT_ID);
// → sends masked value, never the raw secret

Key Details

  • The identifier (client_id or username) is trimmed and lowercased before concatenation
  • hash('sha256', $combined, true) — the second argument true returns raw binary bytes, not a hex string. This is critical. Using the hex string (default) will produce the wrong hash.
  • The binary hash is then base64-encoded (standard base64, not base64url)
  • This masked value is what gets URL-encoded into the POST body via http_build_query()
  • Both maskSecret() and maskPassword() are implemented in pkce.php

Token Storage

Tokens are stored in the iracing_tokens MySQL table. The getValidToken() function checks for a non-expired token and returns it. The token is also checked on every page load.

When a token expires (after ~1 hour), the user must re-authenticate via login.php. Re-authentication is transparent — clicking "Login with iRacing" again will issue a new token.

Token Refresh

If a refresh_token was included in the token response, use the Refresh Token Grant to get a new access token without user interaction. Key facts from the iRacing docs:

  • Single-use: Each refresh token can only be used once. The response provides a new refresh token — always store the latest one.
  • May be omitted: iRacing may choose not to issue a refresh token, in which case the user must re-authenticate when the access token expires.
  • Expires: The refresh_token_expires_in field tells you how long the refresh token is valid.
  • Masked secret required: Same masking applies as in the authorization code grant.
POST https://oauth.iracing.com/oauth2/token

grant_type=refresh_token
&client_id=half-fast
&client_secret=maskSecret(IRACING_CLIENT_SECRET, IRACING_CLIENT_ID)
&refresh_token=STORED_REFRESH_TOKEN

If no refresh token is issued: access tokens expire in ~600 seconds (10 minutes). For an unattended league data app, consider the password_limited grant for initial auth at startup, then use refresh tokens to maintain access. See the /token docs for details.

iRacing API Request Pattern

The iRacing data API uses a two-step fetch pattern — critical to understand:

  1. Step 1: Call GET https://members-ng.iracing.com/data/{endpoint} with Authorization: Bearer TOKEN. The response is a small JSON with a link key.
  2. Step 2: Fetch the URL from the link key. This is an AWS S3 pre-signed URL. No auth header needed — fetch it directly. This returns the actual data payload.
// Step 1 — response from iRacing API:
{
  "link": "https://ir-data.s3.amazonaws.com/member/info?X-Amz-..."

// Step 2 — fetch that link, get actual data:
{
  "cust_id":      12345,
  "display_name": "Driver Name",
  // ...
            

The iRacingAPI class handles this automatically. All endpoint methods return the final data array.

Member Endpoints

MethodEndpointParametersCache
GET/data/member/infoNo params — returns authenticated user60s
GET/data/member/profilecust_id=XXXXX5min
GET/data/member/recent_racescust_id=XXXXX5min
GET/data/member/careercust_id=XXXXX10min
GET/data/member/yearlycust_id=XXXXX10min

Member Info Response Fields

{
  "cust_id":       12345,
  "display_name":  "First Last",
  "email":         "user@example.com",
  "member_since":  "2018-03-15T00:00:00Z",
  "licenses": [    // one per category (road, oval, dirt road, dirt oval)
    {
      "category_id":   2,
      "category":      "road",
      "license_level": 20,     // 1=R, 5=D, 10=C, 15=B, 20=A, 25=Pro
      "safety_rating": 3.45,
      "irating":       2150,
      "tt_rating":     1234
    }
  ]
        

League Endpoints

MethodEndpointParametersCache
GET/data/league/getleague_id=XXXXX10min
GET/data/league/seasonsleague_id=XXXXX [&retired=0]10min
GET/data/league/season_standingsleague_id=X&season_id=X5min
GET/data/league/season_sessionsleague_id=X&season_id=X5min
GET/data/league/rosterleague_id=X [&include_licenses=1]10min

💡 Finding your League ID: Log into iRacing.com, navigate to your league page. The URL will contain the league ID, e.g. /membersite/member/League.do?leagueid=12345

Results Endpoints

MethodEndpointParametersNotes
GET/data/results/getsubsession_id=XXXXXFull race result with all finishers
GET/data/results/lap_datasubsession_id=X&simsession_number=0Every lap for every driver
GET/data/results/lap_chart_datasubsession_id=X&simsession_number=0Position-over-lap chart data
GET/data/results/search_hostedVarious filters (see below)Search hosted/league sessions
GET/data/results/search_seriesVarious filtersSearch official series results

search_hosted Filters

start_range_begin=2024-01-01T00:00:00Z
start_range_end=2024-12-31T23:59:59Z
host_cust_id=XXXXX
league_id=XXXXX
session_name=Race+Name

Series Endpoints

EndpointDescriptionCache
/data/series/getAll iRacing series definitions1hr
/data/series/stats_seriesSeries with cars, tracks per week1hr
/data/series/seasonsSeason list, optionally filtered by series_id1hr

Cars & Tracks

EndpointDescriptionCache
/data/car/getAll car definitions (id, name, category, sku)24hr
/data/carclass/getCar class groupings24hr
/data/track/getAll track definitions (id, name, config, category)24hr

Lookup Endpoints

EndpointParametersDescription
/data/lookup/getnoneLicense levels, categories, club list, countries
/data/lookup/driverssearch_term=NameSearch for iRacing members by name

Stats Endpoints

EndpointParametersDescription
/data/stats/season_driver_standingsseason_id=X&car_class_id=0Official series driver standings for a season
/data/stats/season_tt_standingsseason_id=X&car_class_id=0Time trial standings
/data/stats/season_team_standingsseason_id=X&car_class_id=0Team standings for a season
/data/stats/world_recordscar_id=X&track_id=XWorld records for a car/track combo

Hosted Sessions

EndpointDescription
/data/hosted/combined_sessionsCurrently active/upcoming hosted and league sessions

Database Schema

iracing_tokens

id            INT AUTO_INCREMENT PRIMARY KEY
user_id       VARCHAR(64)     -- 'system' for single-user setup
access_token  TEXT            -- JWT bearer token
refresh_token TEXT            -- for token refresh (if provided)
token_type    VARCHAR(32)     -- 'Bearer'
expires_at    DATETIME        -- when the token expires
scope         TEXT
created_at    DATETIME
updated_at    DATETIME

iracing_oauth_state

id            INT AUTO_INCREMENT PRIMARY KEY
state         VARCHAR(128)    -- random CSRF state value
code_verifier VARCHAR(256)    -- PKCE code verifier, deleted after use
created_at    DATETIME        -- auto-purged after 10 minutes

iracing_cache

id          INT AUTO_INCREMENT PRIMARY KEY
cache_key   VARCHAR(256)    -- md5 of endpoint+params
data        LONGTEXT        -- JSON response
expires_at  DATETIME
created_at  DATETIME

iracing_api_log

id           INT AUTO_INCREMENT PRIMARY KEY
endpoint     VARCHAR(512)
method       VARCHAR(8)      -- GET, POST
status_code  SMALLINT
response_kb  FLOAT
duration_ms  INT
created_at   DATETIME

Caching Strategy

The iRacingAPI class uses MySQL-backed caching to minimize API calls and improve performance. Cache TTLs are set per endpoint based on how frequently data changes:

Data TypeCache TTLReason
Member Info60 secondsChanges rarely, but want freshness
Recent Races / Career5–10 minutesUpdated after each race
League Standings5 minutesUpdated during/after sessions
Series / Season Lists1 hourWeekly schedule changes
Cars / Tracks24 hoursRarely change mid-season
Subsession Results24 hoursHistorical, never changes

The Data Explorer bypasses cache (TTL=0) so you always get fresh data when testing. Set $useCache = false in the iRacingAPI constructor to disable globally.

Server Setup

1. Upload Files

Upload all files to your test.halffast.racing document root. Via cPanel File Manager or FTP.

2. Set Up the Callback Subdomain

The oauth/callback.php file must be accessible at https://league.halffast.racing/oauth/callback. Options:

  • Copy: Upload callback.php to league.halffast.racing/oauth/callback.php
  • Symlink: SSH in and run: ln -s /home/half7175/test.halffast.racing/oauth /home/half7175/league.halffast.racing/oauth

3. Update the Config Path in callback.php

The callback.php file includes a path to config.php. You'll need to update this to match the actual server path. Find your home directory path via cPanel → File Manager, then hardcode it:

require_once '/home/half7175/test.halffast.racing/config.php';

4. Create the DB Tables

Visit https://test.halffast.racing/index.php — the dashboard calls DB::install() which creates all tables automatically on first load.

5. Test the OAuth Flow

Visit https://test.halffast.racing/login.php and click "Login with iRacing". If everything is configured correctly, you'll be redirected to iRacing, then back to the dashboard with a success message.

Subdomain & Callback Routing

Both league.halffast.racing and staging.halffast.racing are registered as redirect URIs with iRacing. The current code uses the league subdomain. To use staging, change IRACING_REDIRECT_URI in config.php.

Important: The redirect_uri in the /authorize request must exactly match one of the registered URIs. Even a trailing slash difference will cause an error.

Security Notes

  • config.php — Add to .gitignore if using version control. Never commit credentials.
  • State parameter — Validates that the callback is in response to our request (CSRF protection). Always verify it.
  • PKCE — Ensures the token exchange can only be completed by the party that initiated the flow.
  • HTTPS — All communication must use HTTPS. Your cPanel host should have SSL certificates for all subdomains.
  • Token storage — Tokens are stored in MySQL, not cookies or localStorage. The session only stores a flag.
  • Error messages — In production (ENV = 'production'), error details are hidden from users and logged server-side only.
  • API rate limits — iRacing enforces rate limits. The caching system helps stay within limits. Don't disable caching in production.

Troubleshooting

"Invalid State" error

The state stored during /authorize doesn't match what was returned. Causes: session expired before callback, multiple browser tabs, or the DB record was purged. Solution: try again. Also ensure iracing_oauth_state table exists and SESSION_NAME is consistent across the app.

Token Exchange Returns 400/401

Check: (1) code_verifier was correctly stored and retrieved, (2) redirect_uri matches exactly, (3) the auth code hasn't expired (they're valid for ~60 seconds only), (4) client_secret is correct.

API Returns null

Check the API log (test/api_log.php) for the HTTP status code. 401 = token expired (re-authenticate). 422 = missing or invalid parameters. 429 = rate limited (add more caching).

Callback file not found (404)

Ensure the file exists at league.halffast.racing/oauth/callback.php and that the subdomain's document root is correctly configured in cPanel.

Database connection error

Verify credentials in config.php match those in cPanel → MySQL Databases. The DB user must have SELECT, INSERT, UPDATE, DELETE, CREATE privileges on the half7175_iracing database.