Skip to main content

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

EventFired when
job.completeJob finishes successfully (or after artist approval if review=true)
job.awaiting_reviewAlignment done and review=true — results held pending artist approval
job.degradedFirst upstream (Gemini 503) retry — job is still processing
job.failedJob fails permanently
batch.completeAll jobs in a batch finish (whether complete or failed)

Retry schedule

lyrcs.ai retries failed webhook deliveries (non-2xx or timeout) on this schedule:
AttemptDelay
1Immediate
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."
}
FieldValue
reasonAlways "gemini_503"
attemptAlways 1 — only fires on the first retry
retrying_in_msMilliseconds 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-..."
    }
  ]
}