This example lets a user connect their Spotify account and displays their top tracks. It uses the same provider, client, and module setup as Usage, then calls Spotify’s API with the access token. Spotify supplies profile data through its API rather than an OIDC ID token.
The helper identifies accounts using immutable
account_id, as specified in Spotify’s
May 2026 changes. Before upgrading an existing app, migrate stored
id mappings and audit digests using both identifiers from
an authenticated profile. Do not merge accounts by email or display
name. allow_legacy_id = TRUE permits a temporary fallback
only when account_id is missing; it does not preserve old
mappings when both exist.
http://127.0.0.1:8100 as its redirect URI.
Spotify accepts a loopback IP address for local HTTP development, but
not localhost. See Spotify’s
redirect URI rules.SPOTIFY_OAUTH_CLIENT_ID and
SPOTIFY_OAUTH_CLIENT_SECRET in your R environment. See the
environment setup
example for .Renviron syntax.Save this as app.R, run it, and open
http://127.0.0.1:8100 in a regular browser. After
connecting Spotify, click Load top tracks.
library(shiny)
library(shinyOAuth)
client <- oauth_client(
provider = oauth_provider_spotify(),
client_id = Sys.getenv("SPOTIFY_OAUTH_CLIENT_ID"),
client_secret = Sys.getenv("SPOTIFY_OAUTH_CLIENT_SECRET"),
redirect_uri = "http://127.0.0.1:8100",
scopes = c("user-read-private", "user-top-read")
)
ui <- oauth_ui(fluidPage(
h2("My top tracks"),
actionButton("load", "Load top tracks"),
tableOutput("tracks")
), id = "auth", client = client)
server <- function(input, output, session) {
auth <- oauth_module_server("auth", client)
tracks <- eventReactive(input[["load"]], {
req(auth[["authenticated"]])
tryCatch({
response <- perform_resource_req(
auth[["token"]],
"https://api.spotify.com/v1/me/top/tracks",
query = list(limit = 10, time_range = "short_term")
)
httr2::resp_check_status(response)
body <- httr2::resp_body_json(response, simplifyVector = FALSE)
vapply(body[["items"]], function(track) track[["name"]], character(1))
}, error = function(e) NULL)
})
output[["tracks"]] <- renderTable({
req(auth[["authenticated"]])
values <- tracks()
validate(need(!is.null(values), "Could not load tracks. Try again later."))
validate(need(length(values) > 0, "No listening data to show yet."))
data.frame(Track = values)
})
}
runApp(shinyApp(ui, server), port = 8100, launch.browser = FALSE)user-top-read grants permission to read listening
preferences. The request above asks for up to ten tracks from the
short-term range. The module obtains and stores the access token;
perform_resource_req() uses it to call the API.
eventReactive() loads the data when the button is clicked,
and renderTable() displays the result.
For other data, choose the API URL and requested scopes together. A successful login does not mean every API is available to the app. Empty results can be normal for accounts without listening history; API errors can indicate missing permissions, app access restrictions, or request limits.
A larger example is installed with the package. It adds profile information, currently playing music, top artists, and recent tracks, with a bslib layout. It uses the same authentication setup as the example above.
install.packages(c("bslib", "ggplot2", "DT", "purrr", "dplyr"))
dashboard_file <- system.file(
"examples", "spotify-dashboard.R", package = "shinyOAuth", mustWork = TRUE
)
file.show(dashboard_file) # Read or copy the complete source.
source(dashboard_file) # Run after setting your Spotify credentials.The source groups the code into client setup, API helpers, UI, and server logic. It validates Spotify links and image URLs before rendering them and keeps table escaping enabled. Preserve those protections when adapting it; profile and track text can contain characters that would otherwise become HTML. Available data still depends on Spotify’s API access rules for your app.