Read maimai DX song data and public player records from your app.
The maishift API gives apps read access to maimai DX song data, to the records players have made public on maishift, and, with a player's permission, to that player's own data. It is read-only, and every request needs an API key.
This page explains how to start and the rules every endpoint follows. For each endpoint's parameters and responses, see the API reference.
X-API-Key header with every request:curl -H "X-API-Key: msk_…" "https://maimai.shiftpsh.com/api/v1/songs?region=intl"
Keep the key out of public code and repositories. If it leaks, revoke it in Settings › Developer and issue a new one. Each app can have two active keys, so you can switch to a new key before revoking the old one.
A mobile, desktop or browser app ships its key to every user, where it can be extracted. That's expected: the key identifies your app and its budget, it doesn't protect players, whose sign-in is protected by PKCE. Someone holding an extracted key can spend your app's budget, so revoke and replace it if that happens. When your app has a server, keep the key there.
| Endpoint | Returns |
|---|---|
GET /api/v1/songs |
Every song in a region, with its charts and their constants |
GET /api/v1/versions |
The game versions a region has had |
GET /api/v1/profiles/{handle} |
A player's public profile |
GET /api/v1/profiles/{handle}/records |
A player's latest records |
GET /api/v1/profiles/{handle}/snapshots |
A player's past syncs |
GET /api/v1/profiles/{handle}/snapshots/{snapshotId} |
A player's records at one past sync |
GET /api/v1/playlists/{playlistId} |
A public playlist |
GET /api/v1/me |
The player who authorized your app (OAuth) |
GET /api/v1/me/records |
Their latest records |
GET /api/v1/me/snapshots |
Their past syncs |
GET /api/v1/me/snapshots/{snapshotId} |
Their records at one past sync |
GET /api/v1/me/playlists |
Their playlists, private ones included |
GET /api/v1/me/playlists/{playlistId} |
One of their playlists |
GET /api/v1/stats/ratings |
How ratings are spread among active players |
GET /api/v1/stats/options |
Which game options players use |
GET /api/v1/stats/plates |
Version plates held, by rating |
GET /api/v1/stats/plate-difficulty |
How hard each version plate is, for a rating band |
GET /api/v1/charts/{chartId}/distribution |
How players score on one chart, by rating band |
GET /api/v1/stats/levels/{internalLevel} |
The same for every chart with one constant |
GET /api/v1/stats/level-charts |
Every chart of a level side by side, for one rating band |
GET /api/v1/stats/clear-ratios |
Which charts players have cleared, by progress on a level |
The full specification is also available as OpenAPI 3.1 at /api/v1/openapi.json, which you can use to generate a client.
Regions. Most endpoints take region: intl for the international version, jp for the Japanese one, or na for the international data as North American cabinets see it. na returns the same data as intl, except that stats/plate-difficulty follows the plate rules of North American cabinets. In both, songs North American cabinets can't select have naUnavailable: true.
Ids. Song and chart ids belong to a region. The same chart has different ids in intl and jp.
Versions. Versions are sent as integer ids, counting from 0 for the first maimai. GET /api/v1/versions gives each region's names and release dates. The kanji is the one version plates use, as in stats/plate-difficulty's prefix.
| id | Version | Kanji | jp release |
intl release |
|---|---|---|---|---|
| 0 | maimai | 舞 | 2012-07-11 | 2012-07-11 |
| 1 | maimai PLUS | 真 | 2012-12-13 | 2012-12-13 |
| 2 | GreeN | 超 | 2013-07-11 | 2013-07-11 |
| 3 | GreeN PLUS | 檄 | 2014-02-26 | 2014-02-26 |
| 4 | ORANGE | 橙 | 2014-09-18 | 2014-09-18 |
| 5 | ORANGE PLUS | 暁 | 2015-03-19 | 2015-03-19 |
| 6 | PiNK | 桃 | 2015-12-09 | 2015-12-09 |
| 7 | PiNK PLUS | 櫻 | 2016-06-30 | 2016-06-30 |
| 8 | MURASAKi | 紫 | 2016-12-15 | 2016-12-15 |
| 9 | MURASAKi PLUS | 菫 | 2017-06-22 | 2017-06-22 |
| 10 | MiLK | 白 | 2017-12-14 | 2017-12-14 |
| 11 | MiLK PLUS | 雪 | 2018-06-21 | 2018-06-21 |
| 12 | FiNALE | 輝 | 2018-12-13 | 2018-12-13 |
| 13 | でらっくす | 熊 | 2019-07-11 | 2020-01-15 |
| 14 | でらっくす PLUS | 華 | 2020-01-23 | 2020-07-29 |
| 15 | Splash | 爽 | 2020-09-17 | 2021-01-29 |
| 16 | Splash PLUS | 煌 | 2021-03-18 | 2021-07-30 |
| 17 | UNiVERSE | 宙 | 2021-09-16 | 2022-01-27 |
| 18 | UNiVERSE PLUS | 星 | 2022-03-24 | 2022-07-28 |
| 19 | FESTiVAL | 祭 | 2022-09-15 | 2023-01-19 |
| 20 | FESTiVAL PLUS | 祝 | 2023-03-23 | 2023-07-27 |
| 21 | BUDDiES | 双 | 2023-09-14 | 2024-01-18 |
| 22 | BUDDiES PLUS | 宴 | 2024-03-21 | 2024-07-25 |
| 23 | PRiSM | 鏡 | 2024-09-12 | 2025-01-16 |
| 24 | PRiSM PLUS | 彩 | 2025-03-13 | 2025-07-24 |
| 25 | CiRCLE | 丸 | 2025-09-18 | 2026-01-22 |
| 26 | CiRCLE PLUS | 廻 | 2026-03-19 | 2026-07-23 |
| 27 | MAGiCAL | — | 2026-09-17 | — |
intl and jp share the ids. From DX (13) on, intl gets each version later, and a version with no intl date hasn't been released there yet. Before DX there was no separate international release, so those versions show the Japanese dates for both.
Scaled integers. Numbers that have decimals in the game are sent as integers:
achievement is ×10000: 1005000 is 100.5000%.internalLevel (the chart constant) is ×10: 134 is 13.4.Constants. Each chart says where its constant comes from in internalLevelSource:
community: published by the community for the current version.maishift: set or inferred by maishift.estimated: not known for the current version yet. The value is an estimate from the displayed level and earlier versions, and ratings computed from it are marked ratingAccurate: false.Times. Timestamps are ISO 8601 in UTC. addedOn is a date (YYYY-MM-DD) in Japan time, as the game counts days.
Public data only. A profile and its latest records can be read only if the player has made their profile public. Past syncs (snapshots) also need the player to have made their past records public. Otherwise the API answers 403 not_public.
The statistics endpoints return only aggregates over players, never anyone's own records. They are recomputed on a schedule (hourly for new charts, otherwise daily or weekly), and most responses say when in computedAt.
Rating bands group players by their rating with an unfilled new-song pool filled in, so a player who hasn't played 15 new charts yet can sit in a higher band than their displayed rating.
When a group of players (a rating band, a bucket) has fewer than 5 players, its figures come back null with suppressed: true. At that size the numbers would describe particular players rather than a group. This mostly affects the very top of the rating range and charts that are new. It is a safeguard against reading individual players off a single figure, not a guarantee: figures for overlapping groups can sometimes be combined. Don't use the statistics to single out players.
How a small group shows differs a little by endpoint: stats/ratings nulls the median and top-percentile thresholds without a flag, and its histogram nulls the band's count too; stats/clear-ratios returns an empty charts; a level band whose player count isn't known yet is suppressed whatever its size. stats/plate-difficulty is an estimate built from many charts' figures and has no suppression.
Errors come with an HTTP status and a body like this:
{
"error": {
"code": "not_public",
"message": "This player's records are not public."
}
}
| Code | Status | Meaning |
|---|---|---|
missing_api_key |
401 | No X-API-Key header |
invalid_api_key |
401 | The key is wrong or revoked |
app_suspended |
403 | The app has been suspended |
invalid_token |
401 | The access token is missing, expired or revoked (/me only) |
insufficient_scope |
403 | The access token lacks the scope this endpoint needs (/me only) |
not_public |
403 | The player hasn't made this data public |
bad_request |
400 | A parameter is invalid, or a key was sent in the URL |
not_found |
404 | No such player, snapshot, playlist or chart, or a statistic not computed yet |
rate_limited |
429 | Too many requests; see below |
internal |
500 | Something went wrong on our side |
Never put the key in the URL: requests with a key in the query string (api_key, apikey, key or access_token) are refused.
On /me endpoints, refused tokens also carry a WWW-Authenticate header (RFC 6750): Bearer scope="…" when no token was sent, Bearer error="invalid_token" for a bad one, and Bearer error="insufficient_scope", scope="…" when it lacks the scope.
Each app has a budget of 120 units, refilled at 2 units per second (120 a minute). Its keys share the budget. Each request costs units by how heavy it is:
| Endpoint | Cost |
|---|---|
versions |
1 |
profiles/{handle}/snapshots, me/snapshots, me/playlists |
2 |
profiles/{handle}, playlists/{playlistId}, me, me/playlists/{playlistId} |
5 |
stats/ratings, stats/options, stats/plates, stats/levels/{internalLevel}, charts/{chartId}/distribution |
6 |
songs, profiles/{handle}/records, profiles/{handle}/snapshots/{snapshotId}, me/records, me/snapshots/{snapshotId} |
10 |
stats/level-charts, stats/clear-ratios |
15 |
stats/plate-difficulty |
30 |
Requests to /me endpoints draw on a separate budget for each player who authorized your app, the same size as the app's, so your room grows with your players. The token and revoke endpoints each cost 1 unit of the app's budget.
Once your key is accepted, every /api/v1 response carries X-RateLimit-Limit and X-RateLimit-Remaining, errors included (except app_suspended). On /me endpoints they describe the player's budget, except for a refused access token, which costs your app 1 unit and shows the app's budget. When you run out, the API answers 429 with a Retry-After header saying how many seconds to wait. The token and revoke endpoints send only Retry-After, with slow_down.
The song list changes at most a few times a day. Cache it instead of fetching it for every request.
Using the API means agreeing to the Developer Terms. In short:
With a player's permission, your app can read their own data, including what they haven't made public. maishift uses the OAuth 2.0 authorization code flow with PKCE, and PKCE is required for every app.
OAuth works only while your own maishift profile is public: players can see who made an app before they connect it. Until it is, the consent page tells players the app can't be connected.
1. Register redirect URIs. In Settings › Developer, add every URI your app receives the player back at. A URI must be https, http on localhost, 127.0.0.1 or [::1] for local testing, or an app scheme in reverse-DNS form such as com.example.app:/callback. It can't contain a fragment (#) or a user name, can't point at maishift itself, and can be up to 512 characters. An app can register up to 10, and requests must use one of them exactly.
2. Send the player to maishift. Make a random code_verifier of 43–128 characters from A–Z a–z 0–9 - . _ ~, and keep it. Its code_challenge is the base64url-encoded SHA-256 of the verifier, without padding. Then open the URL below, with your app's client ID from Settings › Developer. Values must be URL-encoded (the space between scopes becomes %20); the line breaks are only for reading.
https://maimai.shiftpsh.com/oauth/authorize
?response_type=code
&client_id=msc_…
&redirect_uri=https://example.com/callback
&scope=records offline.access
&state=…
&code_challenge=…
&code_challenge_method=S256
The player signs in and sees what your app is asking for. If they allow it, they come back to your redirect URI with code and your state; if not, with error=access_denied. Check that state is the one you sent.
A request that is wrong also comes back with an error and error_description, and your state: invalid_request (PKCE missing or malformed, or a method other than S256), invalid_scope (an unknown scope) or unsupported_response_type. Some problems never come back, and the player sees an error on maishift instead: an unknown client ID, a redirect URI you haven't registered, a suspended app, your profile not being public, and on the player's side a public-computer sign-in or a banned account.
3. Exchange the code for tokens within 10 minutes. The code works once.
curl -X POST https://maimai.shiftpsh.com/api/oauth/token \
-H "X-API-Key: msk_…" \
-d grant_type=authorization_code \
-d code=… \
-d redirect_uri=https://example.com/callback \
-d code_verifier=…
{
"access_token": "msa_…",
"token_type": "Bearer",
"expires_in": 7200,
"refresh_token": "msr_…",
"scope": "profile records offline.access"
}
The token endpoint takes form-encoded or JSON bodies. Your API key identifies the app, so client_id isn't needed here. redirect_uri must be the one you sent to /oauth/authorize. A failed exchange still uses up the code.
The token and revoke endpoints answer errors the OAuth way (RFC 6749), as { "error": "…", "error_description": "…" }:
error |
Status | Meaning |
|---|---|---|
invalid_client |
401 | The API key is missing, wrong or revoked, or the app is suspended |
invalid_grant |
400 | The code or refresh token is wrong, expired, used, or another app's; PKCE or redirect_uri didn't match; or the player or app can no longer sign in |
invalid_request |
400 | A required field is missing |
invalid_scope |
400 | A refresh asked for an unknown scope, or for scopes beyond the ones granted |
unsupported_grant_type |
400 | grant_type is neither authorization_code nor refresh_token |
slow_down |
429 | The app's budget is spent; wait for Retry-After |
server_error |
500 | Something went wrong on our side |
4. Call the /me endpoints with both your API key and the access token:
curl -H "X-API-Key: msk_…" -H "Authorization: Bearer msa_…" \
https://maimai.shiftpsh.com/api/v1/me/records
| Scope | Lets your app |
|---|---|
profile |
Know who the player is (/me). Always granted. |
records |
Read their latest records, even if their profile isn't public |
history |
Read their past syncs, even if their past records aren't public. Includes records, since the newest sync is the latest records. |
playlists |
Read their playlists, private ones included |
offline.access |
Get a refresh token, to keep access past the two-hour token |
Ask only for what your app uses: the player sees the list before they agree. The player's friend code still follows their own setting.
An access token lasts 2 hours. Without offline.access, send the player through authorization again when it runs out.
With offline.access, the token response also has a refresh token. It expires after 90 days unused. Each use returns a new refresh token, and the old one stops working. If an old one is ever used again, maishift takes it as a leak and ends that sign-in: every token from it stops working, and the player has to authorize again on that device. Refresh one at a time, since two refreshes with the same token count as reuse. Each sign-in is separate, so a player signed in on two devices keeps the other one. A refresh can send scope to keep or narrow the granted scopes, never widen them. Narrowing away offline.access returns no new refresh token, so that sign-in ends when its access token does.
curl -X POST https://maimai.shiftpsh.com/api/oauth/token \
-H "X-API-Key: msk_…" \
-d grant_type=refresh_token \
-d refresh_token=msr_…
When the player signs out of your app, send either token to POST /api/oauth/revoke (token=…, with your API key). That ends the sign-in on that device; once no sign-in is left, your app is disconnected. Players can also disconnect your app in Settings › Connected apps, which ends every sign-in. Either way, the tokens stop working: API requests get 401 invalid_token, and a refresh gets invalid_grant. Revoking answers 200 whatever token you send, so it says nothing about which tokens exist.
/me returns a playerId. It stays the same for one player in your app and differs in every other app. Store it to recognize returning players rather than the handle, which players can change.
Songs with blurred decimal places in their difficulty constants are those without known constants in the current version. These values are estimates based on displayed difficulty levels and constants from previous versions, and may not be accurate. The constants are sourced from the maimai譜面定数ちほー and 新・CYCLES FUNの寝言 projects. Some unknown constants are calculated by us based on user data.
This site is a fan-made website created for tracking and recording personal performance in maimai DX. All game-related content used on the site is copyrighted by SEGA and their respective owners.