Response format

The default TTML provider returns lyrics as Timed Text Markup Language, with syllable-level timing.

Response structure

{
  "ttml": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>...",
  "score": 0.97
}
Field Type Description
ttml string The full TTML XML document as a string
score number Match confidence from 0 to 1. Higher is better. Sent only when the lyrics were fetched for the request (or for a matching request already in progress), not on cache hits.

TTML structure

The TTML document contains timing information for each word. For the full specification, see the W3C TTML1 Recommendation.

The example below is formatted for readability. Real responses are not formatted. See whitespace handling.

<?xml version="1.0" encoding="UTF-8"?>
<tt xmlns="http://www.w3.org/ns/ttml">
  <body>
    <div>
      <p begin="00:00:18.234" end="00:00:21.891">
        <span begin="00:00:18.234" end="00:00:18.567">The </span>
        <span begin="00:00:18.567" end="00:00:18.901">club </span>
        <span begin="00:00:18.901" end="00:00:19.234">isn't </span>
        <span begin="00:00:19.234" end="00:00:19.568">the </span>
        <span begin="00:00:19.568" end="00:00:19.901">best </span>
        <span begin="00:00:19.901" end="00:00:20.234">place </span>
        <span begin="00:00:20.234" end="00:00:20.568">to </span>
        <span begin="00:00:20.568" end="00:00:20.901">find </span>
        <span begin="00:00:20.901" end="00:00:21.234">a </span>
        <span begin="00:00:21.234" end="00:00:21.891">lover</span>
      </p>
    </div>
  </body>
</tt>

Key elements

<p> - Line element

Each <p> represents a line of lyrics:

  • begin - Start time of the line
  • end - End time of the line

<span> - Word element

Each <span> within a line represents a word:

  • begin - Start time of the word
  • end - End time of the word
  • Text content is the word itself (including trailing spaces)

Important: whitespace handling

Do not format or pretty-print the TTML response.

The response uses whitespace between <span> elements to represent spaces between syllables/words. This whitespace is not enclosed in tags. If you format the XML (for example with a pretty-printer), you lose these spaces and break the syllable sync.

Keep the original string exactly as received. When parsing, use the raw textContent of each span, which includes the intended whitespace.

Time format

Times can appear in two formats:

  • HH:MM:SS.mmm - e.g., 00:01:30.500 = 90.5 seconds
  • M:SS.mmm - e.g., 1:16.656 = 76.656 seconds

Your parser should handle both. The JavaScript example below does.

Parsing example (JavaScript)

@braccato/parsers is the parser Better Lyrics uses for these responses. It handles background vocals, agents, transliterations and whitespace for you. The example below shows how to do it by hand.

function parseTTML(ttmlString) {
  const parser = new DOMParser();
  const doc = parser.parseFromString(ttmlString, 'text/xml');
  const lines = [];

  doc.querySelectorAll('p').forEach(p => {
    const agent = p.getAttribute('ttm:agent'); // Agent ID as defined in metadata (v1, v2, v1000, etc.)
    const words = [];

    // Recursively collect spans, tracking background vocal context
    function collectSpans(element, isBackground = false) {
      element.childNodes.forEach(node => {
        if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'span') {
          const isBg = isBackground || node.getAttribute('ttm:role') === 'x-bg';
          const begin = node.getAttribute('begin');

          if (begin) {
            // This is a timed span (word)
            words.push({
              text: node.textContent,
              begin: parseTime(begin),
              end: parseTime(node.getAttribute('end')),
              isBackground: isBg
            });
          } else {
            // This is a wrapper span, recurse into it
            collectSpans(node, isBg);
          }
        }
      });
    }

    collectSpans(p);

    lines.push({
      begin: parseTime(p.getAttribute('begin')),
      end: parseTime(p.getAttribute('end')),
      agent,
      words
    });
  });

  return lines;
}

function parseTime(timeStr) {
  // Handles "HH:MM:SS.mmm", "M:SS.mmm", "SS.mmm"
  const parts = timeStr.split(':');
  let seconds = 0;

  if (parts.length === 3) {
    seconds = parseInt(parts[0]) * 3600 +
              parseInt(parts[1]) * 60 +
              parseFloat(parts[2]);
  } else if (parts.length === 2) {
    seconds = parseInt(parts[0]) * 60 +
              parseFloat(parts[1]);
  } else {
    seconds = parseFloat(parts[0]);
  }

  return Math.round(seconds * 1000); // Return milliseconds
}

RTL language support

For right-to-left languages (Arabic, Hebrew, Persian, etc.), the TTML document includes language attributes:

<tt xml:lang="ar" xmlns="http://www.w3.org/ns/ttml">
  ...
</tt>

Check the xml:lang attribute to determine text direction. Common RTL language codes: ar, he, fa, ur.

Multiple speakers

Songs with multiple vocalists use the ttm:agent attribute on <p> elements to identify which speaker is singing each line:

<p ttm:agent="v1" begin="0:05.123" end="0:08.456">
  <span begin="0:05.123" end="0:06.789">First </span>
  <span begin="0:06.789" end="0:08.456">vocalist</span>
</p>
<p ttm:agent="v2" begin="0:08.456" end="0:11.789">
  <span begin="0:08.456" end="0:10.123">Second </span>
  <span begin="0:10.123" end="0:11.789">vocalist</span>
</p>

Agent IDs are defined in the TTML metadata header:

<head>
  <metadata>
    <ttm:agent type="person" xml:id="v1">
      <ttm:name type="full">Artist Name</ttm:name>
    </ttm:agent>
    <ttm:agent type="person" xml:id="v2">
      <ttm:name type="full">Featured Artist</ttm:name>
    </ttm:agent>
  </metadata>
</head>
  • Agent IDs are defined via <ttm:agent xml:id="..."> in the metadata
  • Most files use v1, v2, etc., but some use different IDs like voice1
  • v1000 (group) and v2000 (other) conventionally indicate multiple speakers singing together
  • Agent metadata may include speaker names via <ttm:name>
  • The type attribute must be one of: person, character, group, organization, or other

Background vocals

Background vocals within a line are wrapped in a <span> with ttm:role="x-bg":

<p ttm:agent="v1" begin="0:15.000" end="0:20.000">
  <span begin="0:15.000" end="0:17.500">Lead lyrics </span>
  <span ttm:role="x-bg">
    <span begin="0:17.500" end="0:20.000">background vocals</span>
  </span>
</p>

Transliterations

Some TTML responses include synced transliterations (e.g., romanized Japanese lyrics). These are stored in the metadata header under <transliterations>:

<head>
  <metadata>
    <transliterations>
      <transliteration xml:lang="ja-Latn">
        <text for="L10">
          <span begin="1:03.557" end="1:04.868">mayonaka no</span>
          <span begin="1:04.868" end="1:05.457">doa o</span>
          <span begin="1:05.457" end="1:05.930">tata</span>
          <span begin="1:05.930" end="1:07.907">ki</span>
        </text>
      </transliteration>
    </transliterations>
  </metadata>
</head>
  • The xml:lang attribute specifies the target script (e.g., ja-Latn for Japanese romanization)
  • Each <text for="..."> references a line by its itunes:key attribute
  • Contains timed <span> elements, same as the main lyrics
  • Note: Some transliterations have punctuation (like )) outside of <span> elements as raw text nodes

Response headers

These response headers add context:

Header Description
X-Cache-Status Whether the response came from cache (HIT, MISS, etc.)
X-Provider Which provider served the response. Only on the provider-specific endpoints, such as /ttml/getLyrics. /getLyrics does not send it.
X-RateLimit-Type Current rate limit tier