> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://onepin.ai/docs/reference/api-reference/workflows/get-run-data-api-v-1-workflows-workflow-id-runs-run-id-data-get/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://onepin.ai/_mcp/server. # Get Run Data GET https://api.onepin.ai/api/v1/workflows/{workflow_id}/runs/{run_id}/data Paginated script-and-audio data rows for a completed workflow run. Returns grouped rows where each row represents one source script line. Within each row, `cards` contain the per-language audio outputs, per-card validation scores (word accuracy, naturalness), and short-lived audio `playback_url` values (valid for 15 minutes). **Filtering:** - `search` narrows which rows are returned based on their source script text. - `language` narrows the `cards` list within each returned row to a single locale. Rows with no matching cards are still returned (with empty `cards`), and `pagination.total` always reflects the search-filtered row count regardless of `language`. - `include_dropped=true` adds rejected attempts to `cards` with `status="dropped"`; the default response remains delivered/generated data only. **Pagination:** `pagination.total` is scoped to the `search` filter only. Response includes a `partial` field indicating whether any data is still being computed (e.g. audio not yet generated, validation not yet scored). This endpoint sets `Cache-Control: no-store` because playback URLs are short-lived and data may change while a run is still in progress. Reference: https://onepin.ai/docs/reference/api-reference/workflows/get-run-data-api-v-1-workflows-workflow-id-runs-run-id-data-get ## Authentication - `Authorization` header (bearer token, required) — Clerk JWT token - `Authorization` header (bearer token, required) — Onepin live API key (`op_live_...`). Test and public keys are reserved in Phase 1. ## Servers - `https://api.onepin.ai` (prod, default) - `https://dev-api.onepin.ai` (dev) ## Request ### Path parameters - `workflow_id` (string, required) - `run_id` (string, required) ### Query parameters - `search` (string, optional, nullable) — Case-insensitive search over the source/script text of each row. - `language` (string, optional, nullable) — Exact full-locale code to filter cards within each row (e.g. `en-US`). `_` is normalized to `-`. Filtering is card-level only — rows remain visible even when all their cards are filtered out, and `pagination.total` is unaffected. - `include_dropped` (boolean, optional, default: false) — Include validator-rejected audio cards reconstructed from unwired fail ports. Defaults to false so existing clients continue receiving delivered output only. - `offset` (integer, optional, default: 0) — Zero-based pagination offset. - `limit` (integer, optional, default: 20) — Maximum rows to return (1–100). ### Headers - `X-Workspace-Id` (string, optional, nullable) ## Response ### 200 Successful Response - `data` (WorkflowRunDataOut, required) - `meta` (Meta, required) - `pagination` (CountedPaginationMeta, required) — PaginationMeta variant for endpoints that compute an unpaginated total. ## Errors ### 422 Unprocessable Entity Error Validation Error - `detail` (list of ValidationError, optional) ## Types ### WorkflowRunDataOut - `workflow_id` (string, required) - `run_id` (string, required) - `run_status` (string, required) - `partial` (WorkflowRunDataPartialOut, required) - `rows` (list of WorkflowRunDataRowOut, optional) - `dropped_truncated` (boolean, optional, default: false) ### Meta - `request_id` (string, required) - `timestamp` (datetime, required) ### CountedPaginationMeta PaginationMeta variant for endpoints that compute an unpaginated total. - `limit` (integer, required) - `total` (integer, required) - `next` (string, optional, nullable) - `prev` (string, optional, nullable) ### ValidationError - `loc` (list of ValidationErrorLocItems, required) - `msg` (string, required) - `type` (string, required) - `input` (any, optional) - `ctx` (ValidationErrorCtx, optional) ### WorkflowRunDataPartialOut - `status` (enum, required) - Allowed values: `available`, `not_ready`, `unsupported`, `unavailable` - `reason` (string, optional, nullable) - `source` (string, optional, nullable) ### WorkflowRunDataRowOut - `id` (string, required) - `script_status` (enum, required) - Allowed values: `available`, `not_ready`, `unsupported`, `unavailable` - `line_index` (integer, optional, nullable) - `line_id` (string, optional, nullable) - `source_line_id` (string, optional, nullable) — Source line identity for unresolved translated rows. Null when the grouped source line has been merged; card-level source_line_id preserves translation provenance. - `script` (string, optional, nullable) - `script_reason` (string, optional, nullable) - `auto_corrected` (integer, optional, nullable) - `auto_corrected_status` (enum, optional, default: unavailable) - Allowed values: `available`, `not_ready`, `unsupported`, `unavailable` - `auto_corrected_reason` (string, optional, nullable, default: auto_correction_count_unavailable) - `cards` (list of WorkflowRunDataCardOut, optional) ### ValidationErrorLocItems ### ValidationErrorCtx ### WorkflowRunDataCardOut - `id` (string, required) - `audio` (WorkflowRunDataAudioOut, required) - `voice` (WorkflowRunDataVoiceOut, required) - `line_id` (string, optional, nullable) - `source_line_id` (string, optional, nullable) - `line_index` (integer, optional, nullable) - `locale_code` (string, optional, nullable) - `script` (string, optional, nullable) - `validations` (list of WorkflowRunDataValidationOut, optional) - `normalized` (boolean, optional, default: false) - `waveform_url` (string, optional, nullable) - `waveform_status` (enum, optional, default: unsupported) - Allowed values: `available`, `not_ready`, `unsupported`, `unavailable` - `waveform_reason` (string, optional, nullable, default: waveform_not_generated) - `retry_count` (integer, optional, nullable) - `status` (enum, optional, default: delivered) - Allowed values: `delivered`, `generated`, `not_delivered`, `dropped` - `dropped` (WorkflowRunDataDroppedOut, optional, nullable) - `rejected` (WorkflowRunDataRejectedOut, optional, nullable) — Per-take validator failures, independent of delivery disposition. ### WorkflowRunDataAudioOut - `status` (enum, required) - Allowed values: `available`, `not_ready`, `unsupported`, `unavailable` - `playback_url` (string, optional, nullable) - `reason` (string, optional, nullable) - `duration_ms` (integer, optional, nullable) - `content_type` (string, optional, nullable) - `provider` (string, optional, nullable) - `model` (string, optional, nullable) ### WorkflowRunDataVoiceOut - `status` (enum, required) - Allowed values: `available`, `not_ready`, `unsupported`, `unavailable` - `display_name` (string, optional, nullable) - `reason` (string, optional, nullable) ### WorkflowRunDataValidationOut Per-validator scoring entry for a single line (POD-599). WER entries populate ``wer``/``cer``/``transcript``; naturalness entries populate ``score`` (0-100). All fields except ``kind`` and ``status`` are optional so future validator kinds can omit inapplicable fields. ``scored_on`` records which error metric actually drove ``score`` for the word-accuracy (WER) kind: ``"wer"`` for space-delimited scripts, ``"cer"`` for space-less scripts (ja/zh/th/…) where word-level WER degenerates. It is ``None`` for kinds that don't score on an error rate (e.g. naturalness). ``segments`` is populated on the two highlight-capable kinds (``status == "available"``): the per-unit spans over the card's ``script`` used to highlight it word-by-word. - word-accuracy (``kind == "wer"``): ``word_error`` spans over the delivered take's substituted/deleted units, each carrying which of the two it was in ``error_type``. An empty list means the take WAS scored and no reference unit was counted wrong (a perfect pass, or a rate driven only by insertions with no reference word behind them) — a client renders plain script and must NOT re-align (the empty list is the authoritative "no errors" signal that prevents inventing errors that contradict the score). - pronunciation (``kind == "pronunciation"``): ``pronunciation_error`` + ``corrected`` spans. Pronunciation spans are published on EVERY take the detector scored — each retry card carries the words its own attempt got wrong, so a take history can be read attempt by attempt rather than only at the winner. Word-accuracy spans remain delivered-take-only on purpose: word accuracy is a score-only signal and clients do not paint ``word_error``. A corrector take's card describes the PED→PEC event of that regeneration: the verdict its splice ANSWERED (the newest one recorded before it), with the words its ``correction_metadata`` repaired painted ``corrected`` and the rest of that verdict's flags left ``pronunciation_error``. This holds whether or not a downstream PED re-graded the splice — the re-run's verdict is deliberately NOT painted on the take it measured; it prompts the next splice and renders as the NEXT card's errors, so a repair that did not land shows ``corrected`` here and flagged again one card later. The final take's re-check, which prompted nothing, is unrendered. The card's ``score`` stays the take's own measurement. ``corrected`` is scoped to the take that actually carries the repair: a later regeneration delivers audio that was never spliced and does not inherit it. A split (over-cap) line's merged clip aggregates its pieces' scores and drops their subline-local ``mispronounced``, so it is resolved from the per-piece takes that clip spliced. All of this applies per take, so a retry history reads correctly attempt by attempt and not only at the winner. ``None`` vs ``[]`` is load-bearing and a client must not collapse them. ``[]`` means the take WAS scored and nothing was flagged. ``None`` means no per-word detail is available, which happens when: the take was not scored by that validator; its script is unavailable; a multi-generator fan-in row delivers more than one card and the line-level verdict can't be attributed to a single one (those scored delivered cards carry a ``score`` with ``segments == None``); the verdict named words that none of the published script's tokens matched (a repair whose recorded position holds a different word is withheld the same way); a merged split-line clip had any piece it could not resolve or that was never scored (``[]`` vouches for the WHOLE parent script, so it requires every piece); or the spans failed the ``script[start:end] == text`` self-check against the script the card actually publishes (``display_script`` can stand in for the line script). Never re-derive spans client-side from ``None`` — that reintroduces the false errors the field exists to prevent. Additive/optional otherwise. ``error_counts`` breaks the word-accuracy rate into its substitution / insertion / deletion parts (see :class:`WorkflowRunDataErrorCountsOut`) — the "what kind of error" that ``wer``/``cer`` alone cannot answer. Present on ``kind == "wer"`` entries whose stored envelope carries it; ``None`` for every other kind, and for an envelope that predates the field (older runs, and split lines whose sublines were scored before it). Also additive/optional. - `kind` (string, required) - `status` (enum, required) - Allowed values: `available`, `not_ready`, `unsupported`, `unavailable` - `label` (string, optional, nullable) - `score` (double, optional, nullable) - `reason` (string, optional, nullable) - `wer` (double, optional, nullable) - `cer` (double, optional, nullable) - `transcript` (string, optional, nullable) - `scored_on` (string, optional, nullable) - `segments` (list of WorkflowRunDataSegmentOut, optional, nullable) - `error_counts` (WorkflowRunDataErrorCountsOut, optional, nullable) — The three edit operations a word-accuracy (``kind == "wer"``) rate is the sum of. A rate alone doesn't say what went wrong. The same 0.25 can be three deletions (the take dropped scripted words), three insertions (words heard that the script never had), or three substitutions (the mispronunciation case) — three different fixes. Counted in the units of the metric that actually scored the take (``scored_on``): words for ``"wer"``, characters for ``"cer"``. ``reference_units`` is the denominator those counts were divided by, so ``(substitutions + insertions + deletions) / reference_units`` reproduces the entry's ``wer``/``cer`` exactly and a client can render "3 of 24 words" without re-deriving the reference length (which it cannot: the denominator counts the CANONICALIZED reference, not the displayed ``script``). These are NOT a summary of ``segments``: the counts come from the error-rate alignment, the highlight spans from a separate diff alignment, so ``substitutions + deletions`` may differ from the span count by a unit or two on a badly-mangled take, and tallying ``segments[].error_type`` will not always reproduce these numbers. Use the counts for the line's totals — the ones the rate is actually the sum of, and the only place insertions are reported at all — and the spans for which word failed and how. - `inherited_from_take` (integer, optional, nullable) — 1-based attempt number (retry_count + 1, generator-local — the same basis as the take chips) of the take this entry was actually measured on, set only when it differs from the card's own take. The winner is picked with validator verdicts carried forward onto corrector splices, so the delivered card can carry a score no validator re-measured on its audio — this names where the measurement came from instead of letting it read as a fresh one. None everywhere else, including when the source take's own attempt number is unknowable. ### WorkflowRunDataDroppedOut - `verdicts` (list of WorkflowRunDataDroppedVerdictOut, optional) ### WorkflowRunDataRejectedOut Per-take validator failures, independent of delivery disposition. - `verdicts` (list of WorkflowRunDataDroppedVerdictOut, optional) ### WorkflowRunDataSegmentOut One highlighted span of a take's ``script`` from a highlight-capable validator. Shared by the word-accuracy (``kind == "wer"``) and pronunciation (``kind == "pronunciation"``) validators — one shape, two producers: - ``word_error`` — a reference word (WER) or character (CER) the take got wrong (substituted or deleted vs the STT transcript). Word-accuracy only. - ``pronunciation_error`` — a word the pronunciation error detector heard mispronounced and the corrector left uncorrected. Pronunciation only. - ``corrected`` — a word the pronunciation corrector re-synthesized. Pronunciation only. ``start``/``end`` are Unicode **codepoint** offsets (half-open ``[start, end)``) into the card's ``script`` — Python ``str`` indexing, so a JS client must slice via ``Array.from(script)`` (UTF-16 code units drift on astral/emoji). ``text`` is the script slice (``script[start:end]``) so a client can validate/fall back if its offset math disagrees. Only wrong/corrected units are emitted; ok text is the uncovered gaps. CER spans may be single characters — a client merges touching same-status spans. ``error_type`` says WHICH edit operation this one span was, on ``word_error`` spans: - ``substitution`` — the take said something else here. This is the mispronunciation case, and the only one a phoneme/pronunciation fix addresses. - ``deletion`` — the take dropped this unit; nothing was heard where it belongs. A regeneration or a text split, not a pronunciation fix. Two failures that a single ``word_error`` cannot tell apart, and that ``error_counts`` can only total per line — this is the per-word answer to "which word, what kind". Insertions never appear: an extra heard unit has no reference span in ``script`` to highlight, so it is counted (``error_counts.insertions``) and not painted. ``None`` on ``pronunciation_error``/``corrected`` spans (not an edit operation), and on ``word_error`` spans from a run scored before the validator recorded the type — never guessed, so absence stays absence. ``status`` is deliberately unchanged, so a client that ignores this field renders exactly as before. - `text` (string, required) - `status` (enum, required) - Allowed values: `word_error`, `pronunciation_error`, `corrected` - `start` (integer, required) - `end` (integer, required) - `severity` (enum, optional, nullable) - Allowed values: `critical`, `warning`, `minor` - `error_type` (enum, optional, nullable) - Allowed values: `substitution`, `deletion` - `advisory` (boolean, optional, nullable) ### WorkflowRunDataErrorCountsOut The three edit operations a word-accuracy (``kind == "wer"``) rate is the sum of. A rate alone doesn't say what went wrong. The same 0.25 can be three deletions (the take dropped scripted words), three insertions (words heard that the script never had), or three substitutions (the mispronunciation case) — three different fixes. Counted in the units of the metric that actually scored the take (``scored_on``): words for ``"wer"``, characters for ``"cer"``. ``reference_units`` is the denominator those counts were divided by, so ``(substitutions + insertions + deletions) / reference_units`` reproduces the entry's ``wer``/``cer`` exactly and a client can render "3 of 24 words" without re-deriving the reference length (which it cannot: the denominator counts the CANONICALIZED reference, not the displayed ``script``). These are NOT a summary of ``segments``: the counts come from the error-rate alignment, the highlight spans from a separate diff alignment, so ``substitutions + deletions`` may differ from the span count by a unit or two on a badly-mangled take, and tallying ``segments[].error_type`` will not always reproduce these numbers. Use the counts for the line's totals — the ones the rate is actually the sum of, and the only place insertions are reported at all — and the spans for which word failed and how. - `substitutions` (integer, required) - `insertions` (integer, required) - `deletions` (integer, required) - `reference_units` (integer, required) ### WorkflowRunDataDroppedVerdictOut One failing validator's verdict on a dropped take. A take can fail several parallel validators at once; ``validator_node_id`` disambiguates two validators of the same kind (e.g. two WER nodes with different thresholds). ``threshold`` is the pass bar that actually gated the line (schema defaults resolved); null only for invalid snapshots. - `validator_node_id` (string, required) - `validator_kind` (string, required) - `validator_label` (string, required) - `score` (double, optional, nullable) - `threshold` (double, optional, nullable) ## Examples **Response** ```json { "data": { "workflow_id": "string", "run_id": "string", "run_status": "string", "partial": { "status": "available", "reason": "string", "source": "string" }, "rows": [ { "id": "string", "script_status": "available", "line_index": 1, "line_id": "string", "source_line_id": "string", "script": "string", "script_reason": "string", "auto_corrected": 1, "auto_corrected_status": "unavailable", "auto_corrected_reason": "string", "cards": [ { "id": "string", "audio": { "status": "available", "playback_url": "string", "reason": "string", "duration_ms": 1, "content_type": "string", "provider": "string", "model": "string" }, "voice": { "status": "available", "display_name": "string", "reason": "string" }, "line_id": "string", "source_line_id": "string", "line_index": 1, "locale_code": "string", "script": "string", "validations": [ { "kind": "string", "status": "available", "label": "string", "score": 1.1, "reason": "string", "wer": 1.1, "cer": 1.1, "transcript": "string", "scored_on": "string", "segments": [ { "text": "string", "status": "word_error", "start": 1, "end": 1, "severity": "critical", "error_type": "substitution", "advisory": true } ], "error_counts": { "substitutions": 1, "insertions": 1, "deletions": 1, "reference_units": 1 }, "inherited_from_take": 1 } ], "normalized": false, "waveform_url": "string", "waveform_status": "unsupported", "waveform_reason": "string", "retry_count": 1, "status": "delivered", "dropped": { "verdicts": [ { "validator_node_id": "string", "validator_kind": "string", "validator_label": "string", "score": 1.1, "threshold": 1.1 } ] }, "rejected": { "verdicts": [ { "validator_node_id": "string", "validator_kind": "string", "validator_label": "string", "score": 1.1, "threshold": 1.1 } ] } } ] } ], "dropped_truncated": false }, "meta": { "request_id": "string", "timestamp": "2024-01-15T09:30:00Z" }, "pagination": { "limit": 1, "total": 1, "next": "string", "prev": "string" } } ``` **SDK Code** ```python import requests url = "https://api.onepin.ai/api/v1/workflows/workflow_id/runs/run_id/data" headers = {"Authorization": "Bearer "} response = requests.get(url, headers=headers) print(response.json()) ``` ```javascript const url = 'https://api.onepin.ai/api/v1/workflows/workflow_id/runs/run_id/data'; const options = {method: 'GET', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ```go package main import ( "fmt" "net/http" "io" ) func main() { url := "https://api.onepin.ai/api/v1/workflows/workflow_id/runs/run_id/data" req, _ := http.NewRequest("GET", url, nil) req.Header.Add("Authorization", "Bearer ") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(res) fmt.Println(string(body)) } ``` ```ruby require 'uri' require 'net/http' url = URI("https://api.onepin.ai/api/v1/workflows/workflow_id/runs/run_id/data") http = Net::HTTP.new(url.host, url.port) http.use_ssl = true request = Net::HTTP::Get.new(url) request["Authorization"] = 'Bearer ' response = http.request(request) puts response.read_body ``` ```java import com.mashape.unirest.http.HttpResponse; import com.mashape.unirest.http.Unirest; HttpResponse response = Unirest.get("https://api.onepin.ai/api/v1/workflows/workflow_id/runs/run_id/data") .header("Authorization", "Bearer ") .asString(); ``` ```php request('GET', 'https://api.onepin.ai/api/v1/workflows/workflow_id/runs/run_id/data', [ 'headers' => [ 'Authorization' => 'Bearer ', ], ]); echo $response->getBody(); ``` ```csharp using RestSharp; var client = new RestClient("https://api.onepin.ai/api/v1/workflows/workflow_id/runs/run_id/data"); var request = new RestRequest(Method.GET); request.AddHeader("Authorization", "Bearer "); IRestResponse response = client.Execute(request); ``` ```swift import Foundation let headers = ["Authorization": "Bearer "] let request = NSMutableURLRequest(url: NSURL(string: "https://api.onepin.ai/api/v1/workflows/workflow_id/runs/run_id/data")! as URL, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10.0) request.httpMethod = "GET" request.allHTTPHeaderFields = headers let session = URLSession.shared let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in if (error != nil) { print(error as Any) } else { let httpResponse = response as? HTTPURLResponse print(httpResponse) } }) dataTask.resume() ``` > Onepin is a voice workflow platform that orchestrates, validates, and ships production-ready audio across 33 TTS models.