The Lyrics Plus API provides access to synchronized song lyrics with timestamp data from multiple sources including Apple Music, Spotify, Musixmatch, and LyricsPlus.
The API is available on multiple servers for redundancy and performance:
https://lyricsplus.prjktla.workers.dev(Primary - Cloudflare Workers)https://lyrics-plus-backend.vercel.app(Vercel)
Recommended: Use the primary URL or implement fallback logic to try the alternative server if one is unavailable.
Retrieves synchronized lyrics for a specified song with detailed timing information.
Endpoint: GET /v2/lyrics/get
| Parameter | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | The song title (URL encoded) |
artist |
string | Yes | The artist name(s) (URL encoded, comma-separated for multiple artists) |
album |
string | No | The album name (URL encoded) |
duration |
number | No | Song duration in seconds (helps with matching accuracy) |
source |
string | No | Comma-separated list of sources to query. Options: apple, lyricsplus, musixmatch, spotify, musixmatch-word |
GET /v2/lyrics/get?title=NOT%20CUTE%20ANYMORE&artist=ILLIT&album=NOT%20CUTE%20ANYMORE&duration=132&source=apple,lyricsplus,musixmatch,spotify,musixmatch-wordGET /v2/lyrics/get
?title=NOT CUTE ANYMORE
&artist=ILLIT
&album=NOT CUTE ANYMORE
&duration=132
&source=apple,lyricsplus,musixmatch,spotify,musixmatch-word
interface LyricsResponse {
KpoeTools: string;
type: "Word" | "Line";
metadata: {
source: string;
songWriters: string[];
title: string;
language: string;
};
agents: {
[key: string]: {
type: "person";
name: string;
alias: string;
};
};
totalDuration: string;
lyrics: LyricLine[];
}
interface LyricLine {
time: number;
duration: number;
text: string;
syllabus: SyllabusItem[];
element: {
key: string;
songPart: string;
singer: string;
};
transliteration?: {
lang: string;
text: string;
syllabus: SyllabusItem[];
};
}
interface SyllabusItem {
time: number;
duration: number;
text: string;
isBackground?: boolean;
}{
"KpoeTools": "1.6-ConvertTTMLtoJSON-DOMParser",
"type": "Word | Line",
"metadata": {
"source": "Apple",
"songWriters": ["string"],
"title": "string",
"language": "string"
},
"agents": {
"v1": {
"type": "person",
"name": "string",
"alias": "string"
}
},
"totalDuration": "string",
"lyrics": [
{
"time": number,
"duration": number,
"text": "string",
"syllabus": [
{
"time": number,
"duration": number,
"text": "string",
"isBackground": boolean
}
],
"element": {
"key": "string",
"songPart": "string",
"singer": "string"
},
"transliteration": {
"lang": "string",
"text": "string",
"syllabus": [
{
"time": number,
"duration": number,
"text": "string"
}
]
}
}
]
}| Field | Type | Description |
|---|---|---|
KpoeTools |
string | Version identifier for the conversion tool |
type |
string | Synchronization type: "Word" for word-level timing, "Line" for line-level timing |
metadata |
object | Song metadata information |
agents |
object | Information about performers/singers |
totalDuration |
string | Total song duration in MM:SS.mmm format |
lyrics |
array | Array of lyric line objects with timing data |
| Field | Type | Description |
|---|---|---|
source |
string | Data source (e.g., "Apple") |
songWriters |
array | List of song writers/composers |
title |
string | Song title |
language |
string | Language code (e.g., "en" for English) |
Each agent is keyed by an identifier (e.g., "v1", "v2") and contains:
| Field | Type | Description |
|---|---|---|
type |
string | Agent type (typically "person") |
name |
string | Performer name |
alias |
string | Agent identifier used in lyrics |
| Field | Type | Description |
|---|---|---|
time |
number | Start time in milliseconds |
duration |
number | Duration in milliseconds |
text |
string | Full line of lyrics |
syllabus |
array | Word/syllable-level timing breakdown |
element |
object | Metadata about the lyric line |
| Field | Type | Description |
|---|---|---|
time |
number | Start time in milliseconds |
duration |
number | Duration in milliseconds |
text |
string | Individual word or syllable |
isBackground |
boolean | (Optional) If true, indicates background vocals or ad-libs |
| Field | Type | Description |
|---|---|---|
key |
string | Unique line identifier (e.g., "L1", "L2") |
songPart |
string | Song section (e.g., "Chorus", "Verse", "Bridge", "PreChorus") |
singer |
string | Agent identifier of the singer (e.g., "v1") |
For non-Latin script songs (e.g., Korean, Japanese), this provides romanized text:
| Field | Type | Description |
|---|---|---|
lang |
string | Language code for transliteration (e.g., "ko-Latn" for Korean to Latin) |
text |
string | Romanized version of the full line |
syllabus |
array | Word/syllable-level romanized timing breakdown |
The API returns different levels of timing precision based on availability:
| Feature | Word-Level (type: "Word") |
Line-Level (type: "Line") |
|---|---|---|
| Precision | Word/syllable timing | Line timing only |
syllabus array |
Contains word data | Empty [] |
isBackground field |
Available in syllabus items | Not available |
| Best for | Karaoke, real-time highlighting | Basic lyric display |
| File size | Larger | Smaller |
Provides precise timing for each word/syllable in the syllabus array. Best for karaoke and real-time highlighting applications.
Characteristics:
typefield is"Word"syllabusarray contains detailed word/syllable timing- Each syllabus item includes
time,duration, andtext - May include
isBackground: truefor backing vocals
Example:
{
"time": 12856,
"duration": 2692,
"text": "Tell 'em Kendrick did it, aye, who showed you how to run a blitz?",
"syllabus": [
{ "time": 12856, "duration": 116, "text": "Tell " },
{ "time": 12972, "duration": 180, "text": "'em " },
{ "time": 13152, "duration": 384, "text": "Kendrick " }
]
}Provides timing only for complete lines. The syllabus array will be empty.
Characteristics:
typefield is"Line"syllabusarray is empty[]- Only line-level timing available
- Simpler structure for basic lyric display
Example:
{
"time": 590,
"duration": 3219,
"text": "These red roses damn near turn to ashes",
"syllabus": []
}{
"KpoeTools": "1.6-ConvertTTMLtoJSON-DOMParser",
"type": "Word",
"metadata": {
"source": "Apple",
"songWriters": [
"Kendrick Lamar",
"Jack Antonoff",
"Hitta J3"
],
"title": "gnx",
"language": "en"
},
"agents": {
"v1": {
"type": "person",
"name": "",
"alias": "v1"
}
},
"totalDuration": "3:13.538",
"lyrics": [
{
"time": 12856,
"duration": 2692,
"text": "Tell 'em Kendrick did it, aye, who showed you how to run a blitz?",
"syllabus": [
{
"time": 12856,
"duration": 116,
"text": "Tell "
},
{
"time": 12972,
"duration": 180,
"text": "'em "
},
{
"time": 13152,
"duration": 384,
"text": "Kendrick "
}
],
"element": {
"key": "L1",
"songPart": "Chorus",
"singer": "v1"
}
},
{
"time": 9798,
"duration": 7698,
"text": "Never fear the fall (Phoenix)",
"syllabus": [
{
"time": 9798,
"duration": 464,
"text": "Never "
},
{
"time": 10262,
"duration": 848,
"text": "fear the "
},
{
"time": 11110,
"duration": 986,
"text": "fall "
},
{
"time": 14497,
"duration": 237,
"text": "(Phoe",
"isBackground": true
},
{
"time": 14734,
"duration": 2762,
"text": "nix)",
"isBackground": true
}
],
"element": {
"key": "L3",
"songPart": "Chorus",
"singer": "v1"
}
}
]
}{
"KpoeTools": "1.6-ConvertTTMLtoJSON-DOMParser",
"type": "Word",
"metadata": {
"source": "Apple",
"songWriters": ["C'SA", "Danke", "ENDS"],
"title": "Phoenix",
"language": "ko"
},
"agents": {
"v1": {
"type": "person",
"name": "",
"alias": "v1"
}
},
"totalDuration": "2:44.000",
"lyrics": [
{
"time": 28728,
"duration": 2915,
"text": "한 줌 흙 속에 잠겨",
"syllabus": [
{
"time": 28728,
"duration": 363,
"text": "한 "
},
{
"time": 29091,
"duration": 424,
"text": "줌 "
},
{
"time": 30171,
"duration": 595,
"text": "흙 속에 "
},
{
"time": 30766,
"duration": 877,
"text": "잠겨"
}
],
"element": {
"key": "L7",
"songPart": "Verse",
"singer": "v1"
},
"transliteration": {
"lang": "ko-Latn",
"text": "han jum heuk so ge jam gyeo",
"syllabus": [
{
"time": 28728,
"duration": 363,
"text": "han "
},
{
"time": 29091,
"duration": 424,
"text": "jum "
},
{
"time": 30171,
"duration": 595,
"text": "heuk so ge "
},
{
"time": 30766,
"duration": 877,
"text": "jam gyeo"
}
]
}
}
]
}{
"KpoeTools": "1.6-ConvertTTMLtoJSON-DOMParser",
"type": "Line",
"metadata": {
"source": "Apple",
"songWriters": ["Miguel Jontel Pimentel", "Jhené Aiko"],
"title": "",
"language": "en"
},
"agents": {},
"totalDuration": "3:06.017",
"lyrics": [
{
"time": 590,
"duration": 3219,
"text": "These red roses damn near turn to ashes",
"syllabus": [],
"element": {
"key": "L39608",
"songPart": "Verse",
"singer": ""
}
},
{
"time": 3950,
"duration": 3299,
"text": "If I keep it real, you won't understand it",
"syllabus": [],
"element": {
"key": "L39609",
"songPart": "Verse",
"singer": ""
}
}
]
}async function getLyrics(
title: string,
artist: string,
options?: {
album?: string;
duration?: number;
sources?: string[];
}
): Promise<LyricsResponse> {
const params = new URLSearchParams({
title,
artist,
});
if (options?.album) params.append("album", options.album);
if (options?.duration) params.append("duration", options.duration.toString());
if (options?.sources) params.append("source", options.sources.join(","));
const response = await fetch(
`https://lyricsplus.prjktla.workers.dev/v2/lyrics/get?${params}`
);
if (!response.ok) {
throw new Error(`Failed to fetch lyrics: ${response.statusText}`);
}
return response.json();
}
// Example usage
const lyrics = await getLyrics("NOT CUTE ANYMORE", "ILLIT", {
album: "NOT CUTE ANYMORE",
duration: 132,
sources: ["apple", "lyricsplus", "musixmatch", "spotify"],
});
console.log(lyrics.type); // "Word" or "Line"
console.log(lyrics.totalDuration); // "2:12.000"function displayLyrics(lyrics, currentTime) {
// Find the current line based on playback time
const currentLine = lyrics.lyrics.find((line) => {
const endTime = line.time + line.duration;
return currentTime >= line.time && currentTime < endTime;
});
if (!currentLine) return;
// For word-level sync, highlight individual words
if (lyrics.type === "Word" && currentLine.syllabus.length > 0) {
currentLine.syllabus.forEach((word) => {
const wordEndTime = word.time + word.duration;
const isActive = currentTime >= word.time && currentTime < wordEndTime;
const isBackground = word.isBackground === true;
// Apply different styling for active words and background vocals
console.log({
text: word.text,
isActive,
isBackground,
});
});
} else {
// For line-level sync, just display the line
console.log(currentLine.text);
}
}function getDisplayText(line, useTransliteration = false) {
if (useTransliteration && line.transliteration) {
return line.transliteration.text;
}
return line.text;
}
function getCurrentWord(line, currentTime, useTransliteration = false) {
const syllabus = useTransliteration && line.transliteration
? line.transliteration.syllabus
: line.syllabus;
return syllabus.find((word) => {
const endTime = word.time + word.duration;
return currentTime >= word.time && currentTime < endTime;
});
}import requests
from typing import Optional, List, Dict, Any
from urllib.parse import urlencode
def get_lyrics(
title: str,
artist: str,
album: Optional[str] = None,
duration: Optional[int] = None,
sources: Optional[List[str]] = None
) -> Dict[str, Any]:
params = {
"title": title,
"artist": artist
}
if album:
params["album"] = album
if duration:
params["duration"] = str(duration)
if sources:
params["source"] = ",".join(sources)
url = f"https://lyricsplus.prjktla.workers.dev/v2/lyrics/get?{urlencode(params)}"
response = requests.get(url)
response.raise_for_status()
return response.json()
# Example usage
lyrics = get_lyrics(
title="NOT CUTE ANYMORE",
artist="ILLIT",
album="NOT CUTE ANYMORE",
duration=132,
sources=["apple", "lyricsplus", "musixmatch", "spotify"]
)
print(f"Type: {lyrics['type']}") # "Word" or "Line"
print(f"Duration: {lyrics['totalDuration']}")async function getLyricsWithFallback(title, artist, options) {
const servers = [
"https://lyricsplus.prjktla.workers.dev",
"https://lyrics-plus-backend.vercel.app",
];
for (const server of servers) {
try {
const params = new URLSearchParams({ title, artist });
if (options?.album) params.append("album", options.album);
if (options?.duration) params.append("duration", options.duration.toString());
if (options?.sources) params.append("source", options.sources.join(","));
const response = await fetch(`${server}/v2/lyrics/get?${params}`);
if (response.ok) {
return await response.json();
}
} catch (error) {
console.warn(`Failed to fetch from ${server}, trying next...`);
continue;
}
}
throw new Error("All servers failed to respond");
}All timing values are provided in milliseconds from the start of the song.
- Line timing:
timeanddurationfields in the lyrics array - Word timing:
timeanddurationfields in the syllabus array - Total duration: Formatted as
MM:SS.mmm(minutes:seconds.milliseconds)
// Milliseconds to seconds
const seconds = milliseconds / 1000;
// Milliseconds to MM:SS.mmm format
function formatTime(ms) {
const minutes = Math.floor(ms / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
const milliseconds = ms % 1000;
return `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds}`;
}- Word-level sync: Use the word-level timing data in the
syllabusarray to highlight lyrics in real-time as the song plays - Background vocals: Filter out or style differently lyrics with
isBackground: truefor cleaner karaoke display
- Word-level: Display line-by-line lyrics synchronized with music playback using the
timeanddurationfields - Line-level: Show complete lines when word-level precision isn't needed
- Transliteration: Display romanized text alongside original lyrics for non-Latin script songs (Korean, Japanese, etc.)
- Use the
transliteration.textandtransliteration.syllabusfor karaoke in romanized form
- Analyze song structure using the
songPartfield to identify verses, choruses, bridges, and other sections - Track timing patterns and song composition
- Track which artist is singing at any given time using the
singerfield and cross-referencing with theagentsobject - Useful for duets or songs with multiple featured artists
The API may return errors in the following scenarios:
- Missing required parameters: Title or artist not provided
- No lyrics found: No matching lyrics in any of the specified sources
- Invalid source: Specified source is not supported
Common HTTP status codes:
200 OK: Successful request400 Bad Request: Missing or invalid parameters404 Not Found: No lyrics found for the specified song500 Internal Server Error: Server-side error
Please check with the API provider for specific rate limiting policies.
- URL encode all query parameters
- Multiple artists should be comma-separated
- The API returns either word-level (
type: "Word") or line-level (type: "Line") synchronization - For word-level sync, the
syllabusarray provides word/syllable-level granularity - For line-level sync, the
syllabusarray will be empty[] - Background vocals are marked with
isBackground: truein the syllabus array - Non-Latin script songs (Korean, Japanese, etc.) include a
transliterationobject with romanized text - Censored words may appear as "****" in the lyrics
- Some agent names may be empty strings if not provided by the source
- The
durationparameter helps improve matching accuracy when searching for songs - Server availability: If one server is unavailable, try the alternative base URL
- Best practice: Implement automatic failover logic to switch between servers if needed
- Check response type: Look at
response.typeto determine if you have"Word"or"Line"sync - Word-level sync: If
type === "Word", iterate throughline.syllabusfor word-by-word timing - Line-level sync: If
type === "Line",line.syllabuswill be empty - useline.textdirectly - Background vocals: Check
word.isBackground === trueto style backing vocals differently - Transliteration: Check if
line.transliterationexists for romanized text support - Timing calculation: All times are in milliseconds - convert to seconds by dividing by 1000
- Error handling: Implement fallback to alternative server if primary fails