---
title: "Migrate a voice to a replacement, everywhere it is used."
method: POST
path: "/voice-migrations"
tags: ["Voices"]
---

# Migrate a voice to a replacement, everywhere it is used.

`POST /voice-migrations`

## Request

- Base URL: `https://docs.sleeperhit.studio/api/v1`
- URL: `https://docs.sleeperhit.studio/api/v1/voice-migrations`
- Auth: HTTP bearer

Re-points every use of `fromVoiceId` to `toVoiceId`: each cast canon person pinned to it (which is also a recurring show's pinned voice, and the voice every new episode, deck, trailer and showcase speaks with), the narrator pinned on a trailer or pitch deck, and the voice map of every read with no finished recording yet. `projectId` (and `character`) narrow it to one project (and one person, under any name the canon knows them by). A read with a finished recording keeps it — published audio stays as listeners heard it — and is listed in `skipped`: re-voice its characters (POST /artifacts/{artifactId}/revoice, priced) to finalize it again on the new voice. Free, and TWO CALLS: without `confirmed` it answers `applied: false` with what would change and writes nothing; with `confirmed: true` it re-points and records the migration (`migrationId`), which POST /voice-migrations/{migrationId}/undo reverses. The replacement must be on a provider that is neither retiring nor retired (409 `voice_retired`).  Requires `Idempotency-Key`. Scope: `story:write`.

## Headers

- `Idempotency-Key` string, required

## Request body

- VoiceMigrationRequest
  - `fromVoiceId` string, required: The voice being replaced: its provider voice id (or library `id`).
  - `toVoiceId` string, required: The replacement: one of your library voices (GET /voices), or a catalog voice with `toProvider`.
  - `toProvider` 'cartesia' | 'elevenlabs' | 'openai' | 'deepgram' | 'hume': The replacement's provider, for a catalog voice. A library voice knows its own; otherwise the configured table-read provider.
  - `toVoiceName` string: The name the canon and reads show; the library voice's own name otherwise.
  - `projectId` string: Only this project's uses. Omitted: every use you have.
  - `character` string: Only this character of `projectId`, under any name the canon knows them by.
  - `confirmed` boolean: true migrates and records it. Omitted: answers with what would change and changes nothing.

## Response `200`

The plan, or the recorded migration.

- VoiceMigrationEnvelope: Without `confirmed`: `applied: false` and what would change (nothing was written). With `confirmed: true`: `applied: true` and the recorded migration (`migrationId`, `createdAt`, `undoneAt`, `undo`).
  - `applied` boolean, required
  - `migrationId` string
  - `createdAt` string, date-time
  - `from` VoiceMigrationVoice, required
    - `voiceId` string, required
    - `voiceName` string, nullable, required
    - `provider` string, nullable, required
    - `retiresAt` string, date-time, nullable, required: When the voice's provider retires its voices, if it is retiring.
  - `to` VoiceMigrationVoice, required
    - `voiceId` string, required
    - `voiceName` string, nullable, required
    - `provider` string, nullable, required
    - `retiresAt` string, date-time, nullable, required: When the voice's provider retires its voices, if it is retiring.
  - `scope` object, required
    - `projectId` string, nullable
    - `character` string, nullable
  - `changes` VoiceMigrationChange[], required
    - `kind` 'canon' | 'read' | 'trailer' | 'pitch_deck', required
    - `projectId` string
    - `projectName` string
    - `jobId` string
    - `artifactId` string, nullable
    - `title` string, nullable
    - `character` string
    - `previous` object, required
    - `next` object, required
  - `skipped` VoiceMigrationSkippedRead[], required
    - `jobId` string, required
    - `artifactId` string, nullable, required
    - `title` string, nullable, required
    - `characters` string[], required
    - `reason` string, required
  - `undoneAt` string, date-time, nullable
  - `note` string, required

## Other responses

- `400`: Request payload failed validation. The error message names the offending field.
- `401`: Missing, malformed, revoked, or expired API key.
- `403`: API key is valid but lacks the scope, is blocked by IP allowlist, or the account is disabled.
- `404`: The named project or source does not exist or is owned by another account.
- `409`: A voice its provider has retired is in the way (`voice_retired`), so nothing was queued, charged or changed. `details.voices` names each one; migrate them (POST /voice-migrations), then retry.
- `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)
