> ## 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.

# GET /jobs

> List and sync jobs — the efficient way to track work at catalogue scale

## Endpoint

```
GET https://lyrcs.ai/api/v1/jobs
```

Returns a page of your organisation's jobs, oldest change first. This is the
endpoint to build a catalogue integration on: instead of polling
`GET /jobs/{id}` once per song, you ask once per sweep for everything that has
changed since you last looked.

<Note>
  Jobs are always scoped to the organisation that owns the API key. There is no
  parameter that widens that scope.
</Note>

## Syncing with `updated_since`

Every job carries an `updated_at` timestamp that moves whenever the job actually
changes. The sync loop is:

1. Call `GET /jobs?updated_since={your last high-water mark}`.
2. Process the returned jobs.
3. Store the highest `updated_at` you saw as the new high-water mark.

```bash theme={null}
curl -H "Authorization: Bearer $LYRCS_API_KEY" \
  "https://lyrcs.ai/api/v1/jobs?updated_since=2026-09-01T00:00:00Z&limit=100"
```

`updated_since` is exclusive, so passing back the newest `updated_at` you
received will not return that job again.

Results are ordered by `updated_at` **ascending** — oldest change first. That is
deliberate, and it is what makes the loop safe to interrupt: your watermark only
ever advances over jobs you have actually processed, so a sweep that dies half
way through resumes exactly where it stopped.

Descending order would break this. The first page would already contain the
newest timestamp, so storing it and then failing would skip every older job you
had not yet reached — permanently, because those jobs never become newer than
your watermark again.

## Query parameters

| Parameter       | Type                                   | Default | Description                                                                                      |
| --------------- | -------------------------------------- | ------- | ------------------------------------------------------------------------------------------------ |
| `updated_since` | ISO 8601 timestamp                     | —       | Only jobs changed strictly after this time                                                       |
| `status`        | `processing` \| `complete` \| `failed` | —       | Filter by job status                                                                             |
| `external_id`   | string                                 | —       | Exact match on your own identifier                                                               |
| `isrc`          | string                                 | —       | Exact match on the recording's ISRC (hyphens accepted)                                           |
| `end_user_id`   | string                                 | —       | Only jobs belonging to one of your customers. Takes your own `external_id` or the `id` we return |
| `batch_id`      | UUID                                   | —       | Only jobs belonging to this batch                                                                |
| `limit`         | integer 1–100                          | `50`    | Page size                                                                                        |
| `cursor`        | string                                 | —       | Continue from a previous page — see below                                                        |

## Pagination

When more results exist, `has_more` is `true` and `next_cursor` carries an opaque
cursor. Pass it back as `cursor` to fetch the next page.

```bash theme={null}
curl -H "Authorization: Bearer $LYRCS_API_KEY" \
  "https://lyrcs.ai/api/v1/jobs?limit=100&cursor=MjAyNi0wOS0wMVQwMDowMDowMC4wMDBafGFiYw"
```

Keep every other parameter identical while paging. The cursor encodes a position
in the result order, not a filter.

Because rows are ascending, it is safe to advance your stored watermark page by
page as you go rather than waiting for the last page.

<Warning>
  Treat `next_cursor` as opaque. Its encoding is not part of the contract and may
  change. Do not construct one yourself.
</Warning>

## Response

```json theme={null}
{
  "jobs": [
    {
      "job_id": "a1b2c3d4-...",
      "external_id": "TRACK_12345",
      "isrc": "USSKG2400001",
      "language": "Tamil",
      "status": "complete",
      "stage": "done",
      "end_user": { "id": "2fc0df07-...", "external_id": "cust_8842" },
      "align_requested": true,
      "review_required": false,
      "review_url": null,
      "review_approved_at": null,
      "created_at": "2026-09-01T09:12:00.000Z",
      "updated_at": "2026-09-01T09:13:41.000Z",
      "completed_at": "2026-09-01T09:13:41.000Z",
      "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"
      },
      "second_opinion": { "match_ratio": 0.92, "suggestion_count": 3 },
      "alignment": { "method": "forced", "flagged_lines": 0 }
    }
  ],
  "has_more": true,
  "next_cursor": "MjAyNi0wOS0wMVQwOToxMzo0MS4wMDBafGExYjJjM2Q0"
}
```

Each row carries the same fields as a job in `GET /batch/{id}`, plus `created_at`
and `updated_at`. Rows held at the transcript review gate report
`stage: "awaiting_review"`, which is the cheapest way to find everything waiting
on a human across a whole catalogue. `status` and `stage` mean exactly what they mean on
[`GET /jobs/{id}`](/api-reference/jobs).

`second_opinion` and `alignment` are **summaries** here rather than the full
blocks — a page of suggestion text would be large and mostly unread. Fetch the
individual job for the detail. As everywhere else, `null` means the check did not
run, and is never the same as a clean result.

The full transcript and lyrics are **not** included in list rows. Use
`GET /jobs/{id}` or the download URLs for content.

## Rate limits

Two fields appear only when they apply: `end_user` is present when the job was
submitted with one, and `review_delivery` is present when `review_required` is
`true`. `external_id` and `isrc` are always present, and `null` when unset.

This endpoint draws on the **read** meter, not the submit meter, so polling never
consumes the budget that submits songs. Defaults are 120/minute, 2,000/hour and
20,000/day; your organisation may carry higher limits.

For scale, a 50,000-song catalogue swept at `limit=100` costs about 500 requests
per full pass, against roughly 600,000 for per-job polling.

## Errors

| Status | Code       | Cause                                                              |
| ------ | ---------- | ------------------------------------------------------------------ |
| 400    | `VAL_001`  | Invalid `limit`, `status`, `updated_since`, `batch_id` or `cursor` |
| 401    | `AUTH_001` | Missing, invalid, or expired API key                               |
| 429    | `RATE_001` | Read rate limit exceeded — see `Retry-After`                       |
