Overview
Transcripts are premium, diarized, entity-annotated, and community-correctable.- Base URL:
https://api.arcmira.com, paths under/v1. - Auth:
Authorization: Bearer arc_sk_...orx-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
Minimal working example
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. |
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:| 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. |
{ "request": {...}, "existing": true? }, poll returns the object flat (no wrapper), and list returns { "requests": [...] } with an added nullable title per row.
| 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. |
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 throughPOST /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:
The six kinds, full payloads
line_edit (anchored):
speaker_reassign (anchored; sub-line splits supported; target.kind is existing, new, or role with roles like clip / other / unidentified):
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):
add_person (not anchored; proposes a person not in the index and links the speaker in one action):
entity_tag (anchored; existing entity via entityId, or propose one with proposedName + proposedType):
segment_rewrite (anchored; the structural primitive: replaces segments startIndex..endIndex inclusive with the replacements):
- 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 explicitstart. speakeris optional per replacement; omitted, it inherits from the source segment the replacement’s repaired start time falls in.startIndexmust equalanchor.segmentIndex, and thecontentHashcoversstartIndex..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:
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. |
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. ThePOST response’s row id maps to:
Video-level entity merges
For misattributed name mentions within one video (“mentions of Imad in this video are Emad Mostaque”):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 throughPOST /v1/feedback with the matching type. Catalog: Community Review.
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.revisionyou were served. - Batch dependent edits (splits then edits) under one
seqstream, strict FIFO per video. speaker_identifyhas visible side effects immediately (a community-flagged appearance on the person page); withdrawal removes it.- Refunds revoke unlocks: treat
refundedas “you have neither the transcript nor the charge.” - Quote before you buy:
meta.quoteon a free read tells you the exact row cost; check/v1/mefor budget. existing: trueon submit means an in-flight request already covers the video; poll that one.
Complete examples
Read-unlock-correct, end to end:TypeScript