> ## Documentation Index
> Fetch the complete documentation index at: https://docs.arcmira.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Arcmira has three API capabilities: Search (entities, mentions, appearances, commercial intelligence), Monitors (trackers plus alert delivery), and Transcripts (read, generate, correct). Community Review is the cross-surface correction layer.
> Discover pages at https://docs.arcmira.com/llms.txt, then fetch the matching page's .md export. Each capability has a self-contained '<capability> for coding agents' page; prefer it.
> Auth: Authorization: Bearer arc_sk_... or x-api-key: arc_sk_... (x-api-key wins if both are sent). Never put keys in browser code. Check GET /v1/me for tier, scopes, and remaining rows before metered pulls.
> Search: resolve names with GET /v1/entities/lookup and pin ent_* IDs before pulling metered rows. Only person entities have appearances. Mention and recommendation rows carry canonical numeric timestamps in start_seconds/end_seconds (integer seconds; 0 means full episode); the MM:SS string fields are deprecated. Commercial routes need Pro+ tier AND the recommendations:read scope and bill 10 rows per row.
> Monitors: a tracker watches one entity; a monitor groups trackers with shared delivery (email, Slack, HMAC-signed webhooks). Create trackers with POST /v1/trackers, then attach by id: POST /v1/monitors/{id}/trackers { trackerIds: [...] }. Each fired alert occurrence debits 100 rows. Poll GET /v1/monitors/{id}/alerts?n= for recent deliveries. Send Idempotency-Key on every POST/PATCH.
> Transcripts: switch on the access field (unlocked, locked, premium_pending, not_transcribed, unauthenticated). Honor Retry-After when polling POST /v1/transcriptions jobs. Corrections (POST /v1/videos/{video_id}/corrections; kinds line_edit, speaker_reassign, speaker_identify, add_person, entity_tag, segment_rewrite) cost 0 rows; anchored kinds need revision + anchor.contentHash (djb2 base-36); handle 409 (re-anchor) and 412 (expectedSeq). segment_rewrite replaces an inclusive segment range with new segments (empty replacements array deletes; timestamps repaired server-side).
> Community Review is free (0 rows) and surface-typed: POST /v1/feedback with a type matching the surface you called, the reproducing query, and corrections targeting public IDs (ent_*, men_*, com_*). Nothing auto-applies. Do not send untyped notes.
> Errors: switch on error.code, follow doc_url, quote X-Request-Id to support. Honor Retry-After on 429. Retries of POST/PATCH must reuse the same Idempotency-Key; replays return Idempotency-Replayed: true.

# Transcripts for coding agents

> Self-contained reference for transcripts: read/unlock, transcription submission and polling, and all six correction kinds with full payloads and the queue-client error contract.

## Overview

Transcripts are premium, diarized, entity-annotated, and community-correctable.

* Base URL: `https://api.arcmira.com`, paths under `/v1`.
* Auth: `Authorization: Bearer arc_sk_...` or `x-api-key: arc_sk_...`. Transcript access requires a paid plan.
* Billing: 75 rows per 15-minute block of video, rounded up, minimum one block, debited from the shared monthly pool. Unlocks are permanent per account; re-reads free; a job that fails permanently auto-refunds (status `refunded`) and revokes the unlock it charged for. **Corrections cost 0 rows.**

## Endpoints

```text theme={null}
GET    /v1/transcripts/{video_id}                read; ?unlock=true purchases in the same request
POST   /v1/transcriptions                        submit a video ({ url } or { videoId })
GET    /v1/transcriptions/{id}                   poll status (Retry-After header while in flight)
GET    /v1/transcriptions?video_id=              list your requests
POST   /v1/videos/{video_id}/corrections         unified corrections ingestion (all six kinds)
DELETE /v1/corrections/speaker-edits/{id}        withdraw pending speaker_reassign
DELETE /v1/corrections/entity-tags/{id}          withdraw pending entity_tag
POST   /v1/transcripts/{video_id}/edits          wrapper: line_edit           (DELETE .../edits/{id})
POST   /v1/transcripts/{video_id}/speakers       wrapper: speaker identify    (DELETE .../speakers/{id})
POST   /v1/transcripts/{video_id}/merges         video-level entity merge     (GET, DELETE .../merges/{id})
```

Prefer the unified corrections route; the wrappers are conveniences for single-kind clients.

## Minimal working example

<CodeGroup>
  ```typescript TypeScript theme={null}
  const BASE = 'https://api.arcmira.com';
  const H = { Authorization: `Bearer ${process.env.ARCMIRA_API_KEY}` };

  // Read; unlock if a premium transcript exists
  let t = await fetch(`${BASE}/v1/transcripts/dQw4w9WgXcQ`, { headers: H }).then(r => r.json());
  if (t.access === 'locked') {
    t = await fetch(`${BASE}/v1/transcripts/dQw4w9WgXcQ?unlock=true`, { headers: H }).then(r => r.json());
  }
  if (t.access === 'premium_pending' || t.access === 'not_transcribed') {
    const { request } = await fetch(`${BASE}/v1/transcriptions`, {
      method: 'POST',
      headers: { ...H, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID() },
      body: JSON.stringify({ videoId: 'dQw4w9WgXcQ' }),
    }).then(r => r.json());
    // poll request.id until status is terminal, sleeping on Retry-After
  }
  ```

  ```python Python theme={null}
  import os, time, uuid, httpx

  BASE = 'https://api.arcmira.com'
  H = {'Authorization': f"Bearer {os.environ['ARCMIRA_API_KEY']}"}

  t = httpx.get(f'{BASE}/v1/transcripts/dQw4w9WgXcQ', headers=H).json()
  if t['access'] == 'locked':
      t = httpx.get(f'{BASE}/v1/transcripts/dQw4w9WgXcQ', params={'unlock': 'true'}, headers=H).json()
  elif t['access'] in ('premium_pending', 'not_transcribed'):
      req = httpx.post(
          f'{BASE}/v1/transcriptions',
          json={'videoId': 'dQw4w9WgXcQ'},
          headers={**H, 'Idempotency-Key': str(uuid.uuid4())},
      ).json()['request']
      while True:
          r = httpx.get(f"{BASE}/v1/transcriptions/{req['id']}", headers=H)
          if r.json()['status'] in ('complete', 'failed', 'refunded'):
              break
          time.sleep(int(r.headers.get('Retry-After', '30')))
  ```

  ```bash cURL theme={null}
  curl 'https://api.arcmira.com/v1/transcripts/dQw4w9WgXcQ?unlock=true' \
    -H "Authorization: Bearer $ARCMIRA_API_KEY"
  ```
</CodeGroup>

## Request parameters

`GET /v1/transcripts/{video_id}`:

| Parameter  | Type    | Default | Description                                                                                                                                                     |
| ---------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `video_id` | path    | —       | YouTube video ID (the 11-char form).                                                                                                                            |
| `unlock`   | boolean | `false` | When `access` is `locked`, purchase the permanent unlock in this request. Ignored on `premium_pending` (nothing to unlock yet; the purchase is the generation). |

`POST /v1/transcriptions` body:

| Field     | Type   | Description                                                       |
| --------- | ------ | ----------------------------------------------------------------- |
| `url`     | string | YouTube watch URL. At least one of `url` / `videoId` is required. |
| `videoId` | string | YouTube video ID (11 characters). Prefer this form.               |

Constraints: public YouTube videos only, up to 12 hours, paid tiers only. Rows debited up front; unlock granted at submit time. Resubmitting an in-flight video returns the existing request with `existing: true`. If a premium transcript already exists, the request short-circuits to `complete` (unified pricing; `quote.rows` shows what was actually debited, `0` if you were already unlocked).

## Response schema

Transcript read:

```json theme={null}
{
  "video": { "videoId": "dQw4w9WgXcQ", "title": "…", "channelName": "…", "durationSeconds": 3720 },
  "access": "unlocked",
  "segments": [
    { "index": 0, "start": 0.4, "end": 6.1, "text": "Welcome back to the show…", "speaker": 0 }
  ],
  "speakers": [
    { "id": 0, "label": "Speaker 0", "entity": { "id": 147403, "name": "Emad Mostaque", "slug": "emad-mostaque" } }
  ],
  "annotations": [
    { "segmentIndex": 12, "charStart": 34, "charEnd": 40, "entityId": 120034, "entityType": "organization", "name": "OpenAI", "slug": "openai" }
  ],
  "edits": [],
  "speakerIdentifications": [],
  "speakerEdits": [],
  "entityTags": [],
  "meta": { "diarized": true, "locked": false, "revision": "rwmksmk", "quote": { "quarters": 5, "rows": 375 } }
}
```

| Field                                                              | Type           | Notes                                                                                                                                                                                                                                                                                                                                                                         |
| ------------------------------------------------------------------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `access`                                                           | enum           | `unlocked` (full payload) · `locked` (\~5 teaser segments + quote; buy with `?unlock=true`) · `premium_pending` (entity-type **counts only** in `detected`, no text/timestamps; submit a transcription) · `not_transcribed` (submit) · `unauthenticated` (web-only; unreachable on `/v1`, which always requires a key). Gating is server-side; locked payloads lack the rest. |
| `segments[].start` / `.end`                                        | number         | **Seconds**, fractional allowed.                                                                                                                                                                                                                                                                                                                                              |
| `segments[].speaker`                                               | number         | Diarization speaker id; join against `speakers[]`.                                                                                                                                                                                                                                                                                                                            |
| `speakers[].entity`                                                | object \| null | Null until the speaker is identified as a person.                                                                                                                                                                                                                                                                                                                             |
| `annotations[]`                                                    | array          | Character spans (`charStart`/`charEnd`) within `segments[segmentIndex].text`. Note the camelCase; `entityId` is a raw integer (batch display metadata via `/v1/entities/cards`).                                                                                                                                                                                              |
| `edits` / `speakerIdentifications` / `speakerEdits` / `entityTags` | arrays         | **Your own pending corrections**, returned only to you, only when unlocked. Each row carries the `id` used to withdraw it.                                                                                                                                                                                                                                                    |
| `meta.revision`                                                    | string         | Opaque transcript+corrections state id. Echo it in anchored corrections; a changed value means re-read before continuing.                                                                                                                                                                                                                                                     |
| `meta.quote`                                                       | object         | `{ quarters, rows }`: the exact unlock price. Quoting is free.                                                                                                                                                                                                                                                                                                                |

Transcription request object. Three envelopes, one shape: **submit** wraps it as `{ "request": {...}, "existing": true? }`, **poll** returns the object flat (no wrapper), and **list** returns `{ "requests": [...] }` with an added nullable `title` per row.

```json theme={null}
{
  "id": "0b0e8f3a-…",
  "videoId": "dQw4w9WgXcQ",
  "status": "queued",
  "stage": "queued",
  "quote": { "quarters": 5, "rows": 375 },
  "etaSeconds": 678,
  "nextPollSeconds": 30
}
```

| Field                            | Type   | Notes                                                                                                                                                                                                                                              |
| -------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status`                         | enum   | Walks `queued` → `downloading` → `transcribing` → `analyzing` → `complete`. Failures surface as `refunded`: rows returned, and the unlock revoked when this submission charged them. (`failed` is reserved in the enum but not currently written.) |
| `etaSeconds` / `nextPollSeconds` | number | Present while in flight, alongside a `Retry-After` response header (seconds). Terminal statuses drop the header.                                                                                                                                   |
| List rows                        | —      | `GET /v1/transcriptions` returns `{ "requests": [...] }` with `title` (nullable) per row; 20 rows unfiltered, 5 when filtered by `video_id`. Up to 5 in-flight rows are reconciled against live pipeline state per list call.                      |

Polling loop: sleep on `Retry-After`, re-poll, stop on any terminal status (`complete` / `refunded`; treat `failed` as terminal too for forward compatibility). On `complete`, `GET /v1/transcripts/{video_id}` works immediately: you were unlocked at submission. Never busy-poll.

## Corrections

All six kinds go through `POST /v1/videos/{video_id}/corrections` with a discriminated `kind`, optional `seq`, and (for anchored kinds) `revision` + `anchor`. Free, `Idempotency-Key` supported, pending review for everyone but you.

Envelope:

```json theme={null}
{
  "kind": "line_edit",
  "seq": 4,
  "revision": "rwmksmk",
  "anchor": { "segmentIndex": 12, "contentHash": "1a2b3c" },
  "payload": { }
}
```

### The six kinds, full payloads

`line_edit` (anchored):

```json theme={null}
{
  "kind": "line_edit",
  "revision": "rwmksmk",
  "anchor": { "segmentIndex": 12, "contentHash": "1a2b3c" },
  "payload": {
    "segmentIndex": 12,
    "originalText": "Anthropics new cloud 4.5",
    "correctedText": "Anthropic's new Claude 4.5"
  }
}
```

`speaker_reassign` (anchored; sub-line splits supported; `target.kind` is `existing`, `new`, or `role` with roles like `clip` / `other` / `unidentified`):

```json theme={null}
{
  "kind": "speaker_reassign",
  "revision": "rwmksmk",
  "anchor": { "segmentIndex": 18, "contentHash": "9zq1aa" },
  "payload": {
    "selection": { "startIndex": 18, "startChar": 0, "endIndex": 19, "endChar": 42 },
    "target": { "kind": "existing", "speakerId": 2 }
  }
}
```

`speaker_identify` (not anchored; creates a community-flagged appearance on the person's page immediately; requires a numeric `entityId`. To propose a person by name, use `add_person` instead):

```json theme={null}
{
  "kind": "speaker_identify",
  "revision": "rwmksmk",
  "payload": { "speakerId": 2, "entityId": 147403 }
}
```

`add_person` (not anchored; proposes a person not in the index and links the speaker in one action):

```json theme={null}
{
  "kind": "add_person",
  "revision": "rwmksmk",
  "payload": { "speakerId": 3, "name": "Jordi Hays" }
}
```

`entity_tag` (anchored; existing entity via `entityId`, or propose one with `proposedName` + `proposedType`):

```json theme={null}
{
  "kind": "entity_tag",
  "revision": "rwmksmk",
  "anchor": { "segmentIndex": 12, "contentHash": "1a2b3c" },
  "payload": { "segmentIndex": 12, "charStart": 34, "charEnd": 40, "entityId": 120034 }
}
```

`segment_rewrite` (anchored; the structural primitive: replaces segments `startIndex..endIndex` inclusive with the replacements):

```json theme={null}
{
  "kind": "segment_rewrite",
  "revision": "rwmksmk",
  "anchor": { "segmentIndex": 12, "contentHash": "8kq2mf" },
  "payload": {
    "startIndex": 12,
    "endIndex": 14,
    "replacements": [
      { "text": "So Anthropic's Claude 4.5 is the new model they shipped.", "start": 754 },
      { "text": "It handles long-horizon coding tasks.", "speaker": 3 }
    ]
  }
}
```

* Any N-to-M shape: merge mangled ASR lines into one clean segment, split one into several, or delete the range outright with `"replacements": []`.
* **Timestamps are repaired, optionally pinned**: `start`/`end` (seconds) are optional pins per replacement and must be non-decreasing across the list. Every unpinned time is interpolated char-proportionally between the surrounding pins; with no pins at all, the outer bounds are the source time range. To fix a wrong timestamp, rewrite the segment with the same text and an explicit `start`.
* `speaker` is optional per replacement; omitted, it inherits from the source segment the replacement's repaired start time falls in.
* `startIndex` must equal `anchor.segmentIndex`, and the `contentHash` covers `startIndex..endIndex` (joined with `\n`). Limits: 50 source segments, 50 replacements, 2000 chars per replacement.

### Anchors and the content hash

`anchor.contentHash` is a djb2 hash of the covered segment text (segments joined with `\n` for multi-segment selections), base-36 encoded:

```javascript theme={null}
function djb2(input) {
  let hash = 5381;
  for (let i = 0; i < input.length; i++) {
    hash = ((hash << 5) + hash + input.charCodeAt(i)) >>> 0;
  }
  return hash.toString(36);
}
```

### `seq` and the queue-client contract

`seq` is an optional per-user, per-video monotonic counter for clients submitting streams of dependent corrections (sub-line splits re-index later segments, so order matters). One-off submissions can omit it. The error semantics are designed for at-least-once queue clients:

| Status        | Meaning                                                                                                                                               | What to do                                                                        |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `201`         | Stored. Body: `{ ok, kind, result }` with the row `id` (use it to withdraw).                                                                          | Continue.                                                                         |
| `409`         | Revision or anchor mismatch: the transcript changed underneath you. Body: `{ error, reason, currentRevision }`. **The sequence number was consumed.** | Drop or re-anchor this correction; continue the queue.                            |
| `412`         | Sequence mismatch. Body: `{ error, expectedSeq }`.                                                                                                    | Refetch the transcript, rebase your counter, resend. Never cached by idempotency. |
| `401`         | Authentication expired.                                                                                                                               | Pause the queue, re-authenticate, resume. Never drop.                             |
| `429` / `5xx` | Transient.                                                                                                                                            | Retry the same event with backoff and the **same** `Idempotency-Key`.             |

Use the correction's client-generated UUID as the `Idempotency-Key`: replays of final outcomes return the stored response with `Idempotency-Replayed: true`; non-final outcomes (412) are never cached.

### Withdrawal

Every pending correction is deletable by its author. The `POST` response's row `id` maps to:

```text theme={null}
DELETE /v1/transcripts/{video_id}/edits/{id}        line edits
DELETE /v1/transcripts/{video_id}/speakers/{id}     speaker identifications (also removes the community appearance)
DELETE /v1/corrections/speaker-edits/{id}           speaker reassigns/splits
DELETE /v1/corrections/entity-tags/{id}             entity tags
DELETE /v1/corrections/segment-rewrites/{id}        segment rewrites
DELETE /v1/transcripts/{video_id}/merges/{id}       video-level merges
```

### Video-level entity merges

For misattributed name mentions within one video ("mentions of *Imad* in this video are Emad Mostaque"):

```json theme={null}
POST /v1/transcripts/{video_id}/merges
{ "sourceName": "Imad", "targetEntityId": 147403, "replaceWith": "Emad" }
```

`replaceWith` optionally respells the transcript text. Mentions of the same name in other videos are untouched.

### Timestamps

Segment times are pipeline-derived and repaired on every structural correction: sub-line splits interpolate at the split point, `segment_rewrite` interpolates char-proportionally between pins. To set a time explicitly, pin it: rewrite the segment with the same text and an explicit `start` (or `end`). Dependent edits should share a `seq` stream so reviewers see them together.

## Errors

| Code                     | HTTP | When                                                                                                                                      |
| ------------------------ | ---- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_api_key`        | 401  | Bad key, or session expired mid-queue.                                                                                                    |
| `quota_exceeded`         | 402  | Monthly pool exhausted with on-demand off; standard envelope, fired before any handler (no quote).                                        |
| Unlock/submit 402        | 402  | Not enough rows for this specific purchase. **Bare `{ error, quote }` body**, no envelope, no code: the refused price is in `quote`.      |
| `api_not_enabled`        | 403  | Your plan does not include API access.                                                                                                    |
| Paid-plan gate           | 403  | Transcript surfaces require a paid plan; bare `{ error }` body (no envelope code).                                                        |
| `invalid_body`           | 400  | Correction failed schema validation.                                                                                                      |
| `invalid_video_id`       | 400  | `video_id` must be the 11-character YouTube form.                                                                                         |
| `idempotency_conflict`   | 409  | Same `Idempotency-Key`, different body.                                                                                                   |
| Correction `409` / `412` | —    | Anchor/revision and sequence semantics above: `{ error, reason, currentRevision }` / `{ error, expectedSeq }`, not the standard envelope. |
| `rate_limit_exceeded`    | 429  | Honor `Retry-After`.                                                                                                                      |

## Rate limits & idempotency

Standard per-key limits (`RateLimit-*` headers). `Idempotency-Key` on every POST; for corrections, only final outcomes are cached, and a `412` never replays.

## Community Review

Transcript corrections **are** the Community Review surface for transcript content: pending-review rows, attributed to your key, 0 rows, withdrawable, reputation-building. Anything that is not transcript content (wrong entity in search, a bad mention row that led you to this video) goes through `POST /v1/feedback` with the matching type. Catalog: [Community Review](/feedback).

## Common mistakes

| Wrong                                                               | Correct                                                                                               |
| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| Expecting text when `access` is `locked`                            | Pass `?unlock=true` (quoted in `meta.quote`) or show the teaser                                       |
| `unlock=true` on `premium_pending`                                  | Nothing to unlock yet; `POST /v1/transcriptions` runs the premium generation                          |
| Busy-polling `/v1/transcriptions/{id}`                              | Sleep on `Retry-After` / `nextPollSeconds`; terminal statuses drop the header                         |
| Submitting anchored kinds without `revision` + `anchor.contentHash` | `line_edit`, `speaker_reassign`, `entity_tag`, `segment_rewrite` require both; expect 409 on mismatch |
| Retrying a 409 correction verbatim                                  | The transcript changed; re-anchor against the new revision. The seq was consumed                      |
| Treating 412 as fatal                                               | Rebase to `expectedSeq` and resend; it is a sync signal, not an error                                 |
| Empty `correctedText` to delete a line                              | Use `segment_rewrite` with `"replacements": []`                                                       |
| Expecting exact times without pins                                  | Unpinned rewrite times are interpolated; pin `start`/`end` where the exact value matters              |
| Charging users for corrections                                      | Corrections are always 0 rows                                                                         |
| SHA-256 for `anchor.contentHash`                                    | It is djb2, base-36 encoded (function above)                                                          |

## Patterns and gotchas

* Always read before you write: corrections anchor to the `meta.revision` you were served.
* Batch dependent edits (splits then edits) under one `seq` stream, strict FIFO per video.
* `speaker_identify` has visible side effects immediately (a community-flagged appearance on the person page); withdrawal removes it.
* Refunds revoke unlocks: treat `refunded` as "you have neither the transcript nor the charge."
* Quote before you buy: `meta.quote` on a free read tells you the exact row cost; check `/v1/me` for budget.
* `existing: true` on submit means an in-flight request already covers the video; poll that one.

## Complete examples

Read-unlock-correct, end to end:

```typescript TypeScript theme={null}
const BASE = 'https://api.arcmira.com';
const H = { Authorization: `Bearer ${process.env.ARCMIRA_API_KEY}` };

function djb2(input: string) {
  let hash = 5381;
  for (let i = 0; i < input.length; i++) hash = ((hash << 5) + hash + input.charCodeAt(i)) >>> 0;
  return hash.toString(36);
}

const videoId = 'dQw4w9WgXcQ';
const t = await fetch(`${BASE}/v1/transcripts/${videoId}?unlock=true`, { headers: H }).then(r => r.json());
if (t.access !== 'unlocked') throw new Error(`not unlocked: ${t.access}`);

// Fix a mis-transcription in segment 12
const seg = t.segments[12];
const res = await fetch(`${BASE}/v1/videos/${videoId}/corrections`, {
  method: 'POST',
  headers: { ...H, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID() },
  body: JSON.stringify({
    kind: 'line_edit',
    revision: t.meta.revision,
    anchor: { segmentIndex: 12, contentHash: djb2(seg.text) },
    payload: { segmentIndex: 12, originalText: seg.text, correctedText: seg.text.replace('cloud', 'Claude') },
  }),
});
if (res.status === 409) {
  // transcript changed underneath us: refetch and re-anchor
} else if (res.ok) {
  const { result } = await res.json();
  console.log('pending edit id', result.id); // DELETE /v1/transcripts/{videoId}/edits/{id} to withdraw
}
```
