---
title: "Durably render a table read artifact as an MP3 or MP4."
method: POST
path: "/artifacts/{artifactId}/finalize"
tags: ["Artifacts"]
---

# Durably render a table read artifact as an MP3 or MP4.

`POST /artifacts/{artifactId}/finalize`

## Request

- Base URL: `https://docs.sleeperhit.studio/api/v1`
- URL: `https://docs.sleeperhit.studio/api/v1/artifacts/{artifactId}/finalize`
- Auth: HTTP bearer

Initiates a durable render of a `table_read` artifact alongside the live share.  `mode: "audio"` (default, free) queues a durable full-mix MP3 render (voices, adaptive soundtrack, and SFX) regardless of whether a live Pipecat/Daily recording exists. When a completed recording is present, the worker freezes that take as the voice layer; otherwise it synthesizes the voice track headlessly from TTS using the same producer the MP4 export uses. Poll GET /artifacts/{id}: `manifest.audio.finalize.status` progresses through `queued → rendering → uploading → complete` (or `failed` with `manifest.audio.finalize.error`). On `complete`, `manifest.audio.recordingUrl` (full-mix MP3 download URL) and `manifest.audio.recordingDurationMs` are populated.  `mode: "video"` (free) queues the Remotion MP4 export (same render as POST /artifacts/{id}/render-video). Poll GET /artifacts/{id}: `manifest.video.status` and `manifest.video.url` on completion.  Replay-safe: re-calling finalize while in-flight, or when complete and the recording still carries the read's current mix, returns the current status without re-charging credits (`finalize.mix: "current"`). A complete recording that the read has moved past since — its music, sound cues or voice track changed, in the app or through this API — is rendered again (free) under a new media revision; the old `manifest.audio.recordingUrl` keeps serving until the new render completes. A complete recording made before its mix was tracked replays with `finalize.mix: "unknown"`; pass `refresh: true` to render it again from the read as it plays now (the app's "Update the recording"). A recording that is current still replays free with `refresh: true`.  A finalize that would SPEAK a voice its provider has retired is refused before anything is charged or queued (409 `voice_retired`, `details.voices`): migrate those voices (POST /voice-migrations), or re-voice the character, then finalize again. A read whose voices-only track is current is mixed from it and speaks nothing, so it is never refused for its voices.  A read whose table read is not paid for yet — a Writing Lab read that was cast but never prepared — is refused before anything is charged or queued (409 `table_read_not_prepared`): prepare it (POST /scripts/{scriptUploadId}/table-read/prepare), then finalize. A read a Story API job made is paid by that job.  Requires `Idempotency-Key`. Scope: `artifact:publish` (billable + mutating).

## Headers

- `Idempotency-Key` string, required

## Request body

- object
  - `mode` 'audio' | 'video': `audio` (default) queues a durable full-mix MP3 (voices + adaptive soundtrack + SFX); the worker uses a completed Pipecat/Daily recording when present, otherwise synthesizes the voice track from TTS. `video` queues the Remotion MP4 export.
  - `refresh` boolean: Audio only. `true` also re-renders a complete recording whose mix is unknown (`finalize.mix: "unknown"`, made before mixes were tracked). A recording already carrying the read's current mix replays free either way; one the read moved past re-renders without it.

## Response `200`

Finalize accepted. For audio, poll GET /artifacts/{id} for `manifest.audio.finalize.status`. For video, poll `manifest.video.status`.

- object
  - `artifactId` string, required
  - `mode` 'audio' | 'video', required
  - `finalize` object, required: Current finalize state at the time of the call. For audio: `{ status }`, plus `recordingUrl` and `mix` (`current` | `unknown`) when a complete recording was returned. For video: render acknowledgement object.

## Other responses

- `400`: Request payload failed validation. The error message names the offending field.
- `401`: Missing, malformed, revoked, or expired API key.
- `402`: Not enough Studio Credits to reserve this job (`insufficient_credits`), and nothing was reserved. `details` is InsufficientCreditsDetails: `required` and `available`, and `autoTopup` when auto top-up is on but did not cover it — `pending` (retry in a moment), `over_cap` (raise the monthly spending cap), `too_small` (buy a bigger pack) or `failed`.
- `403`: API key is valid but lacks the scope, is blocked by IP allowlist, or the account is disabled.
- `404`: The story artifact does not exist or is owned by another account.
- `409`: The artifact is not in a state where this operation is valid yet (e.g. requesting an MP4 render before the share link exists).
- `429`: Rate limit for this API key + IP + route family was exceeded.
- `500`: The API failed unexpectedly. Safe to retry with the same Idempotency-Key.

---

[API](https://skmtc.dev/sleeperhit/apis/sleeper-hit-studio-story-api.md) · [All operations](https://skmtc.dev/sleeperhit/apis/sleeper-hit-studio-story-api/llms.txt) · [OpenAPI document](https://skmtc.dev/sleeperhit/apis/sleeper-hit-studio-story-api/revisions/86e084144cef?raw)
