Authentication

Most endpoints are open. A few carry player data and need an API key in the Saphi-Api-Key header - ask us for one. The API is currently read-only; write-endpoints might be added over time.

# open endpoint, no credentials
curl https://www.projectsaphi.com/api/v2/tracks

# player data, needs a key
curl -H "Saphi-Api-Key: your-api-key" \
  https://www.projectsaphi.com/api/v2/players

Rate limits

Every request costs budget, and expensive endpoints cost more. Each response reports what you have left in X-RateLimit-Remaining. A 429 tells you how long to wait in Retry-After.

# every response
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 2

# budget spent
HTTP/1.1 429
Retry-After: 5
{ "error": "Rate limit exceeded.", "code": "RATE_LIMITED" }

Caching

Shared reads carry an ETag. Send it back as If-None-Match and an unchanged response comes back as a bodyless 304, charged 1 instead of the endpoint's cost.

# first response
ETag: "8f14e45fceea167a"

# ask again with the validator
curl -H 'If-None-Match: "8f14e45fceea167a"' \
  https://www.projectsaphi.com/api/v2/tracks

HTTP/1.1 304 # unchanged, no body sent

Event 2 endpoints

List the kinds of timeline event that exist.

display_in_sr_history marks the types that /site-records/progression interleaves into its meta; the others are timeline-only.

Example

curl https://www.projectsaphi.com/api/v2/event-types

Response

{
    "data": [
        {
            "id": 2,
            "name": "Track Update",
            "color": "#4a7fd4",
            "display_in_sr_history": true
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 3,
        "total": 3,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

List timeline events.

Newest first. An event with a null track_id applies to the whole site rather than one track, and a null category_id applies to both categories. The timeline is small enough to fetch whole, so there is no name search - filter the result client-side.

Parameters

Name Type Required Description
track_id integer No Only events attached to this track
event_type_id integer No Only events of this type, as listed by /event-types
category_id integer No Only events attached to this category: 1 (Course) or 2 (Lap)
page integer No Page number, starting at 1.
per_page integer No Items per page. Capped at 100 without an API key, 500 with one.

Example

curl https://www.projectsaphi.com/api/v2/events

Response

{
    "data": [
        {
            "id": 4,
            "event_type_id": 2,
            "event_type_name": "Shortcut Discovery",
            "event_type_color": "#4a7fd4",
            "event_type_display_in_sr_history": true,
            "track_id": 1,
            "track_name": "Drive Thru Danger",
            "category_id": null,
            "category_name": null,
            "name": "MisterM Skip",
            "description": "Dropping down at the start to the S section was discovered.",
            "event_date": "2026-03-14 12:00:00",
            "created_at": "2026-03-14 19:02:11"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 100,
        "total": 12,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

Leaderboard 4 endpoints

Fetch the leaderboard for a single track.

Entries are ranked, and carry the achieved standard, SR:PR ratio, points and percentile.

Parameters

Name Type Required Description
track_id integer Yes Track to read
category_id integer No Category to filter by: 1 (Course) or 2 (Lap)
engine_id integer No Only times set with this engine
page integer No Page number, starting at 1.
per_page integer No Items per page. Capped at 100 without an API key, 500 with one.

Example

curl https://www.projectsaphi.com/api/v2/leaderboards?track_id=1

Response

{
    "data": [
        {
            "id": 25523,
            "rank": 1,
            "user_id": 31,
            "username": "Infernal",
            "display_name": null,
            "name": "Infernal",
            "country_id": 83,
            "country_name": "Italy",
            "category_id": 1,
            "category_name": "Course",
            "character_id": 14,
            "character_name": "Penta Penguin",
            "engine_id": 4,
            "engine_name": "Turn",
            "paintjob_id": 14,
            "paintjob_name": "Penta White",
            "skin_id": null,
            "skin_name": null,
            "wheels_id": 1,
            "wheels_name": "Standard",
            "default_cam": 0,
            "time": 9223,
            "time_formatted": "1:32.23",
            "submitted_at": "2026-02-10 21:49:39",
            "standard_id": 2,
            "standard_name": "God",
            "sr_pr": 1,
            "points": 30,
            "percentile": 100
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 100,
        "total": 345,
        "total_pages": 4
    }
}

A real response, trimmed to a single item.

Returns an overview of tracks that have leaderboards.

Each entry carries track data and how many players have submitted times on that track.

Parameters

Name Type Required Description
scope string No Track pool to draw from One of: saphi, community, original
page integer No Page number, starting at 1.
per_page integer No Items per page. Capped at 100 without an API key, 500 with one.

Example

curl https://www.projectsaphi.com/api/v2/leaderboards/overview

Response

{
    "data": [
        {
            "track_id": 1,
            "track_name": "Drive Thru Danger",
            "author": "Boxic",
            "lap_count": 3,
            "difficulty": 9,
            "track_type_id": 1,
            "track_type": "saphi",
            "submission_count": 1966,
            "player_count": 345,
            "thumbnail_url": "/media/thumbnails/original/9e28f866-c645-4320-af0a-870d99499240.jpg"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 100,
        "total": 30,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

List the site records on every track.

Parameters

Name Type Required Description
category_id integer No Category to filter by: 1 (Course) or 2 (Lap)
engine_id integer No Only records set with this engine
country_id integer No Only records held by players from this country
scope string No Track pool to draw from One of: saphi, community, original

Example

curl https://www.projectsaphi.com/api/v2/site-records

Response

{
    "data": [
        {
            "id": 25523,
            "track_id": 1,
            "track_name": "Drive Thru Danger",
            "track_author": "Boxic",
            "category_id": 1,
            "category_name": "Course",
            "user_id": 31,
            "username": "Infernal",
            "display_name": null,
            "name": "Infernal",
            "country_id": 83,
            "country_name": "Italy",
            "character_id": 14,
            "character_name": "Penta Penguin",
            "engine_id": 4,
            "engine_name": "Turn",
            "paintjob_id": 14,
            "paintjob_name": "Penta White",
            "skin_id": null,
            "skin_name": null,
            "wheels_id": 1,
            "wheels_name": "Standard",
            "default_cam": 0,
            "time": 9223,
            "time_formatted": "1:32.23",
            "submitted_at": "2026-02-10 21:49:39",
            "standard_id": 2,
            "standard_name": "God"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 61,
        "total": 61,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

Trace how the site record for a track changed over time.

Historical site records are returned oldest first, each with the improvement over the previous record. Timeline events affecting the track are in meta.events rather than mixed into the list.

Parameters

Name Type Required Description
track_id integer Yes Track to trace
category_id integer No Category to filter by: 1 (Course) or 2 (Lap)
start_date integer No Unix timestamp; ignore records before this
end_date integer No Unix timestamp; ignore records after this

Example

curl https://www.projectsaphi.com/api/v2/site-records/progression?track_id=1

Response

{
    "data": [
        {
            "index": 1,
            "user_id": 5,
            "username": "TheUbMunster",
            "name": "TheUbMunster",
            "country_id": 192,
            "country_name": "United States of America",
            "time": 17203,
            "time_formatted": "2:52.03",
            "improvement": null,
            "improvement_formatted": null,
            "character_id": 1,
            "character_name": "Crash Bandicoot",
            "engine_id": 1,
            "engine_name": "Balanced",
            "standard_tier_id": null,
            "standard_tier_name": null,
            "submit_date": "2025-10-15 00:19:58",
            "submission_id": 1
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 21,
        "total": 21,
        "total_pages": 1,
        "track_id": 1,
        "track_name": "Drive Thru Danger",
        "category_id": 1,
        "has_records": true,
        "events": [
            {
                "name": "Blounard Skip",
                "description": null,
                "type": "Shortcut Discovery",
                "color": "#8c1818",
                "date": "2025-11-06 23:00:00"
            }
        ]
    }
}

A real response, trimmed to a single item.

Metrics 1 endpoints

List every metric the client can record.

The catalogue behind /metrics/{userId}, which only reports the metrics a player actually has values for. Read this to know what may appear and how to render it: type is how repeated sessions are merged, format is how the value should be shown, and divisor has already been applied to the value that endpoint returns.

Example

curl https://www.projectsaphi.com/api/v2/metrics

Response

{
    "data": [
        {
            "key": "total_distance_driven",
            "label": "Total Distance Driven",
            "type": "cumulative",
            "format": "float",
            "icon": "fa-road",
            "divisor": 7680
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 30,
        "total": 30,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

Player 8 endpoints

Compare two players head to head.

Returns each player's time per track alongside the gap between them.

Parameters

Name Type Required Description
player1_id integer Yes First player Example: 5
player2_id integer Yes Second player Example: 13
categories string No Comma-separated category IDs (default 1,2) Example: 1,2

Example

curl -H "Saphi-Api-Key: your-api-key" https://www.projectsaphi.com/api/v2/matchups?player1_id=5&player2_id=13&categories=1,2

Response

{
    "data": [
        {
            "player1": {
                "id": 5,
                "username": "TheUbMunster",
                "display_name": null,
                "name": "TheUbMunster",
                "country_id": 192,
                "country_name": "United States of America",
                "wins": 0
            },
            "player2": {
                "id": 13,
                "username": "Garma",
                "display_name": null,
                "name": "Garma",
                "country_id": 65,
                "country_name": "Germany",
                "wins": 60
            },
            "ties": 0,
            "total_tracks": 60,
            "overall_winner": 2,
            "comparisons": [
                {
                    "track_id": 1,
                    "track_name": "Drive Thru Danger",
                    "track_author": "Boxic",
                    "category_id": 1,
                    "category_name": "Course",
                    "player1_time": 14236,
                    "player2_time": 10649,
                    "player1_time_formatted": "2:22.36",
                    "player2_time_formatted": "1:46.49",
                    "player1_character_id": 7,
                    "player1_character_name": "Polar",
                    "player2_character_id": 14,
                    "player2_character_name": "Penta Penguin",
                    "difference": 3587,
                    "difference_formatted": "35.87",
                    "winner": 2
                }
            ]
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 1,
        "total": 1,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

Fetch a player's recorded race metrics.

Play time, distance driven and similar counters the Saphi client reports.

Parameters

Name Type Required Description
userId path integer Yes Player to report on Example: 5
track_id integer No Only metrics for this track

Example

curl -H "Saphi-Api-Key: your-api-key" https://www.projectsaphi.com/api/v2/metrics/5

Response

{
    "data": [
        {
            "trackId": 1,
            "key": "average_airtime",
            "value": 0.18039159663866,
            "formattedValue": "0.18s",
            "label": "Average Airtime",
            "icon": "fa-cloud",
            "updatedAt": "2026-05-08 07:57:13"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 444,
        "total": 444,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

Fetch a player's personal bests.

Give either user_id or username.

Parameters

Name Type Required Description
user_id integer No Player to read Example: 5
username string No Player to read, by username
track_id integer No Only this track
category_id integer No Category to filter by: 1 (Course) or 2 (Lap)
engine_id integer No Only times set with this engine
per_engine boolean No Also return the best time per engine, tagged with engine_id
scope string No Track pool to draw from One of: saphi, community, original
page integer No Page number, starting at 1.
per_page integer No Items per page. Capped at 100 without an API key, 500 with one.

Example

curl -H "Saphi-Api-Key: your-api-key" https://www.projectsaphi.com/api/v2/pbs?user_id=5

Response

{
    "data": [
        {
            "id": 423,
            "track_id": 1,
            "track_name": "Drive Thru Danger",
            "category_id": 1,
            "category_name": "Course",
            "character_id": 7,
            "character_name": "Polar",
            "engine_id": 4,
            "engine_name": "Turn",
            "paintjob_id": 7,
            "paintjob_name": "Polar Cyan",
            "skin_id": null,
            "skin_name": null,
            "wheels_id": 1,
            "wheels_name": "Standard",
            "default_cam": 0,
            "time": 14236,
            "time_formatted": "2:22.36",
            "submitted_at": "2025-10-16 02:04:13",
            "rank": 63,
            "standard_id": 4,
            "standard_name": "Champion",
            "sr_pr": 0.6478645686990727,
            "points": 0,
            "percentile": 81.97674418604652
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 100,
        "total": 60,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

Search players.

Parameters

Name Type Required Description
id integer No Return only this player
username string No Filter by username, matching anywhere in it
country_id integer No Only players from this country
status_id integer No Only players with this account status. Unverified and banned accounts are never listed
discord_id string No Find the player linked to this Discord account
page integer No Page number, starting at 1.
per_page integer No Items per page. Capped at 100 without an API key, 500 with one.

Example

curl -H "Saphi-Api-Key: your-api-key" https://www.projectsaphi.com/api/v2/players

Response

{
    "data": [
        {
            "id": 1,
            "username": "Jasu14",
            "display_name": "Jasu14",
            "name": "Jasu14",
            "created_at": "2023-08-10 08:54:31",
            "country_id": 165,
            "country_name": "Spain",
            "status": 5
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 100,
        "total": 974,
        "total_pages": 10
    }
}

A real response, trimmed to a single item.

Fetch a player profile along with their statistics.

Give either user_id or username.

Parameters

Name Type Required Description
user_id integer No Player to fetch Example: 5
username string No Player to fetch, by username

Example

curl -H "Saphi-Api-Key: your-api-key" https://www.projectsaphi.com/api/v2/profiles?user_id=5

Response

{
    "data": [
        {
            "id": 5,
            "username": "TheUbMunster",
            "display_name": null,
            "name": "TheUbMunster",
            "avatar_url": null,
            "country": {
                "id": 192,
                "name": "United States of America"
            },
            "stats": {
                "total_points": 0,
                "course_points": 0,
                "lap_points": 0,
                "total_time": 502473,
                "total_time_formatted": "83:44.73",
                "average_finish": 132.85,
                "average_standard": 3.808,
                "average_sr_pr": 0.7684,
                "first_places": 0,
                "podium_finishes": 0,
                "tracks_submitted": 123,
                "completed_tracks": 30,
                "total_tracks": 30,
                "first_submission_at": "2025-10-15 00:19:58",
                "last_submission_at": "2026-05-27 21:25:02",
                "most_played_track": {
                    "id": 9,
                    "name": "Redrock Ravine",
                    "submission_count": 10
                },
                "most_played_character": {
                    "id": 1,
                    "name": "Crash Bandicoot",
                    "submission_count": 95
                },
                "most_played_engine": {
                    "id": 1,
                    "name": "Balanced",
                    "submission_count": 91
                }
            }
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 1,
        "total": 1,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

Fetch a player profile by ID.

The same payload as /profiles, addressable by path so any user_id in a response can be followed directly.

Parameters

Name Type Required Description
id path integer Yes Player to fetch

Example

curl -H "Saphi-Api-Key: your-api-key" https://www.projectsaphi.com/api/v2/profiles/1

Response

{
    "data": [
        {
            "id": 1,
            "username": "Jasu14",
            "display_name": "Jasu14",
            "name": "Jasu14",
            "avatar_url": null,
            "country": {
                "id": 165,
                "name": "Spain"
            },
            "stats": {
                "total_points": 6,
                "course_points": 0,
                "lap_points": 6,
                "total_time": 551348,
                "total_time_formatted": "91:53.48",
                "average_finish": 112.717,
                "average_standard": 3.404,
                "average_sr_pr": 0.7349,
                "first_places": 0,
                "podium_finishes": 0,
                "tracks_submitted": 125,
                "completed_tracks": 20,
                "total_tracks": 30,
                "first_submission_at": "2025-10-23 00:04:22",
                "last_submission_at": "2026-02-11 11:08:04",
                "most_played_track": {
                    "id": 6,
                    "name": "Green Hill Raceway",
                    "submission_count": 13
                },
                "most_played_character": {
                    "id": 1,
                    "name": "Crash Bandicoot",
                    "submission_count": 31
                },
                "most_played_engine": {
                    "id": 4,
                    "name": "Turn",
                    "submission_count": 74
                }
            }
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 1,
        "total": 1,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

Fetch what a player has unlocked.

Parameters

Name Type Required Description
id path integer Yes Player to report on
show_locked boolean No Also list unlockables the player has not earned

Example

curl -H "Saphi-Api-Key: your-api-key" https://www.projectsaphi.com/api/v2/profiles/1/unlockables

Response

{
    "data": [
        {
            "id": 23,
            "key": "oxide_character",
            "name": "Oxide",
            "description": null,
            "type": "character",
            "type_label": "Character",
            "hidden": true,
            "progress": true,
            "unlocked": true,
            "unlocked_at": "2026-04-30 23:11:32"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 2,
        "total": 2,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

Fetch which standards a player has reached.

The standard times of every track together with the best standard the given player has achieved.

Parameters

Name Type Required Description
player_id integer Yes Player to report on Example: 5

Example

curl -H "Saphi-Api-Key: your-api-key" https://www.projectsaphi.com/api/v2/standards/achievements?player_id=5

Response

{
    "data": [
        {
            "track_id": 1,
            "track_name": "Drive Thru Danger",
            "track_author": "Boxic",
            "standards": {
                "1": {
                    "2": {
                        "time": 12400,
                        "time_formatted": "2:04.00",
                        "achieved": false
                    },
                    "3": {
                        "time": 12800,
                        "time_formatted": "2:08.00",
                        "achieved": false
                    },
                    "4": {
                        "time": 14400,
                        "time_formatted": "2:24.00",
                        "achieved": true
                    },
                    "5": {
                        "time": 16000,
                        "time_formatted": "2:40.00",
                        "achieved": false
                    }
                },
                "2": {
                    "2": {
                        "time": 3950,
                        "time_formatted": "39.50",
                        "achieved": false
                    },
                    "3": {
                        "time": 4100,
                        "time_formatted": "41.00",
                        "achieved": false
                    },
                    "4": {
                        "time": 4400,
                        "time_formatted": "44.00",
                        "achieved": false
                    },
                    "5": {
                        "time": 5000,
                        "time_formatted": "50.00",
                        "achieved": true
                    }
                }
            }
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 26,
        "total": 26,
        "total_pages": 1,
        "player_id": 5,
        "categories": [
            {
                "id": 1,
                "name": "Course"
            }
        ],
        "tiers": [
            {
                "id": 2,
                "name": "God"
            }
        ]
    }
}

A real response, trimmed to a single item.

Ranking 7 endpoints

Players ranked by their average finishing position.

Parameters

Name Type Required Description
category string No Category to rank within: course, lap or combined
type string No Either single player or country One of: player, country
country integer No Only include players from this country
engine integer No Only include times set with this engine
scope string No Track pool to draw from One of: saphi, community, original
page integer No Page number, starting at 1.
per_page integer No Items per page. Capped at 100 without an API key, 500 with one.

Example

curl https://www.projectsaphi.com/api/v2/rankings/average-finish

Response

{
    "data": [
        {
            "id": 31,
            "username": "Infernal",
            "name": "Infernal",
            "country_id": 83,
            "country_name": "Italy",
            "rank": 1,
            "tracks_submitted": 60,
            "total_tracks": 60,
            "average_finish": 3.01667,
            "average_by_category": {
                "1": 3.1,
                "2": 2.93333
            }
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 100,
        "total": 704,
        "total_pages": 8,
        "ranking_type": "player",
        "category": null,
        "track_counts": {
            "total": 60,
            "by_category": {
                "1": 30,
                "2": 30
            }
        }
    }
}

A real response, trimmed to a single item.

Players ranked by their average standard.

Parameters

Name Type Required Description
category string No Category to rank within: course, lap or combined
type string No Either single player or country One of: player, country
country integer No Only include players from this country
engine integer No Only include times set with this engine
scope string No Track pool to draw from One of: saphi, community, original
page integer No Page number, starting at 1.
per_page integer No Items per page. Capped at 100 without an API key, 500 with one.

Example

curl https://www.projectsaphi.com/api/v2/rankings/average-rank

Response

{
    "data": [
        {
            "id": 31,
            "username": "Infernal",
            "name": "Infernal",
            "country_id": 83,
            "country_name": "Italy",
            "rank": 1,
            "tracks_submitted": 52,
            "total_tracks": 52,
            "average_rank": 0,
            "average_by_category": {
                "1": 0,
                "2": 0
            },
            "standard_id": 2,
            "standard_name": "God",
            "standard_by_category": {
                "1": 2,
                "2": 2
            },
            "standard_name_by_category": {
                "1": "God",
                "2": "God"
            },
            "lowest_standard_id": 2,
            "lowest_standard_name": "God",
            "lowest_standard_by_category": {
                "1": 2,
                "2": 2
            },
            "lowest_standard_name_by_category": {
                "1": "God",
                "2": "God"
            }
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 100,
        "total": 704,
        "total_pages": 8,
        "ranking_type": "player",
        "category": "total",
        "track_counts": {
            "total": 52,
            "by_category": {
                "1": 26,
                "2": 26
            }
        }
    }
}

A real response, trimmed to a single item.

Players by based on their finishing positions.

Parameters

Name Type Required Description
category string No Category to rank within: course, lap or combined
type string No Either single player or country One of: player, country
country integer No Only include players from this country
engine integer No Only include times set with this engine
scope string No Track pool to draw from One of: saphi, community, original
page integer No Page number, starting at 1.
per_page integer No Items per page. Capped at 100 without an API key, 500 with one.

Example

curl https://www.projectsaphi.com/api/v2/rankings/medal-table

Response

{
    "data": [
        {
            "id": 31,
            "username": "Infernal",
            "name": "Infernal",
            "country_id": 83,
            "country_name": "Italy",
            "rank": 1,
            "tracks_submitted": 60,
            "total_tracks": 60,
            "total_medals": {
                "1": 19,
                "2": 16,
                "3": 8,
                "4_5": 10,
                "6_10": 6,
                "11_20": 1,
                "21_50": 0,
                "51_100": 0,
                "101_200": 0
            },
            "medals_by_category": {
                "1": {
                    "1": 8,
                    "2": 9,
                    "3": 4,
                    "4_5": 5,
                    "6_10": 4,
                    "11_20": 0,
                    "21_50": 0,
                    "51_100": 0,
                    "101_200": 0
                },
                "2": {
                    "1": 11,
                    "2": 7,
                    "3": 4,
                    "4_5": 5,
                    "6_10": 2,
                    "11_20": 1,
                    "21_50": 0,
                    "51_100": 0,
                    "101_200": 0
                }
            }
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 100,
        "total": 704,
        "total_pages": 8,
        "ranking_type": "player",
        "category": null,
        "track_counts": {
            "total": 60,
            "by_category": {
                "1": 30,
                "2": 30
            }
        }
    }
}

A real response, trimmed to a single item.

Players ranked by their average percentile.

Parameters

Name Type Required Description
category string No Category to rank within: course, lap or combined
type string No Either single player or country One of: player, country
country integer No Only include players from this country
engine integer No Only include times set with this engine
scope string No Track pool to draw from One of: saphi, community, original
page integer No Page number, starting at 1.
per_page integer No Items per page. Capped at 100 without an API key, 500 with one.

Example

curl https://www.projectsaphi.com/api/v2/rankings/percentile

Response

{
    "data": [
        {
            "id": 31,
            "username": "Infernal",
            "name": "Infernal",
            "country_id": 83,
            "country_name": "Italy",
            "rank": 1,
            "tracks_submitted": 60,
            "total_tracks": 60,
            "percentile": 99.221089166,
            "percentile_by_category": {
                "1": 99.181701666,
                "2": 99.260476666
            }
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 100,
        "total": 704,
        "total_pages": 8,
        "ranking_type": "player",
        "category": null,
        "track_counts": {
            "total": 60,
            "by_category": {
                "1": 30,
                "2": 30
            }
        }
    }
}

A real response, trimmed to a single item.

Players ranked by points earned.

Parameters

Name Type Required Description
category string No Category to rank within: course, lap or combined
type string No Either single player or country One of: player, country
country integer No Only include players from this country
engine integer No Only include times set with this engine
scope string No Track pool to draw from One of: saphi, community, original
page integer No Page number, starting at 1.
per_page integer No Items per page. Capped at 100 without an API key, 500 with one.

Example

curl https://www.projectsaphi.com/api/v2/rankings/points

Response

{
    "data": [
        {
            "id": 31,
            "username": "Infernal",
            "name": "Infernal",
            "country_id": 83,
            "country_name": "Italy",
            "rank": 1,
            "tracks_submitted": 60,
            "total_tracks": 60,
            "points": 1679,
            "points_by_category": {
                "1": 837,
                "2": 842
            }
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 100,
        "total": 129,
        "total_pages": 2,
        "ranking_type": "player",
        "category": null,
        "track_counts": {
            "total": 60,
            "by_category": {
                "1": 30,
                "2": 30
            }
        }
    }
}

A real response, trimmed to a single item.

Players by how close they are to the site record.

Parameters

Name Type Required Description
category string No Category to rank within: course, lap or combined
type string No Either single player or country One of: player, country
country integer No Only include players from this country
engine integer No Only include times set with this engine
scope string No Track pool to draw from One of: saphi, community, original
page integer No Page number, starting at 1.
per_page integer No Items per page. Capped at 100 without an API key, 500 with one.

Example

curl https://www.projectsaphi.com/api/v2/rankings/sr-pr

Response

{
    "data": [
        {
            "id": 31,
            "username": "Infernal",
            "name": "Infernal",
            "country_id": 83,
            "country_name": "Italy",
            "rank": 1,
            "tracks_submitted": 60,
            "total_tracks": 60,
            "sr_pr": 0.981188666,
            "sr_pr_by_category": {
                "1": 0.988939,
                "2": 0.973438333
            }
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 100,
        "total": 704,
        "total_pages": 8,
        "ranking_type": "player",
        "category": null,
        "track_counts": {
            "total": 60,
            "by_category": {
                "1": 30,
                "2": 30
            }
        }
    }
}

A real response, trimmed to a single item.

Players ranked by the sum of their best times.

Parameters

Name Type Required Description
category string No Category to rank within: course, lap or combined
type string No Either single player or country One of: player, country
country integer No Only include players from this country
engine integer No Only include times set with this engine
scope string No Track pool to draw from One of: saphi, community, original
page integer No Page number, starting at 1.
per_page integer No Items per page. Capped at 100 without an API key, 500 with one.

Example

curl https://www.projectsaphi.com/api/v2/rankings/total-time

Response

{
    "data": [
        {
            "id": 31,
            "username": "Infernal",
            "name": "Infernal",
            "country_id": 83,
            "country_name": "Italy",
            "rank": 1,
            "tracks_submitted": 60,
            "total_tracks": 60,
            "total_time": 391558,
            "total_time_formatted": "65:15.58",
            "time_by_category": {
                "1": {
                    "total": 299884,
                    "total_formatted": "49:58.84",
                    "tracks": 30,
                    "submitted": 30
                },
                "2": {
                    "total": 91674,
                    "total_formatted": "15:16.74",
                    "tracks": 30,
                    "submitted": 30
                }
            }
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 100,
        "total": 704,
        "total_pages": 8,
        "ranking_type": "player",
        "category": null,
        "track_counts": {
            "total": 60,
            "by_category": {
                "1": 30,
                "2": 30
            }
        }
    }
}

A real response, trimmed to a single item.

Reference 12 endpoints

List time trial categories.

Example

curl https://www.projectsaphi.com/api/v2/categories

Response

{
    "data": [
        {
            "id": 1,
            "name": "Course"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 2,
        "total": 2,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

List playable characters.

Example

curl https://www.projectsaphi.com/api/v2/characters

Response

{
    "data": [
        {
            "id": 1,
            "game_id": 0,
            "name": "Crash Bandicoot",
            "default_engine_id": 1,
            "default_paintjob_id": 1,
            "default_skin_id": null,
            "default_wheels_id": 1,
            "engine_name": "Balanced",
            "default_paintjob_name": "Crash Blue",
            "default_skin_name": null,
            "default_wheels_name": "Standard",
            "submission_count": 2404
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 16,
        "total": 16,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

List the countries players can represent.

Parameters

Name Type Required Description
page integer No Page number, starting at 1.
per_page integer No Items per page. Capped at 100 without an API key, 500 with one.

Example

curl https://www.projectsaphi.com/api/v2/countries

Response

{
    "data": [
        {
            "id": 1,
            "name": "Afghanistan"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 100,
        "total": 200,
        "total_pages": 2
    }
}

A real response, trimmed to a single item.

List available engine styles.

Example

curl https://www.projectsaphi.com/api/v2/engines

Response

{
    "data": [
        {
            "id": 1,
            "game_id": 0,
            "name": "Balanced",
            "submission_count": 5544
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 4,
        "total": 4,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

List game modes.

A game mode says what a track media version can be played for. Currently it is only used for tagging track media.

Example

curl https://www.projectsaphi.com/api/v2/game-modes

Response

{
    "data": [
        {
            "id": 1,
            "slug": "time_trial",
            "name": "Time Trial"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 3,
        "total": 3,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

List available character paintjobs.

Example

curl https://www.projectsaphi.com/api/v2/paintjobs

Response

{
    "data": [
        {
            "id": 1,
            "game_id": 0,
            "name": "Crash Blue"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 15,
        "total": 15,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

Fetch every reference data list in one request.

Characters, engines, categories, standards, paintjobs, skins, wheels, game modes, track types and user statuses together, so an external application can warm its lookups in a single call instead of ten.

Example

curl https://www.projectsaphi.com/api/v2/reference-data

Response

{
    "data": [
        {
            "characters": [
                {
                    "id": 1,
                    "game_id": 0,
                    "name": "Crash Bandicoot",
                    "default_engine_id": 1,
                    "default_paintjob_id": 1,
                    "default_skin_id": null,
                    "default_wheels_id": 1,
                    "engine_name": "Balanced",
                    "default_paintjob_name": "Crash Blue",
                    "default_skin_name": null,
                    "default_wheels_name": "Standard",
                    "submission_count": 2404
                }
            ],
            "engines": [
                {
                    "id": 1,
                    "game_id": 0,
                    "name": "Balanced",
                    "submission_count": 5544
                }
            ],
            "categories": [
                {
                    "id": 1,
                    "name": "Course"
                }
            ],
            "standards": [
                {
                    "id": 1,
                    "name": "Faker",
                    "display_order": 1,
                    "numeric_value": null,
                    "include_in_average": false
                }
            ],
            "paintjobs": [
                {
                    "id": 1,
                    "game_id": 0,
                    "name": "Crash Blue"
                }
            ],
            "skins": [],
            "wheels": [
                {
                    "id": 1,
                    "game_id": 0,
                    "name": "Standard"
                }
            ],
            "game_modes": [
                {
                    "id": 1,
                    "slug": "time_trial",
                    "name": "Time Trial"
                }
            ],
            "track_types": [
                {
                    "id": 1,
                    "slug": "saphi",
                    "name": "Saphi"
                }
            ],
            "user_statuses": [
                {
                    "id": 3,
                    "name": "Verified"
                }
            ]
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 1,
        "total": 1,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

List available character skins.

Example

curl https://www.projectsaphi.com/api/v2/skins

Response

{
    "data": [],
    "meta": {
        "current_page": 1,
        "per_page": 0,
        "total": 0,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

List available standards.

Example

curl https://www.projectsaphi.com/api/v2/standards

Response

{
    "data": [
        {
            "id": 1,
            "name": "Faker",
            "display_order": 1,
            "numeric_value": null,
            "include_in_average": false
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 5,
        "total": 5,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

List the pools a track can belong to.

Resolves the track_type_id carried by /tracks. Saphi tracks are the curated pool the standards and rankings are built on, community tracks are everything else players have uploaded, and original tracks are the ones that shipped with the game. Each pool is a separate leaderboard; times are never compared across them. Only saphi and community tracks are downloadable, so a submission for an original track carries no track_media_id.

Example

curl https://www.projectsaphi.com/api/v2/track-types

Response

{
    "data": [
        {
            "id": 1,
            "slug": "saphi",
            "name": "Saphi"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 3,
        "total": 3,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

List the account statuses a player can have.

Resolves the status field carried by /players and /profiles. Only verified accounts and above appear in the player list, so a consumer of those endpoints will not see the lower statuses in practice.

Example

curl https://www.projectsaphi.com/api/v2/user-statuses

Response

{
    "data": [
        {
            "id": 3,
            "name": "Verified"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 5,
        "total": 5,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

List available kart wheels.

Example

curl https://www.projectsaphi.com/api/v2/wheels

Response

{
    "data": [
        {
            "id": 1,
            "game_id": 0,
            "name": "Standard"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 1,
        "total": 1,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

Statistics 1 endpoints

Fetch the global statistics

Totals, recent activity, track and world record leaders, character and engine usage, standard achievement counts and completion distributions. totals.tracks and completion are keyed by track pool slug, one entry per pool from /track-types, so a pool added later does not change the payload shape; a pool with no active tracks has a null completion entry. Recomputed at most every 15 minutes, and expensive to request: read the ETag and send If-None-Match rather than polling this endpoint.

Example

curl https://www.projectsaphi.com/api/v2/statistics

Response

{
    "data": [
        {
            "totals": {
                "tracks": { "saphi": 30, "community": 21, "original": 18, "total": 69 },
                "categories": 2,
                "countries": 69,
                "characters": 16,
                "players": 974,
                "unlockables": 13,
                "submissions": { "total": 44901, "course": 23271, "lap": 21630 }
            },
            "activity": {
                "submissions_last_24h": 12,
                "submissions_last_7d": 286,
                "submissions_last_30d": 1319,
                "average_per_day": 44,
                "most_active_weekday": { "day_name": "Thursday", "submission_count": 7442 },
                "peak_day": { "peak_date": "2026-01-01", "submission_count": 966 },
                "most_active_players": [{ "id": 189, "name": "RandomFun", "submission_count": 1204 }],
                "newest_active_players": [{ "id": 812, "name": "Newcomer", "created_at": "2026-07-02 10:11:00" }]
            },
            "tracks": {
                "most_submissions": { "course": [], "lap": [] },
                "most_players": { "course": [], "lap": [] },
                "most_record_changes": { "course": [], "lap": [] },
                "tightest_competition": { "course": [], "lap": [] },
                "most_playtime": [],
                "most_popular": []
            },
            "world_records": {
                "oldest": { "course": {}, "lap": {} },
                "newest": { "course": {}, "lap": {} },
                "top_players": { "course": [], "lap": [] },
                "top_countries": { "course": [], "lap": [] },
                "top_characters": { "course": [], "lap": [] }
            },
            "players": {
                "most_podiums": { "course": [], "lap": [] },
                "countries_with_most_players": [],
                "countries_with_most_submissions": []
            },
            "usage": {
                "characters": { "course": [], "lap": [] },
                "engines": { "course": [], "lap": [] }
            },
            "standards": { "standards": [], "no_standards": 0 },
            "unlockables": { "most_common": [], "least_common": [] },
            "completion": { "saphi": {}, "community": {}, "original": null }
        }
    ]
}

A real response, trimmed to a single item.

Submission 4 endpoints

List recently submitted times.

Parameters

Name Type Required Description
time_filter string No How far back to look: a count of hours, or a value like 24h, 7d or 30d (default 24h) Example: 30d
track_id integer No Only this track
category_id integer No Category to filter by: 1 (Course) or 2 (Lap)
user_id integer No Only this player
page integer No Page number, starting at 1.
per_page integer No Items per page. Capped at 100 without an API key, 500 with one.

Example

curl https://www.projectsaphi.com/api/v2/submissions/recent?time_filter=30d

Response

{
    "data": [
        {
            "id": 44982,
            "user_id": 249,
            "username": "jmz",
            "display_name": null,
            "name": "jmz",
            "country_id": 32,
            "country_name": "Canada",
            "track_id": 13,
            "track_name": "Papu's SG Temple",
            "category_id": 2,
            "category_name": "Lap",
            "character_id": 11,
            "character_name": "Ripper Roo",
            "engine_id": 4,
            "engine_name": "Turn",
            "paintjob_id": 14,
            "paintjob_name": "Penta White",
            "skin_id": null,
            "skin_name": null,
            "wheels_id": 1,
            "wheels_name": "Standard",
            "default_cam": 1,
            "time": 2916,
            "time_formatted": "29.16",
            "submitted_at": "2026-07-23 16:55:08",
            "is_world_record": false
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 100,
        "total": 1253,
        "total_pages": 13
    }
}

A real response, trimmed to a single item.

Fetch a single submission.

The same shape as an entry of /submissions/recent.

Parameters

Name Type Required Description
id path integer Yes Submission to fetch

Example

curl https://www.projectsaphi.com/api/v2/submissions/1

Response

{
    "data": [
        {
            "id": 1,
            "user_id": 5,
            "username": "TheUbMunster",
            "display_name": null,
            "name": "TheUbMunster",
            "country_id": 192,
            "country_name": "United States of America",
            "track_id": 1,
            "track_name": "Drive Thru Danger",
            "category_id": 1,
            "category_name": "Course",
            "character_id": 1,
            "character_name": "Crash Bandicoot",
            "engine_id": 1,
            "engine_name": "Balanced",
            "paintjob_id": 1,
            "paintjob_name": "Crash Blue",
            "skin_id": null,
            "skin_name": null,
            "wheels_id": 1,
            "wheels_name": "Standard",
            "default_cam": 0,
            "time": 17203,
            "time_formatted": "2:52.03",
            "submitted_at": "2025-10-15 00:19:58",
            "is_world_record": false
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 1,
        "total": 1,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

Download a submission's ghost file.

Returns the file itself, not JSON. A few kilobytes.

Parameters

Name Type Required Description
id path integer Yes Submission the ghost belongs to

Example

curl https://www.projectsaphi.com/api/v2/submissions/1/ghost

Download a submission's replay file.

Returns the file itself, not JSON. A few kilobytes.

Parameters

Name Type Required Description
id path integer Yes Submission the replay belongs to

Example

curl https://www.projectsaphi.com/api/v2/submissions/1/replay

System 2 endpoints

Check that the REST API is reachable.

Example

curl https://www.projectsaphi.com/api/v2/heartbeat

Response

{
    "data": [
        {
            "status": "ok",
            "timestamp": 1785172828,
            "processing_time_ms": 2.86,
            "client": {
                "ip": "127.0.0.1",
                "user_agent": null
            }
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 1,
        "total": 1,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

Fetch the OpenAPI description of the REST API.

An OpenAPI 3.1 document covering every endpoint an outside consumer can call. Generated from the API's own routing attributes, so it always matches the deployed code. Returned unwrapped, not inside the usual data envelope.

Example

curl https://www.projectsaphi.com/api/v2/openapi.json

Response

{
    "openapi": "3.1.0",
    "info": {
        "title": "Saphi REST API",
        "version": "2.0.0",
        "description": "..."
    },
    "servers": [{ "url": "https://example.com" }],
    "tags": [{ "name": "Track" }],
    "paths": {
        "/api/v2/tracks": {
            "get": {
                "operationId": "api.v2.tracks",
                "summary": "List tracks",
                "x-rate-limit-cost": 2,
                "x-rate-limit-bucket": "api",
                "parameters": [],
                "responses": {}
            }
        }
    },
    "components": {
        "securitySchemes": { "ApiKey": { "type": "apiKey", "in": "header", "name": "Saphi-Api-Key" } },
        "schemas": {}
    }
}

A real response, trimmed to a single item.

Track 6 endpoints

List the standard times of every track.

The same rows the standards array of /tracks carries, listed on their own so they can be fetched without the tracks around them and filtered by tier or category. Use /standards/achievements instead if you need to know which of them a player has reached.

Parameters

Name Type Required Description
track_id integer No Only standards of this track
tier_id integer No Only standards of this tier, as listed by /standards
category_id integer No Only standards of this category: 1 (Course) or 2 (Lap)
page integer No Page number, starting at 1.
per_page integer No Items per page. Capped at 100 without an API key, 500 with one.

Example

curl https://www.projectsaphi.com/api/v2/track-standards

Response

{
    "data": [
        {
            "id": 26,
            "track_id": 1,
            "tier_id": 2,
            "tier_name": "God",
            "tier_numeric_value": 0,
            "category_id": 1,
            "category_name": "Course",
            "time": 12400,
            "time_formatted": "2:04.00",
            "track_name": "Drive Thru Danger"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 100,
        "total": 408,
        "total_pages": 5
    }
}

A real response, trimmed to a single item.

List active tracks across every pool.

The three relation arrays are all included unless turned off. Set the include_* flags to 0 for a lighter payload - each one you drop also drops the lookups behind it.

Parameters

Name Type Required Description
id integer No Return only this track
name string No Filter by name or description
author_id integer No Only tracks by this author
is_active boolean No Filter by whether the track is currently playable
track_type string No Restrict to one pool One of: saphi, community, original
include_standards boolean No Include the standards array (default true)
include_downloads boolean No Include the downloads array (default true)
include_events boolean No Include the events array - timeline events attached to the track (default true)
page integer No Page number, starting at 1.
per_page integer No Items per page. Capped at 100 without an API key, 500 with one.

Example

curl https://www.projectsaphi.com/api/v2/tracks

Response

{
    "data": [
        {
            "id": 1,
            "name": "Drive Thru Danger",
            "author": "Boxic",
            "lap_count": 3,
            "difficulty": 9,
            "is_active": true,
            "track_type_id": 1,
            "track_type": "saphi",
            "submission_count": 1966,
            "average_rating": 8.6,
            "rating_count": 5,
            "created_at": "2025-10-14 18:35:30",
            "standards": [
                {
                    "id": 26,
                    "track_id": 1,
                    "tier_id": 2,
                    "tier_name": "God",
                    "tier_numeric_value": 0,
                    "category_id": 1,
                    "category_name": "Course",
                    "time": 12400,
                    "time_formatted": "2:04.00"
                }
            ],
            "downloads": [
                {
                    "id": 339,
                    "track_media_id": 282,
                    "type": "sca",
                    "crc32_hash": 4081018681,
                    "version": "1.0.0",
                    "modes": [
                        "time_trial"
                    ],
                    "file_size": 385228,
                    "file_size_formatted": "376.2 KB",
                    "download_url": "/api/v2/tracks/1/downloads/339",
                    "created_at": "2026-05-21 00:33:34"
                }
            ],
            "events": [
                {
                    "id": 1,
                    "event_type_id": 1,
                    "event_type_name": "Shortcut Discovery",
                    "event_type_color": "#8c1818",
                    "event_type_display_in_sr_history": true,
                    "track_id": 1,
                    "track_name": "Drive Thru Danger",
                    "category_id": null,
                    "category_name": null,
                    "name": "MisterM Skip",
                    "description": null,
                    "event_date": "2026-02-02 23:00:00",
                    "created_at": "2026-02-03 18:16:16"
                }
            ]
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 100,
        "total": 51,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

Fetch a single track.

Identical to /tracks filtered to one id, addressable by path.

Parameters

Name Type Required Description
id path integer Yes Track to fetch
include_standards boolean No Include the standards array (default true)
include_downloads boolean No Include the downloads array (default true)
include_events boolean No Include the events array - timeline events attached to the track (default true)

Example

curl https://www.projectsaphi.com/api/v2/tracks/1

Response

{
    "data": [
        {
            "id": 1,
            "name": "Drive Thru Danger",
            "author": "Boxic",
            "lap_count": 3,
            "difficulty": 9,
            "is_active": true,
            "track_type_id": 1,
            "track_type": "saphi",
            "submission_count": 1966,
            "average_rating": 8.6,
            "rating_count": 5,
            "created_at": "2025-10-14 18:35:30",
            "standards": [
                {
                    "id": 26,
                    "track_id": 1,
                    "tier_id": 2,
                    "tier_name": "God",
                    "tier_numeric_value": 0,
                    "category_id": 1,
                    "category_name": "Course",
                    "time": 12400,
                    "time_formatted": "2:04.00"
                }
            ],
            "downloads": [
                {
                    "id": 339,
                    "track_media_id": 282,
                    "type": "sca",
                    "crc32_hash": 4081018681,
                    "version": "1.0.0",
                    "modes": [
                        "time_trial"
                    ],
                    "file_size": 385228,
                    "file_size_formatted": "376.2 KB",
                    "download_url": "/api/v2/tracks/1/downloads/339",
                    "created_at": "2026-05-21 00:33:34"
                }
            ],
            "events": [
                {
                    "id": 1,
                    "event_type_id": 1,
                    "event_type_name": "Shortcut Discovery",
                    "event_type_color": "#8c1818",
                    "event_type_display_in_sr_history": true,
                    "track_id": 1,
                    "track_name": "Drive Thru Danger",
                    "category_id": null,
                    "category_name": null,
                    "name": "MisterM Skip",
                    "description": null,
                    "event_date": "2026-02-02 23:00:00",
                    "created_at": "2026-02-03 18:16:16"
                }
            ]
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 1,
        "total": 1,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

List a track's downloadable files.

Parameters

Name Type Required Description
id path integer Yes Track to list files for
type string No Filter to one media type, such as lev, vrm or sca

Example

curl https://www.projectsaphi.com/api/v2/tracks/1/downloads

Response

{
    "data": [
        {
            "id": 339,
            "track_media_id": 282,
            "type": "sca",
            "crc32_hash": 4081018681,
            "version": "1.0.0",
            "modes": [
                "time_trial"
            ],
            "file_size": 385228,
            "file_size_formatted": "376.2 KB",
            "download_url": "/api/v2/tracks/1/downloads/339",
            "created_at": "2026-05-21 00:33:34"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 5,
        "total": 5,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

Download one track file.

Returns the file itself, not JSON. A whole track is three files and up to 2.5 MB, so this endpoint draws from the separate download budget.

Parameters

Name Type Required Description
id path integer Yes Track the file belongs to
download_id path integer Yes File to download, taken from the downloads list Example: 10

Example

curl https://www.projectsaphi.com/api/v2/tracks/1/downloads/10

Fetch a track's community rating summary.

Rating a track is a website action; the API reports the result only.

Parameters

Name Type Required Description
id path integer Yes Track to fetch ratings for

Example

curl https://www.projectsaphi.com/api/v2/tracks/1/ratings

Response

{
    "data": [
        {
            "track_id": 1,
            "average": 8.6,
            "count": 5,
            "min": 1,
            "max": 10
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 1,
        "total": 1,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

Track Author 1 endpoints

List track authors.

A track author is a different entity from a user - reason being that it is possible for a track to be authored by someone who is not registered on the site. If a track author is registered, the user_id and username is set.

Parameters

Name Type Required Description
name string No Filter by name, matching anywhere in it
page integer No Page number, starting at 1.
per_page integer No Items per page. Capped at 100 without an API key, 500 with one.

Example

curl https://www.projectsaphi.com/api/v2/track-authors

Response

{
    "data": [
        {
            "id": 2,
            "name": "Anfrost",
            "user_id": 10,
            "username": "Anfrost",
            "has_user": true
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 100,
        "total": 13,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

Unlockable 2 endpoints

List the kinds of unlockables that exist.

Example

curl https://www.projectsaphi.com/api/v2/unlockable-types

Response

{
    "data": [
        {
            "id": 3,
            "name": "character",
            "label": "Character"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 4,
        "total": 4,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.

List available unlockables.

Parameters

Name Type Required Description
type string No Restrict to one unlockable type
hidden boolean No Include unlockables normally hidden until earned
progress boolean No Include the progress requirements of each unlockable

Example

curl https://www.projectsaphi.com/api/v2/unlockables

Response

{
    "data": [
        {
            "id": 21,
            "key": "digital_n_tropy_skin",
            "name": "Digital N. Tropy Skin",
            "description": null,
            "type": "skin",
            "type_label": "Skin",
            "hidden": true,
            "progress": true
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 13,
        "total": 13,
        "total_pages": 1
    }
}

A real response, trimmed to a single item.