Lyrics alignment API

Send audio plus the lyrics for that recording and receive word-level timing. Each word includes a start time, an end time, and a confidence value, and every job can be exported as LRC, SRT, VTT, or JSON. The API runs the same alignment used on this site.

https://api.lyrictimestamps.com

Quickstart

Three calls: send the audio and the lyrics, wait, take the file. Most songs finish in a minute or two.

  1. 1. Create a key on your account page. It is shown once.
  2. 2. Check it below, before you spend a song finding out.
  3. 3. Run the calls under Timing a song.

Check your key

Paste a key to confirm the API accepts it. This only calls GET /v1/usage — no job is created and no minutes are used. Your key is not saved anywhere.

Authentication

Send your key as a bearer token on every request. There is nothing else to sign, and no session to refresh.

Authorization: Bearer ltk_7f3a9c21b8d4_...
A wrong key is not always an error. On job creation a bad key is refused with 401. But on read endpoints it is treated as an anonymous visitor — and anonymous visitors are capped at a 60-second preview. So a mistyped or revoked key can come back as a finished job containing the first minute of the song. Call GET /v1/usage first: if the response says "plan": "anonymous" and has no tier, your key was not accepted. Build that check into your pipeline.

Keys are created from a signed-in browser session and never from another key, so a leaked key cannot mint more of itself, list your other keys, or revoke them. Revoke one at any time from your account page — it stops on the next request.

Timing a song

1. Create the job

Multipart form data. Send audio_file plus either lyrics_file or lyrics_text. Prefer the file: your line breaks decide where caption lines end, and every word comes back tagged with the line it came from.

curl -X POST https://api.lyrictimestamps.com/v1/alignments \
  -H "Authorization: Bearer $LT_API_KEY" \
  -F "audio_file=@song.mp3" \
  -F "lyrics_file=@lyrics.txt;type=text/plain"

# {"job_id":"a1b2...","status":"queued","created_at":"2026-09-23T12:00:00Z"}

Optional field: language (default auto). Every finished job produces all four formats regardless of what you ask for.

2. Poll for it

Statuses run queued → uploading → preparing_audio → reading_lyrics → aligning → checking → exporting → done. The only terminal states are done and failed — branch on those two and treat the rest as progress. Poll every 2–3 seconds.

curl https://api.lyrictimestamps.com/v1/alignments/$JOB_ID \
  -H "Authorization: Bearer $LT_API_KEY"

# {"job_id":"a1b2...","status":"aligning","progress":40}

A done job can still carry soft_fail: true — we finished, but we do not trust the result. soft_fail_code says why (collapsed_region, almost_no_words, zero_confidence, blank_words, too_few_words, too_many_words). Treat that list as open.

3. Take the output

/result gives word-level JSON — each word with start, end, confidence and line_index. /exports/{format} gives a finished file in srt, vtt, lrc or json.

# word-level JSON
curl https://api.lyrictimestamps.com/v1/alignments/$JOB_ID/result \
  -H "Authorization: Bearer $LT_API_KEY"

# a finished caption file — the response IS the file
curl -o song.srt https://api.lyrictimestamps.com/v1/alignments/$JOB_ID/exports/srt \
  -H "Authorization: Bearer $LT_API_KEY"

# one word per cue, for karaoke highlighting
curl -o words.srt "https://api.lyrictimestamps.com/v1/alignments/$JOB_ID/exports/srt?words=1" \
  -H "Authorization: Bearer $LT_API_KEY"

Export options: ?words=1 for one word per cue (VTT timestamp tags, enhanced LRC), ?long=1 to stretch each word into the surrounding silence, ?variant= for an alternative timing technique.

Files over the direct-upload limit

Multipart has a hard ceiling at our edge. Above it, take a ticket, PUT the bytes straight to storage, then create the job with the ticket. Send the same Authorization header on all three calls — the ticket is bound to the caller.

# 1. ask for a ticket — bytes must be the EXACT byte count
curl -X POST https://api.lyrictimestamps.com/v1/uploads \
  -H "Authorization: Bearer $LT_API_KEY" -H "Content-Type: application/json" \
  -d '{"filename":"song.wav","bytes":48231904}'
# -> {"upload_token":"...","upload_url":"...","method":"PUT","headers":{...}}

# 2. PUT the bytes, echoing the returned headers object back verbatim
curl -X PUT "$UPLOAD_URL" -H "Content-Type: audio/wav" --data-binary @song.wav

# 3. create the job with the ticket instead of the file
curl -X POST https://api.lyrictimestamps.com/v1/alignments \
  -H "Authorization: Bearer $LT_API_KEY" \
  -F "upload_token=$UPLOAD_TOKEN" -F "lyrics_file=@lyrics.txt"

Endpoints

POST/v1/alignments
Create a job. Multipart. Returns 201 with job_id.
GET/v1/alignments/{job_id}
Status, progress, warnings, soft_fail, and error on a failure.
GET/v1/alignments/{job_id}/result
Word-level result. 202 while still running, so only call it on done.
GET/v1/alignments/{job_id}/exports/{format}
The finished file itself — srt, vtt, lrc, json.
GET/v1/alignments
Your past jobs, newest first.
DELETE/v1/alignments/{job_id}
Delete a job and everything stored for it.
POST/v1/uploads
A ticket for the two-step upload path, for large files.
GET/v1/usage
Your tier and remaining minutes. Use it to verify a credential.
GET/v1/config
Limits and accepted formats. No authentication needed.
GET/v1/formats
Supported output formats.

Key management lives on the same API and takes a signed-in session rather than a key: POST /v1/api-keys, GET /v1/api-keys, DELETE /v1/api-keys/{key_id}.

Limits

Read live from GET /v1/config, so this table cannot drift from what the server enforces. Call it yourself — it needs no authentication.

Loading limits…

API usage draws on its own monthly allowance, separate from the minutes the same account uses on this website — a batch run cannot spend the minutes you need in the browser. API jobs also yield GPU capacity to interactive users rather than queueing ahead of them, so heavy use does not slow the site down for anyone else.

Errors

Errors are {"code", "message"}. Branch on code; the message is prose and may be reworded.

CodeMeaningRetry?
401 invalid_api_keyKey wrong, or revokedNo — make a new one
401 invalid_tokenSession token expiredNo — refresh it
400 missing_audioNo audio_file or upload_tokenNo
409 duplicate_in_progressSame submission already runningYes, after a pause
413 file_too_largeAbove the upload ceilingNo — use /v1/uploads
413 song_too_longLonger than your per-song capNo
429 limit_reachedOut of minutes, or too many jobsNo
404 job_not_foundWrong id — or wrong credentialNo — see below
503 temporarily_unavailableA dependency is downYes, with backoff

Things that trip people

  • Send form data, not JSON. A JSON body is not rejected — every field has a default, so the request runs and answers 400 missing_audio with your lyrics silently discarded.
  • A missing bearer on a later call is a 404, not a 403. A job created with a credential requires that same credential to read or export it. If your download worker does not carry the header your polling loop had, you will go hunting for a job-id bug that is not there.
  • The export response is the file. There is no signed-URL step and nothing expires.
  • Caption lines follow your lyric lines. One cue per line you submit, never re-chopped. Want shorter captions? Send shorter lines.
  • exports/json and /result are different shapes. The export is a bare array of words; /result is an object with them under words. Easy to trip over with one parser.
  • Call it from a server. Browser JavaScript on your own domain will be refused by CORS. Ask us and we can add your origin, but the API is built for backend use.
  • There is no sandbox. Every call is a real job against real minutes. Test with short clips — metering is by audio duration, and a failed job gives the minutes back.