GET /getLyrics
GET /getLyrics
Fetch synced lyrics for a song, with syllable-level timing in TTML format.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
s, song, songName | string | Always send | Song name. |
a, artist, artistName | string | Always send | Artist name. |
al, album, albumName | string | Always send | Album name. Improves matching. |
d, duration | string of digits | Always send | Track duration in whole seconds. Improves matching. |
videoId, v | string | No | Video ID. Links the video to the song so downstream caches can be revalidated. |
The API rejects a request (422) only when both song and artist are missing. Send all four
anyway: the cache key includes album and duration, so a request without them often misses the cached
entry and, without an API key, returns 401.
If you send more than one alias of a parameter (for example s and song),
the values are joined together. Send only one alias of each.
Headers
| Name | Required | Description |
|---|---|---|
X-API-Key | Conditional | Needed for cache misses when the server enforces API keys. A valid key also skips rate limiting. |
Example request
curl "https://api.betterlyrics.org/getLyrics?s=E85&a=Don%20Toliver&al=OCTANE&d=154"const res = await fetch("https://api.betterlyrics.org/getLyrics?s=E85&a=Don%20Toliver&al=OCTANE&d=154");
const { ttml } = await res.json();import requests
res = requests.get("https://api.betterlyrics.org/getLyrics?s=E85&a=Don%20Toliver&al=OCTANE&d=154")
ttml = res.json()["ttml"]package main
import (
"fmt"
"io"
"net/http"
)
func main() {
res, err := http.Get("https://api.betterlyrics.org/getLyrics?s=E85&a=Don%20Toliver&al=OCTANE&d=154")
if err != nil {
panic(err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(string(body))
} Success response
Status: 200 OK, Content-Type: application/json
{
"ttml": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<tt xmlns=\"http://www.w3.org/ns/ttml\">\n <body>\n <div>\n <p begin=\"00:00:18.234\" end=\"00:00:21.891\">\n <span begin=\"00:00:18.234\" end=\"00:00:18.567\">The </span>\n ...\n </p>\n </div>\n </body>\n</tt>",
"score": 0.97
} Response fields
| Field | Type | Always present | Description |
|---|---|---|---|
ttml | string | Yes | TTML document with syllable or line timing. |
score | number | No | Match confidence. Present only on fresh fetches. |
Response headers
| Header | Description |
|---|---|
X-Cache-Status | HIT cached lyrics. MISS fetched from upstream (or nothing found). STALE upstream failed and an older entry was served. NEGATIVE_HIT a cached "no lyrics" result. |
X-RateLimit-Type | normal full service. cached normal tier exhausted, only cached lyrics are served. exceeded both tiers exhausted. bypass valid API key. |
X-RateLimit-Limit | Burst size of the current rate limit tier. Absent with a valid API key. |
X-RateLimit-Remaining | Requests left in the current tier. Absent with a valid API key. |
X-RateLimit-Bypass | true when a valid API key skipped rate limiting. |
X-Auth-Mode | Set only when the server enforces API keys on this path. authenticated valid key, invalid wrong key, cache no key (cache hits only). |
Retry-After | Seconds to wait before retrying. |
/getLyrics does not send X-Provider. See
Provider-specific endpoints for the endpoints that do.
Error responses
All error responses are JSON, except 422 and the 429 sent when both rate limit tiers are exhausted, which are plain text.
Missing parameters (422)
Sent when neither song nor artist is given. Content-Type: text/plain:
Song name or artist name not provided API key required (401)
The query is not cached, the server enforces API keys, and the request has no key:
{
"error": "API key required",
"message": "Uncached queries require a valid API key via X-API-Key header"
} With a wrong key:
{
"error": "Invalid API key",
"message": "The provided API key is not valid"
} Not found (404)
{
"error": "Lyrics not available for this track"
}
The error text depends on why no lyrics were found. This result is cached, and later
requests for the same query get X-Cache-Status: NEGATIVE_HIT.
Rate limited (429)
Both rate limit tiers are exhausted. Content-Type: text/plain,
Retry-After: 1, X-RateLimit-Type: exceeded:
Too Many Requests
The normal tier is exhausted and the query is not cached, so the cached tier cannot serve it.
JSON, Retry-After: 60, X-RateLimit-Type: cached:
{
"error": "Rate limit exceeded. This request requires cached data, but no cache is available for this query.",
"message": "Please try again later or reduce your request rate."
} Upstream error (500)
The upstream fetch failed and no cached fallback exists. The error text is the upstream error:
{
"error": "<upstream error message>"
} Cache-only mode (503)
The server is temporarily serving only cached lyrics and the query is not cached:
{
"error": "Service running in cache-only mode. No cached lyrics available for this query."
} Provider-specific endpoints
These endpoints select one provider. They take the same s, a, al
and d parameters (but not videoId), and return the same 422 and
rate limit responses.
| Endpoint | Format | Timing | API key for cache misses |
|---|---|---|---|
/ttml/getLyrics | TTML | Syllable-level | When the server enforces API keys |
/kugou/getLyrics | LRC-style text | Line-level | Never |
/qq/getLyrics | QRC | Word-level | Never |
{
"lyrics": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>...",
"provider": "ttml"
} They differ from /getLyrics in these ways:
- The body is
lyricsandprovider, notttml. There is noscore. - Every JSON response, errors included, has an
X-Providerheader with the provider name. JSON error bodies also have aproviderfield. - Cache lookups use the exact cache key. There is no duration tolerance and no
STALEfallback. -
/ttml/getLyricsbuilds its cache keys in a different format from/getLyrics. When you send an album or a duration, the two endpoints do not share cache entries. X-Auth-Modeis sent only by/ttml/getLyrics, and only when the server enforces API keys.