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. Create a key on your account page. It is shown once.
- 2. Check it below, before you spend a song finding out.
- 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_...
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
/v1/alignments201 with job_id./v1/alignments/{job_id}soft_fail, and error on a failure./v1/alignments/{job_id}/result202 while still running, so only call it on done./v1/alignments/{job_id}/exports/{format}srt, vtt, lrc, json./v1/alignments/v1/alignments/{job_id}/v1/uploads/v1/usage/v1/config/v1/formatsKey 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.
| Code | Meaning | Retry? |
|---|---|---|
| 401 invalid_api_key | Key wrong, or revoked | No — make a new one |
| 401 invalid_token | Session token expired | No — refresh it |
| 400 missing_audio | No audio_file or upload_token | No |
| 409 duplicate_in_progress | Same submission already running | Yes, after a pause |
| 413 file_too_large | Above the upload ceiling | No — use /v1/uploads |
| 413 song_too_long | Longer than your per-song cap | No |
| 429 limit_reached | Out of minutes, or too many jobs | No |
| 404 job_not_found | Wrong id — or wrong credential | No — see below |
| 503 temporarily_unavailable | A dependency is down | Yes, 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_audiowith 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/jsonand/resultare different shapes. The export is a bare array of words;/resultis an object with them underwords. 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.