API Reference
BridgeBeats exposes a REST API for cross-provider music link conversion and lookup. This reference covers authentication, the lookup endpoints, rate limiting, the response shape, error codes, and the Open Graph card routes.
The routes and authentication below are documented from the current controllers
(MusicLookupController, AccountController, CardApiController, OpenGraphCardController in
src/BridgeBeats.Web/Controllers).
Authentication
The two URL lookup endpoints (music/lookup/url and music/lookup/urlList) are public and accept
anonymous requests. The identifier and name-search endpoints (music/lookup/isrc,
music/lookup/upc, music/lookup/title) require authentication.
MusicLookupController is decorated with [Authorize] for the API-key and internal-service schemes
at the class level; the two URL actions override that with [AllowAnonymous]. The scheme is chosen
per request by the header present (StartupExtensions.ConfigureApiKeyAuth):
| Header present | Scheme used |
|---|---|
X-Service-Key |
Internal service-to-service |
X-API-Key |
API key |
| neither | Identity cookie (web UI) |
To obtain an API key, register an account.
Creating an account
account/register issues a one-time API key and signs the new user in with a cookie. The endpoint
requires a valid anti-forgery token ([ValidateAntiForgeryToken]); fetch one from
account/antiforgery-token first and send it in the X-XSRF-TOKEN header.
POST /account/register
Content-Type: application/json
X-XSRF-TOKEN: <anti-forgery-token>
{
"email": "[email protected]",
"password": "SecurePassword123"
}
Response:
{
"userId": "...",
"apiKey": "YOUR_API_KEY_HERE",
"message": "Registration successful. Save your API key - it will not be shown again."
}
Save the API key immediately. Only its hash is stored, so the key cannot be retrieved later — it can only be regenerated.
Using your API key
Send the key in the X-API-Key header on the protected endpoints:
X-API-Key: YOUR_API_KEY_HERE
The protected endpoints also accept internal service-to-service authentication via the
X-Service-Key header (used by worker services, not by end users).
Logging in
account/login authenticates an existing account and signs it in. It also requires a valid
anti-forgery token. If the account has no API key yet, a new one is generated and returned; if it
already has one, the response returns the placeholder ***EXISTING_KEY*** rather than exposing the
stored key.
POST /account/login
Content-Type: application/json
X-XSRF-TOKEN: <anti-forgery-token>
{
"email": "[email protected]",
"password": "SecurePassword123"
}
Regenerating your API key
account/regenerate-api-key replaces the stored key and returns the new one. It requires an
authenticated session ([Authorize]) and a valid anti-forgery token. The previous key stops working
once the update succeeds.
POST /account/regenerate-api-key
X-XSRF-TOKEN: <anti-forgery-token>
Other account routes
| Route | Method | Auth | Purpose |
|---|---|---|---|
account/antiforgery-token |
GET | anonymous | Returns an anti-forgery request token and stores its companion cookie. |
account/status |
GET | anonymous | Reports whether the caller is authenticated, with user id and email when signed in. |
account/logout |
POST | anti-forgery token | Signs the current user out. |
Rate limiting
RateLimitingMiddleware enforces a per-user hourly quota on the identifier and name-search lookups.
- Limit: 20 requests per hour per authenticated user. Configurable via
RateLimitRequestsPerHour(src/BridgeBeats.Web/Configuration/AppSettings.cs; default20). - Applies to:
music/lookup/isrc,music/lookup/upc,music/lookup/title(POST only). - Exempt: the two URL lookup endpoints (
music/lookup/urlandmusic/lookup/urlList). They are anonymous and not counted.
The counter is tracked per user over a rolling one-hour window. When the quota is exceeded the request is rejected with HTTP 429 before reaching the controller:
HTTP/1.1 429 Too Many Requests
Retry-After: 2700
Content-Type: application/json
{
"error": "Rate limit exceeded",
"message": "Maximum 20 requests per hour allowed. Please try again in 45 minutes.",
"retryAfter": 2700
}
The Retry-After header and retryAfter field report the seconds remaining until the window resets.
Lookup endpoints
All five lookup endpoints live under the music/lookup route. The table is the authoritative list of
what exists now:
| Endpoint | Method | Auth | Rate-limited | Returns |
|---|---|---|---|---|
music/lookup/url |
POST | anonymous | no | Streamed sequence of MediaLinkResult |
music/lookup/urlList |
POST | anonymous | no | JSON array of MediaLinkResult |
music/lookup/isrc |
POST | API key or service key | yes | A single MediaLinkResult |
music/lookup/upc |
POST | API key or service key | yes | A single MediaLinkResult |
music/lookup/title |
POST | API key or service key | yes | A single MediaLinkResult |
There is no batch URL endpoint beyond urlList. The URL endpoints extract every https:// provider
link found in the request text, so a single request can carry several links; results are
deduplicated on external IDs (ISRC for tracks, UPC for albums).
Lookup by URL, streaming (public)
music/lookup/url returns results progressively. The action is declared as
IAsyncEnumerable<MediaLinkResult>, which ASP.NET Core serializes as a JSON array streamed to the
client as each result is produced. This is a streamed JSON array, not Server-Sent Events.
POST /music/lookup/url
Content-Type: application/json
{
"uri": "https://music.apple.com/us/album/..."
}
Lookup by URL, list (public)
music/lookup/urlList buffers all results and returns them as a single JSON array of
MediaLinkResult objects.
POST /music/lookup/urlList
Content-Type: application/json
{
"uri": "https://music.apple.com/us/album/..."
}
Lookup by ISRC (protected)
POST /music/lookup/isrc
Content-Type: application/json
X-API-Key: YOUR_API_KEY
{
"isrc": "USVI20000123"
}
When nothing matches, the endpoint returns 200 with a MediaLinkResult whose messages array holds
"No results found for ISRC.".
Lookup by UPC (protected)
POST /music/lookup/upc
Content-Type: application/json
X-API-Key: YOUR_API_KEY
{
"upc": "012345678901"
}
When nothing matches, the endpoint returns 200 with a MediaLinkResult whose messages array holds
"No results found for UPC.".
Lookup by title and artist (protected)
POST /music/lookup/title
Content-Type: application/json
X-API-Key: YOUR_API_KEY
{
"title": "Song Name",
"artist": "Artist Name"
}
When nothing matches, the endpoint returns 200 with a MediaLinkResult whose messages array holds
"No results found for title/artist.".
Response shape
The lookup endpoints serialize the MediaLinkResult type (src/BridgeBeats.Contracts/DTOs). The
single-result endpoints (isrc, upc, title) return one object; urlList returns an array of
them; url streams them.
MediaLinkResult has these public members:
| Field | Type | Notes |
|---|---|---|
results |
object (map) keyed by provider name | Each key is a SupportedProviders member name (AppleMusic, Spotify, Tidal); each value is the provider's result. Populated as each provider responds. |
messages |
array of strings or null |
Informational messages; null when there are none. |
lookedUpAt |
string (ISO 8601, UTC) | When the aggregate was looked up. |
isPartial |
boolean | true when some providers were rate-limited and have not yet resolved. |
rateLimitedProviders |
array of provider names or null |
The providers still rate-limited; null unless isPartial is true. |
The results keys are the SupportedProviders enum member names in PascalCase — AppleMusic,
Spotify, Tidal — not numeric values. System.Text.Json serializes enum dictionary keys as the
member name by default, and no value converter overrides that for this response. Match on these exact
key strings.
Each provider entry is a MusicLookupResult:
| Field | Type | Notes |
|---|---|---|
artist |
string | Primary artist name. |
title |
string | Track or album title. |
externalId |
string | ISRC for tracks, UPC for albums; empty string when the provider returns none. |
url |
string | The provider's web link. |
artUrl |
string | Cover artwork URL; empty when unavailable. |
marketRegion |
string | ISO 3166-1 alpha-2 region code; defaults to "us". Case is provider-dependent (for example, Apple Music and Spotify return "us" while Tidal returns "US"). |
isAlbum |
boolean or null | true for albums, false for tracks, null when not determined. |
Example response
A POST /music/lookup/urlList call returns a JSON array. Each element is a MediaLinkResult:
[
{
"results": {
"AppleMusic": {
"artist": "Rick Astley",
"title": "Never Gonna Give You Up",
"externalId": "GBARL9300135",
"url": "https://music.apple.com/us/album/never-gonna-give-you-up/1485596039?i=1485596041",
"artUrl": "https://is1-ssl.mzstatic.com/image/thumb/Music123/v4/17/3c/cb/173ccb70-6d48-24cc-3b5a-ff41560a0fda/4050538545074.jpg/4000x4000bb.jpg",
"marketRegion": "us",
"isAlbum": false
},
"Spotify": {
"artist": "Rick Astley",
"title": "Never Gonna Give You Up",
"externalId": "GBARL9300135",
"url": "https://open.spotify.com/track/4cOdK2wGLETKBW3PvgPWqT",
"artUrl": "https://i.scdn.co/image/ab67616d0000b273baf89eb11ec7c657805d2da0",
"marketRegion": "us",
"isAlbum": false
},
"Tidal": {
"artist": "Rick Astley",
"title": "Never Gonna Give You Up",
"externalId": "GBARL9300135",
"url": "https://tidal.com/browse/track/491303910",
"artUrl": "https://resources.tidal.com/images/cd490142/ab29/4a53/92b9/843b6895b3e4/1280x1280.jpg",
"marketRegion": "US",
"isAlbum": false
}
},
"messages": null,
"lookedUpAt": "2026-06-14T20:13:53.2582978Z",
"isPartial": false,
"rateLimitedProviders": null
}
]
The url endpoint streams these same objects as a JSON array. The isrc, upc, and title
endpoints return a single such object (not wrapped in an array). On a miss, that single object
carries a messages entry and an empty results map — see Lookup misses.
Persisted record shape differs from the API response
The live REST response above is not the same shape as the ATProto record BridgeBeats persists. The
stored link.bridgebeats.lookup record (MediaLinkResultRecord /
ProviderResultRecord in src/BridgeBeats.Contracts/Records, also covered in
CACHING.md) holds results as an array of rows, and each row carries a camelCase
provider string field (appleMusic, spotify, tidal). The REST response instead uses a
map keyed by the PascalCase provider name (AppleMusic, Spotify, Tidal) with no separate
provider field. Don't conflate the two: code that reads the API response keys on the map; code that
reads a stored record iterates the array and reads each row's provider string.
Supported providers
SupportedProviders (src/BridgeBeats.Contracts/Enums/SupportedProviders.cs):
- Apple Music
- Spotify
- Tidal
Error responses
400 Bad Request
Returned for invalid request payloads. Account registration and login return the ASP.NET Core
ModelState error collection when validation fails (for example, a duplicate email on register).
401 Unauthorized
Returned when a protected lookup endpoint is called without a valid API key or service key, and when
account/login is given an unknown email or wrong password. The login error body is:
{
"message": "Invalid email or password"
}
429 Too Many Requests
Returned by the rate-limit middleware. See Rate limiting for the body shape.
Lookup misses
The lookup endpoints do not return 404 for "not found." A miss returns 200 with a MediaLinkResult
carrying a message (for example "No results found for ISRC.") and an empty or partial results
map.
Open Graph card endpoints
BridgeBeats renders shareable Open Graph cards for media-link results.
Store a card
CardApiController (route api/card) is used by the Discord worker to persist a result and obtain a
card URL.
POST /api/card/store
Content-Type: application/json
{ /* a MediaLinkResult */ }
Responses:
200 OKwith{ "cardUrl": "..." }on success.503 Service Unavailablewith{ "cardUrl": null }when the card service is disabled.400 Bad Requestwith{ "cardUrl": null }when the result is null or has no provider results.
View a card
OpenGraphCardController (route card) renders the card pages.
| Route | Method | Returns |
|---|---|---|
card/{id} |
GET | The full card page; 404 Not Found when the card is unknown or expired. |
card/{id}/embed |
GET | The embeddable card view, no QR code. |
card/{id}/embed/qr |
GET | The embeddable card view with a QR code. |
When a card is not held in memory, the controller recovers it from the cache by card id and re-stores it.
The full card page emits Open Graph metadata (built by MediaLinkResult.ToOpenGraphMetadata) so the
card renders correctly when shared on platforms such as Discord, Slack, and others.
Card lifetime is governed by CardCacheExpirationHours (AppSettings; default 1 hour), not a
fixed 24 hours.
Code examples
The examples use the public urlList endpoint, which needs no API key.
Python
import requests
BASE_URL = "https://bridgebeats.example.com"
# Public endpoint — no API key required.
response = requests.post(
f"{BASE_URL}/music/lookup/urlList",
json={"uri": "https://open.spotify.com/track/..."}
)
if response.status_code == 200:
for result in response.json():
for provider, item in result["results"].items():
print(f"{provider}: {item['url']}")
JavaScript / Node.js
const axios = require('axios');
const BASE_URL = 'https://bridgebeats.example.com';
// Public endpoint — no API key required.
async function lookup(uri) {
try {
const response = await axios.post(`${BASE_URL}/music/lookup/urlList`, { uri });
return response.data; // array of MediaLinkResult
} catch (error) {
if (error.response?.status === 429) {
console.log(`Rate limited. Retry after ${error.response.headers['retry-after']} seconds`);
}
throw error;
}
}
cURL
# Public endpoint — no API key required.
curl -X POST https://bridgebeats.example.com/music/lookup/urlList \
-H "Content-Type: application/json" \
-d '{"uri": "https://open.spotify.com/track/..."}'
Practical notes
- Prefer ISRC and UPC over title/artist when you have them — they match more reliably across providers.
- Honor the
Retry-Afterheader on 429 responses. - Cache results on your side to avoid redundant calls.
- Check
isPartialon the response: whentrue, some providers were rate-limited and the result may be updated once they resolve.
Swagger
When the application is running, interactive API documentation is served at /swagger. The Swagger
UI is generated from the XML doc comments and lets you browse endpoints, view request and response
schemas, and download the OpenAPI specification. The Swagger document defines an X-API-Key security
scheme for the protected endpoints.