osu-api-v2-js is a JavaScript & TypeScript package that helps you interact with osu!api (v2).
The documentation for the latest version of this package can be found at any time on osu-v2.taevas.xyz!
Before installing, if using Node.js, check if you're running version 20 or above:
node -v # displays your version of node.js
Then to install the package, use a command from your package manager:
npm i osu-api-v2-js # if using npm
yarn add osu-api-v2-js # if using yarn
pnpm add osu-api-v2-js # if using pnpm
bun a osu-api-v2-js # if using bun
You will want to create your own OAuth application: https://osu.ppy.sh/home/account/edit#oauth
To use (import) the package in your project and start interacting with the API, you may do something like that:
// TypeScript
import * as osu from "osu-api-v2-js"
async function logUserTopPlayBeatmap(username: string) {
// It's more convenient to use `osu.API.createAsync()` instead of `new osu.API()` as it doesn't require you to directly provide an access_token!
// You may wish to make it so you can use your `api` object multiple times instead of creating multiple `api` objects
const api = await osu.API.createAsync("<client_id>", "<client_secret>") // id as a number, secret as a string
const user = await api.getUser(username) // We need to get the id of the user in order to request what we want
const scores = await api.getUserScores(user, "best", osu.Ruleset.osu, {lazer: false}, {limit: 1}) // Specifying the Ruleset is optional
const score = scores[0] // Array is already sorted
const beatmapDifficulty = await api.getBeatmapDifficultyAttributesOsu(score.beatmap, score.mods) // Specify the mods to get the appropriate SR
const x = `${score.beatmapset.artist} - ${score.beatmapset.title} [${score.beatmap.version}]`
const y = `+${score.mods.map((m) => m.acronym).toString()} (${beatmapDifficulty.star_rating.toFixed(2)}*)`
console.log(`${username}'s top play is on: ${x} ${y}`)
// Doomsday fanboy's top play is on: Erio o Kamattechan - os-Uchuujin(Asterisk Makina Remix) [Mattress Actress] +DT,CL (8.87*)
}
logUserTopPlayBeatmap("Doomsday fanboy")
Your api
object has many properties (listed as Accessors in the documentation) which you can modify in order to change its behaviour. There are two ways to do that, the first of which is to do it at any point after you've created your object:
const api = await osu.API.createAsync(0, "<client_secret>")
// Log all requests made by this api object
api.verbose = "all"
// Change the amount of seconds it takes for requests to timeout
api.timeout = 10
The other way would be if you're creating your api
object manually through new osu.API()
, like that:
// Same as above, in one line as the api object gets created
const api = new API({access_token: "my_token", client_id: 0, client_secret: "<client_secret>", verbose: "all", timeout: 10})
// PS: Specifying the `client_id` and `client_secret` here can make it more convenient to use features related to token refreshing!
An access_token
is required to access the API, and should be valid for 24 hours. When you first create your api
object through createAsync()
, that token is automatically set, so you don't have to worry about that! But how about after those 24 hours?
Once an access_token
has become invalid, the server will no longer respond correctly to requests made with it, instead responding with 401. Thankfully, there are solutions to get and set new access_token
s in a convenient way, so there is no need to create a new api
object every day!
access_token
, calling refreshToken()
will replace your previous token with a new onerefresh_token_on_expires
option to true (it's false by default)refresh_token_on_401
option is set to true, which (as its name indicates) will do that upon encountering a 401retry_on_automatic_token_refresh
is set to true as it is by default, it will retry the request it has encountered a 401 on, with the new token! (note that loops are prevented, it won't retry or refresh if the same request with the new token gets a 401)At any point in time, you can see when the current access_token
is set to expire through the expires
property of the API.
You may also choose to manually invalidate your access_token
through revokeToken()
instead of waiting for it to expire!
Your api
object has a configurable behaviour when it comes to handling requests that have failed for certain reasons. It may "retry" a request, meaning not throwing and making the request again as if it hasn't failed under the following circumstances:
NOTE: retry_maximum_amount
must be above 0 for any retry to happen in the first place!
refresh_token_on_401
is set, the refresh is successful, and retry_on_automatic_token_refresh
is also setretry_on_status_codes
retry_on_timeout
is set and a timeout has happenedYou can further configure retries through retry_delay
and the aforementioned retry_maximum_amount
.
A simple guide on how to do extra fancy stuff
If your application is meant to act on behalf of a user after they've clicked on a button to say they "consent to your application identifying them and reading public data on their behalf and some other stuff maybe", then you will need to take a slightly different approach to create your api
object.
Let's take it step by step! First, this package comes with generateAuthorizationURL()
, which will create a link for you that you can share so users can click on it and allow your application to do stuff on their behalf.
This function requires you to specify scopes. Here are some useful things to know about scopes:
identify
is always implicitly SPECIFIED, so you should always use itpublic
is always implicitely REQUIRED, so you should (basically) always use itscopes
property of your api
objectThe TLDR of scopes: Use identify
and public
, add other scopes on top of that when necessary!
The user clicked your link and authorized your application! ...Now what?
When a user authorizes your application, they get redirected to your Application Callback URL
with a huge code as a GET parameter (the name of the parameter is code
), and it is this very code that will allow you to proceed with the authorization flow! So make sure that somehow, you retrieve this code!
With this code, thanks to the createAsync()
method, you're able to create your api
object:
const api = await osu.API.createAsync("<client_id>", "<client_secret>", {code: "<code>", redirect_uri: "<application_callback_url>"})
Congrats on making your api
object! As you may have seen from glancing at the documentation, it is pretty important as it holds a lot of information, such as the refresh_token
which is needed to get a new access_token
without asking the user for their consent again.
So as a reminder, it has:
access_token
and the refresh_token
, as well as the expiry dateuser
propertyEverything is available to read about in the documentation!
Here's a full example where you will launch a server that will:
// TypeScript
import * as osu from "osu-api-v2-js"
import * as http from "http"
import { exec } from "child_process"
// This should be from an application registered on https://osu.ppy.sh/home/account/edit#oauth
const id = 0 // replace with your client id
const secret = "<client_secret>"
const redirect_uri = "<application_callback_url>" // assuming localhost with any unused port for convenience (like http://localhost:7272/)
// Because we need to act as an authenticated user, we need to go through the authorization procedure
// This function largely takes care of it by itself
async function getCode(authorization_url: string): Promise<string> {
// Open a temporary server to receive the code when the browser is sent to the redirect_uri after confirming authorization
const httpserver = http.createServer()
const host = redirect_uri.substring(redirect_uri.indexOf("/") + 2, redirect_uri.lastIndexOf(":"))
const port = Number(redirect_uri.substring(redirect_uri.lastIndexOf(":") + 1).split("/")[0])
httpserver.listen({host, port})
// Open the browser to the page on osu!web where you click a button to say you authorize your application
console.log("Waiting for code...")
const command = (process.platform == "darwin" ? "open" : process.platform == "win32" ? "start" : "xdg-open")
exec(`${command} "${authorization_url}"`)
// Check the URL for a `code` GET parameter, get it if it's there
const code: string = await new Promise((resolve) => {
httpserver.on("request", (request, response) => {
if (request.url) {
console.log("Received code!")
response.end("Worked! You may now close this tab.", "utf-8")
httpserver.close() // Close the temporary server as it is no longer needed
resolve(request.url.substring(request.url.indexOf("code=") + 5))
}
})
})
return code
}
async function getSelf() {
// Get the code needed for the api object
const url = osu.generateAuthorizationURL(id, redirect_uri, ["public", "identify"])
const code = await getCode(url)
const api = await osu.API.createAsync(id, secret, {code, redirect_uri}, {verbose: "all"})
// Use the `me` endpoint, which gives information about the authorized user!
const me = await api.getResourceOwner()
console.log("My id is", me.id, "but I'm better known as", me.username)
// If you're not gonna use the token anymore, might as well revoke it for the sake of security
await api.revokeToken().then(() => console.log("Revoked the token, it can no longer be used!"))
}
getSelf()
If you're looking for an example that involves WebSockets, you might wanna take a look at lib/tests/websocket.ts
in the package's repository!
This package's functions can be accessed both through the api object and through namespaces! It essentially means that for convenience's sake, there are two ways to do anything:
// Obtaining a match, assuming an `api` object already exists and everything from the package is imported as `osu`
const match_1 = await api.getMatch(103845156) // through the api object
const match_2 = await osu.Match.getOne.call(api, 103845156) // through the namespaces
// `match_1` and `match_2` are the same, because they're essentially using the same function!
// The same, but for obtaining multiple lazer updates
const builds_1 = await api.getChangelogBuilds("lazer")
const builds_2 = await osu.Changelog.Build.getMultiple.call(api, "lazer")
// `build_1` and `build_2` are also the same!
As you may have noticed, when calling the functions through the namespaces, instead of doing something like getOne()
, we instead do getOne.call()
and use the call() method in order to provide a this
value; the api object!
Of course, using the apply() method would also work, so just do things the way you prefer or the way that is more intuitive to you!
In the same order as on the API's official documentation:
GET /me/beatmapset-favourites
-> getFavouriteBeatmapsetsIds()GET /beatmaps/packs
-> getBeatmapPacks()GET /beatmaps/packs/{pack}
-> getBeatmapPack()GET /beatmaps/lookup
-> lookupBeatmap()GET /beatmaps/{beatmap}/scores/users/{user}
-> getBeatmapUserScore()GET /beatmaps/{beatmap}/scores/users/{user}/all
-> getBeatmapUserScores()GET /beatmaps/{beatmap}/scores
-> getBeatmapScores()GET /beatmaps
-> getBeatmaps()GET /beatmaps/{beatmap}
-> getBeatmap()POST /beatmaps/{beatmap}/attributes
-> getBeatmapDifficultyAttributes()GET /beatmapsets/discussions/posts
-> getBeatmapsetDiscussionPosts()GET /beatmapsets/discussions/votes
-> getBeatmapsetDiscussionVotes()GET /beatmapsets/discussions
-> getBeatmapsetDiscussions()GET /beatmapsets/search
-> searchBeatmapset()GET /beatmapsets/lookup
-> lookupBeatmapset()GET /beatmapsets/{beatmapset}
-> getBeatmapset()GET /changelog/{stream}/{build}
-> getChangelogBuild()GET /changelog
-> getChangelogBuilds() and getChangelogStreams() (removing search
, putting builds
behind getChangelogBuilds(), and streams
behind getChangelogStreams())GET /changelog/{changelog}
-> lookupChangelogBuild()POST /chat/ack
-> keepChatAlive()POST /chat/new
-> sendChatPrivateMessage()GET /chat/channels/{channel}/messages
-> getChatMessages()POST /chat/channels/{channel}/messages
-> sendChatMessage()PUT /chat/channels/{channel}/users/{user}
-> joinChatChannel()DELETE /chat/channels/{channel}/users/{user}
-> leaveChatChannel()PUT /chat/channels/{channel}/mark-as-read/{message}
-> markChatChannelAsRead()GET /chat/channels
-> getChatChannels()POST /chat/channels
-> createChatPrivateChannel() and createChatAnnouncementChannel()GET /chat/channels/{channel}
-> getChatChannel() (without users
because channel
would already have this property)GET /comments
-> getComments()GET /comments/{comment}
-> getComment()GET /events
-> getEvents()POST /forums/topics/{topic}/reply
-> replyForumTopic()GET /forums/topics
-> getForumTopics()POST /forums/topics
-> createForumTopic()GET /forums/topics/{topic}
-> getForumTopic() (removing search
for simplicity)PUT /forums/topics/{topic}
-> editForumTopicTitle()PUT /forums/posts/{post}
-> editForumPost()GET /forums
-> getForums()GET /forums/{forum}
-> getForum()GET /search
-> searchUser() and searchWiki()GET /matches
-> getMatches()GET /matches/{match}
-> getMatch()GET /rooms/{room}/playlist/{playlist}/scores
-> getPlaylistItemScores()GET /rooms
-> getRooms()GET /news
-> getNewsPosts() (removing everything except news_sidebar.news_posts
)GET /news/{news}
-> getNewsPost()GET /rankings/kudosu
-> getKudosuRanking()GET /rankings/{mode}/{type}
-> getUserRanking() and getCountryRanking() and getSpotlightRanking()GET /spotlights
-> getSpotlights()GET /scores
-> getScores()GET /beatmapsets/events
-> getBeatmapsetEvents()GET /seasonal-backgrounds
-> getSeasonalBackgrounds()GET /rooms/{room}
-> getRoom()GET /rooms/{room}/leaderboard
-> getRoomLeaderboard()GET /rooms/{room}/events
-> getRoomEvents()GET /scores/{score}/download
-> getReplay()GET /scores/{rulesetOrScore}/{score?}
-> getScore()GET /users/lookup
-> lookupUsers()GET /friends
-> getFriends()GET /tags
-> getBeatmapUserTags()GET /users/{user}/kudosu
-> getUserKudosuHistory()GET /users/{user}/scores/{type}
-> getUserScores()GET /users/{user}/beatmapsets/{type}
-> getUserBeatmaps() and getUserMostPlayed()GET /users/{user}/recent_activity
-> getUserRecentActivity()GET /users/{user}/beatmaps-passed
-> getUserPassedBeatmaps()GET /users/{user}/{mode?}
-> getUser()GET /users
-> getUsers()GET /me/{mode?}
-> getResourceOwner()GET /wiki/{locale}/{path}
-> getWikiPage()