Documentation Index
Fetch the complete documentation index at: https://docs.lyrcs.ai/llms.txt
Use this file to discover all available pages before exploring further.
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.
Events
| Event | Fired when |
|---|
job.complete | Job finishes successfully (or after artist approval if review=true) |
job.awaiting_review | Alignment done and review=true — results held pending artist approval |
job.degraded | First upstream (Gemini 503) retry — job is still processing |
job.failed | Job fails permanently |
batch.complete | All jobs in a batch finish (whether complete or failed) |
Retry schedule
lyrcs.ai retries failed webhook deliveries (non-2xx or timeout) on this schedule:
| Attempt | Delay |
|---|
| 1 | Immediate |
| 2 | +1 minute |
| 3 | +5 minutes |
| 4 | +30 minutes |
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.
Signature verification
Every webhook delivery includes an X-Lyrcs-Signature header containing an HMAC-SHA256 signature of the request body, keyed with your webhook_secret.
Verify the signature before processing the payload to confirm the request came from lyrcs.ai.
Node.js — signature verification
import crypto from "crypto";
export function verifyLyrcsSignature(rawBody, signature, webhookSecret) {
const expected = crypto
.createHmac("sha256", webhookSecret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex")
);
}
// In your webhook handler (e.g. Express):
app.post("/webhooks/lyrcs", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-lyrcs-signature"];
if (!verifyLyrcsSignature(req.body, signature, process.env.LYRCS_WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body.toString());
// handle event...
res.sendStatus(200);
});
Event payloads
job.complete
Fires when a job completes successfully. downloads is omitted when align=false was set. cultural_notes may be null.
{
"event": "job.complete",
"job_id": "a1b2c3d4-...",
"created_at": "2024-01-01T00:00:00.000Z",
"language": "Tamil",
"duration_seconds": 214,
"studio_url": "https://lyrcs.ai/studio/a1b2c3d4-...",
"results": {
"transcript": "நான் உன்னை நேசிக்கிறேன்...",
"transliteration": "Naan unnai nesikirein...",
"translation": "I love you...",
"cultural_notes": null,
"downloads": {
"lrc_original": "https://lyrcs.ai/api/v1/jobs/a1b2c3d4-.../download/lrc/original",
"lrc_transliterated": "https://lyrcs.ai/api/v1/jobs/a1b2c3d4-.../download/lrc/transliterated",
"srt_original": "https://lyrcs.ai/api/v1/jobs/a1b2c3d4-.../download/srt/original",
"srt_transliterated": "https://lyrcs.ai/api/v1/jobs/a1b2c3d4-.../download/srt/transliterated"
}
}
}
Download URLs in the payload still require your API key in the Authorization header to fetch.
job.awaiting_review
Fires instead of job.complete when review=true and alignment succeeded. No results block — lyrics are held until the artist approves.
{
"event": "job.awaiting_review",
"job_id": "a1b2c3d4-...",
"language": "Punjabi",
"review_url": "https://lyrcs.ai/review/a1b2c3d4-...?token=abc123",
"expires_at": "2024-01-02T00:00:00.000Z",
"studio_url": "https://lyrcs.ai/studio/a1b2c3d4-..."
}
Forward review_url to the artist. When they approve, job.complete fires with the full payload.
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.
{
"event": "job.degraded",
"job_id": "a1b2c3d4-...",
"language": "Hindi",
"reason": "gemini_503",
"attempt": 1,
"retrying_in_ms": 7243,
"message": "Transcription delayed due to upstream capacity. Job is retrying automatically."
}
| Field | Value |
|---|
reason | Always "gemini_503" |
attempt | Always 1 — only fires on the first retry |
retrying_in_ms | Milliseconds until the retry fires (includes jitter) |
job.failed
Fires when a job fails permanently after all retries are exhausted.
{
"event": "job.failed",
"job_id": "a1b2c3d4-...",
"created_at": "2024-01-01T00:00:00.000Z",
"language": "Tamil",
"error": "processing_failed"
}
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.
{
"event": "batch.complete",
"batch_id": "b1a2t3c4-...",
"status": "partial",
"job_count": 3,
"completed": 2,
"failed": 1,
"jobs": [
{
"job_id": "a1b2c3d4-...",
"language": "Hindi",
"status": "complete",
"review_required": false,
"review_url": null,
"review_approved_at": null,
"studio_url": "https://lyrcs.ai/studio/a1b2c3d4-...",
"downloads": {
"lrc_original": "https://lyrcs.ai/api/v1/jobs/a1b2c3d4-.../download/lrc/original",
"lrc_transliterated": "https://lyrcs.ai/api/v1/jobs/a1b2c3d4-.../download/lrc/transliterated",
"srt_original": "https://lyrcs.ai/api/v1/jobs/a1b2c3d4-.../download/srt/original",
"srt_transliterated": "https://lyrcs.ai/api/v1/jobs/a1b2c3d4-.../download/srt/transliterated"
}
},
{
"job_id": "b2c3d4e5-...",
"language": "Tamil",
"status": "failed",
"review_required": false,
"review_url": null,
"review_approved_at": null,
"studio_url": "https://lyrcs.ai/studio/b2c3d4e5-..."
}
]
}