Skip to main content
Webhooks deliver event payloads to your server as jobs complete. Pass a webhook_url when submitting a job or batch — lyrcs.ai will POST the event payload to that URL.
Webhooks are the recommended way to consume the API. They fire only on terminal outcomes, so there is no in-flight state to interpret and no need to decide how long is too long. Polling works, but it puts the burden of judging “is this job stuck?” on your client — and that judgement is the single most common source of integration bugs.If you do poll, see job status for how long jobs legitimately take, and don’t impose a shorter timeout of your own.

Events

Retry schedule

lyrcs.ai retries failed webhook deliveries (non-2xx or timeout) on this schedule: Your endpoint must return a 2xx response within 10 seconds or the delivery is counted as failed.
The job.complete webhook fired after artist approval in the review flow uses single-attempt delivery — it does not retry. Ensure your review webhook endpoint is reliable.

Delivery headers

Every delivery carries these:

Signature verification

Every webhook delivery includes an X-Lyrcs-Signature header containing an HMAC-SHA256 signature of the raw request body, keyed with your webhook_secret. The header value is prefixed, in the form sha256=<hex digest>. Strip the prefix before comparing — passing the whole header to a hex decoder yields a buffer of the wrong length, and timingSafeEqual throws rather than returning false. Verify the signature before processing the payload to confirm the request came from lyrcs.ai.
Node.js — signature verification

Event payloads

job.complete

Fires when a job completes successfully. downloads is omitted when align=false was set. cultural_notes may be null. end_user is present only when the job was submitted with one.
Download URLs in the payload still require your API key in the Authorization header to fetch.
words_original and words_transliterated are present when word_align_requested: true (the default). Jobs submitted with word_align=false include only lrc_* and srt_* in downloads.
second_opinion and alignment are always present and may be null, which means the check did not run. An empty suggestions array is the opposite signal — it ran and agreed with every line. See Quality Signals.

job.awaiting_review

Fires instead of job.complete when review=true and the job reaches its review gate. No results block — the job.complete webhook is what waits for approval. The gate is after transcription when review_stage is "transcript", and after alignment when it is "aligned" (the default). The payload’s review_stage field says which, since the same event fires at both. Note that only this webhook is deferred: GET /jobs/{id} continues to report the job’s real state, and on an aligned-stage job the results are readable before approval. review_approved_at is the field that means a human signed off. review_url and expires_at are null when the job was submitted with review_delivery: "api" — no review link is created for those jobs by design. Approve them at approve_url; see Running Review In Your Own Product.
Forward review_url to the artist. When they approve, job.complete fires with the full payload. artist_url is a durable, catalogue-wide link for a submission that carried an end_user — it opens that artist’s own page (their songs, review, downloads, and video ordering) and slides its own expiry, so it keeps working for weeks. It is null when the job had no end_user. See Running Review In Your Own Product.

job.degraded

Fires on the first Gemini 503 retry only. The job is still processing — this is informational. Do not assume the job has failed.

job.failed

Fires when a job fails permanently after all retries are exhausted.

batch.complete

Fires when all jobs in a batch have finished — whether complete or failed. status is "complete" if all jobs succeeded, "partial" if any failed or timed out. downloads is included per job only when status === "complete" AND align_requested === true for that job.