Example: Spotify login to display listening data

Overview

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.

Spotify app registration

  1. Create an app in the Spotify developer dashboard.
  2. Register 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.
  3. Store the credentials as SPOTIFY_OAUTH_CLIENT_ID and SPOTIFY_OAUTH_CLIENT_SECRET in your R environment. See the environment setup example for .Renviron syntax.
  4. Check your app’s development-mode access rules. Development apps require the owner to have Spotify Premium, and test users need to be on the app’s allowlist.

Shiny app example

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)

API requests and scopes

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.

Full dashboard example

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.