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

# POST /jobs/[id]/approve

> Approve a job held for review, from your own server

## Endpoint

```
POST https://lyrcs.ai/api/v1/jobs/{job_id}/approve
```

Approves a job held at a review gate, authenticated with your API key. No review
link, no browser, no visit to lyrcs.ai — for when the artist reviews the lyrics
inside your own product.

This is one of two doors into the same held job. The other is our review page,
reached through `review_url`. Both converge on the same approval and the same
outputs. See [Running Review In Your Own Product](/guides/partner-review).

## Request

Both fields are optional. Omit them to approve the lyrics unchanged.

```bash theme={null}
curl -X POST https://lyrcs.ai/api/v1/jobs/{job_id}/approve \
  -H "Authorization: Bearer $LYRCS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "lines": ["first line", "second line corrected", "third line"],
    "transliterated_lines": ["…", "…", "…"]
  }'
```

| Field                  | Type       | Notes                                                                            |
| ---------------------- | ---------- | -------------------------------------------------------------------------------- |
| `lines`                | `string[]` | Edited lines of the original script. Cannot be an empty array — omit it instead. |
| `transliterated_lines` | `string[]` | Edited transliteration. Position-parallel to `lines`.                            |

### The two scripts move together

Index 3 of `transliterated_lines` is the romanisation of index 3 of `lines`, and
alignment inherits each transliteration's timing from its original by index. When
you add or remove a line, send **both** arrays — sending one alone with a
different length is refused with `409 APR_005`.

The refusal exists because the alternative is silent: an uneven pair does not fail
alignment, it produces a blank transliterated line, and a deletion shifts every
remaining pair onto the wrong original for the rest of the song.

### Text only, no timestamps

Timings are taken from the job, not from you. You have not run the aligner, so
you have no measured timings to send, and anything an edit invalidates is
re-derived from the audio on approval.

Read the current lyrics from the `review` block on
[`GET /jobs/{id}`](/api-reference/jobs) while the job is held.

## Responses

**Approved at the transcript gate** — `200`. The job is *not* finished; alignment
now runs on the text you approved.

```json theme={null}
{
  "job_id": "a1b2c3d4-…",
  "status": "approved",
  "stage": "transcript",
  "lines_edited": 1,
  "transliteration_lines_edited": 1,
  "aligning": true,
  "message": "Approved. Alignment is running; poll GET /v1/jobs/{id} or wait for the job.complete webhook."
}
```

**Approved at the aligned gate** — `200`. The job is finished.

```json theme={null}
{
  "job_id": "a1b2c3d4-…",
  "status": "approved",
  "stage": "aligned",
  "lines_edited": 1,
  "transliteration_lines_edited": 0,
  "alignment": "realigned"
}
```

`alignment` reports what happened to the timings, and is absent when no edit
landed:

| Value                    | Meaning                                                                                                                                 |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| `realigned`              | The edit was re-timed against the audio.                                                                                                |
| `word_timings_dropped`   | Re-alignment was unavailable, so the now-incorrect word-level track was removed rather than returned stale. Line timings are unchanged. |
| `line_timings_unchanged` | The job had no word-level track to drop; line timings are unchanged.                                                                    |

**Already approved** — `200`. Not an error.

```json theme={null}
{ "job_id": "a1b2c3d4-…", "status": "approved", "already_approved": true, "stage": "transcript" }
```

The first approval wins, whichever door it came through. Retrying after a timeout
is safe: nothing is applied twice, nothing is re-aligned, nothing is charged.

## Errors

| Status | Code      | Meaning                                                                                               |
| ------ | --------- | ----------------------------------------------------------------------------------------------------- |
| 400    | `APR_002` | `lines` or `transliterated_lines` was not an array of strings, or was empty                           |
| 404    | `NOT_001` | No such job, or it belongs to another organisation                                                    |
| 409    | `APR_001` | The job is not waiting for review                                                                     |
| 409    | `APR_003` | The job was submitted with `review_delivery: "link"`, so it can only be approved from the review page |
| 409    | `APR_005` | `lines` and `transliterated_lines` would have different lengths                                       |
| 409    | `REV_001` | Line count changed at the aligned gate and the timings cannot be re-derived                           |
| 502    | `APR_004` | The edits were saved but the job could not be released. Retry — the approval was rolled back          |

## Rate limiting

Counted against your **read** limit, not your submission limit. A reviewed job
draws one submission when you create it, and approving it is the second half of
that same submission rather than a new one.
