Ditto Audio Reader API

Audiobooks — persist a book, render its narration in the background, stream the result

Operations 17

GET /v1/reader/audiobooks List your audiobooks #
POST /v1/reader/audiobooks Create an audiobook #
PUT /v1/reader/audiobooks/{audiobookId} Create or update an audiobook #
GET /v1/reader/audiobooks/{audiobookId} Get one audiobook #
DELETE /v1/reader/audiobooks/{audiobookId} Delete an audiobook #
POST /v1/reader/audiobooks/{audiobookId}/source Get an upload URL for the source document #
GET /v1/reader/audiobooks/{audiobookId}/source Get a download URL for the source document #
POST /v1/reader/audiobooks/{audiobookId}/source/confirm Confirm the source upload landed #
GET /v1/reader/audiobooks/{audiobookId}/progress Where the listener is #
PUT /v1/reader/audiobooks/{audiobookId}/progress Update the listening position #
POST /v1/reader/audiobooks/{audiobookId}/renders Start rendering this audiobook in the background #
GET /v1/reader/audiobooks/{audiobookId}/renders List renders for one audiobook #
GET /v1/reader/renders List your renders #
GET /v1/reader/renders/{renderId} Poll a render #
POST /v1/reader/renders/{renderId}/cancel Cancel a render #
GET /v1/reader/audiobooks/{audiobookId}/timeline The playback manifest #
GET /v1/reader/segments/{segmentId}/audio Stream one narration segment #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/ditto-audio:ditto-audio-reader-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

ditto-audio-reader-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: ditto.audio Reader API
  description: Cloud API behind ditto.audio. The reader (reader.ditto.audio) and the voice studio (studio.ditto.audio) are separate apps sharing this one backend — see /docs/reader and /docs/inference for each app’s public surface. Third-party integrations want /docs/public (rendered for humans at https://ditto.audio/docs).
  version: '1.0'
  contact: {}
servers: []
tags:
- name: Reader
  description: Audiobooks — persist a book, render its narration in the background, stream the result
paths:
  /v1/reader/audiobooks:
    get:
      description: Every audiobook in your library (the Ditto apps' books included), each with an `audio` block counting the narration stored for it in every voice (`audio.segments`; `audio.storedSegments` is the same count, kept for compatibility). Unpaginated — the library is capped.
      operationId: ReaderController_list
      parameters: []
      responses:
        '200':
          description: ''
      security:
      - bearer: []
      summary: List your audiobooks
      tags:
      - Reader
    post:
      description: 'Send an `id` to choose your own (an ISBN, your primary key); omit it and one is generated. Creating with an id that already exists updates it — identical to `PUT /v1/reader/audiobooks/{id}`. Your ids are yours alone: another account''s audiobook with the same id is a different audiobook.'
      operationId: ReaderController_create
      parameters: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpsertAudiobookDto'
      responses:
        '400':
          description: Invalid title/cover/hash.
        '409':
          description: SOURCE_ALREADY_SET — a `contentHash` other than the source the audiobook already has.
      security:
      - bearer: []
      summary: Create an audiobook
      tags:
      - Reader
  /v1/reader/audiobooks/{audiobookId}:
    put:
      description: 'Idempotent upsert keyed on the path id — safe to retry, and safe to call from several of your own devices. `title` is required; any other field you omit keeps the value it already had.


        `contentHash` names the source document. It can be set while the audiobook has no source; after that only the same hash is accepted (an audiobook narrates one text: see the source upload).'
      operationId: ReaderController_upsert
      parameters:
      - name: audiobookId
        required: true
        in: path
        description: Your id for this audiobook.
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpsertAudiobookDto'
      responses:
        '409':
          description: SOURCE_ALREADY_SET — a `contentHash` other than the source the audiobook already has.
      security:
      - bearer: []
      summary: Create or update an audiobook
      tags:
      - Reader
    get:
      operationId: ReaderController_get
      parameters:
      - name: audiobookId
        required: true
        in: path
        schema:
          type: string
      responses:
        '404':
          description: No such audiobook in your library.
      security:
      - bearer: []
      summary: Get one audiobook
      tags:
      - Reader
    delete:
      description: 'Removes the audiobook and its stored source file from your library on every device. Narration already rendered stays in storage but belongs to no audiobook any more: creating the same id again starts a new audiobook, with no narration. Its renders keep answering under your id.'
      operationId: ReaderController_remove
      parameters:
      - name: audiobookId
        required: true
        in: path
        schema:
          type: string
      responses:
        '204':
          description: Deleted.
        '404':
          description: Already gone, or never yours.
      security:
      - bearer: []
      summary: Delete an audiobook
      tags:
      - Reader
  /v1/reader/audiobooks/{audiobookId}/source:
    post:
      description: 'Two steps, because the bytes go straight to storage and never through this API: `POST` here for a presigned `uploadUrl`, `PUT` your file to it, then `POST …/source/confirm`.


        Send `contentHash` and the upload may be skipped entirely: if those exact bytes are already stored the response is `alreadyUploaded: true` and you go straight to confirm.


        Required before a render can make progress: your devices and the **Ditto cloud renderer** all read the text from this upload. Only EPUB sources can be narrated.


        An audiobook takes ONE source document: its narration is addressed by position in the book, not by the words, so narration of one text would play for another. Once it has a source (or a declared `contentHash`), only the same one is accepted, by its `contentHash`. To narrate another text, create another audiobook, or delete this one and create it again.'
      operationId: ReaderController_presignSource
      parameters:
      - name: audiobookId
        required: true
        in: path
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PresignSourceDto'
      responses:
        '403':
          description: CLOUD_BACKUP_LIMIT — the free tier stores 10 books.
        '409':
          description: SOURCE_ALREADY_SET — the audiobook already has another source document.
      security:
      - bearer: []
      summary: Get an upload URL for the source document
      tags:
      - Reader
    get:
      operationId: ReaderController_downloadSource
      parameters:
      - name: audiobookId
        required: true
        in: path
        schema:
          type: string
      responses:
        '404':
          description: No stored copy of this source.
      security:
      - bearer: []
      summary: Get a download URL for the source document
      tags:
      - Reader
  /v1/reader/audiobooks/{audiobookId}/source/confirm:
    post:
      description: Hashes the bytes you uploaded and refuses them unless they are the `contentHash` you declared, then records their real size — the size you claim is never trusted. Send the `s3Key` the presign returned. Returns the updated audiobook.
      operationId: ReaderController_confirmSource
      parameters:
      - name: audiobookId
        required: true
        in: path
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConfirmSourceDto'
      responses:
        '400':
          description: Nothing uploaded, bytes that are not the declared contentHash, or a key outside your namespace.
        '409':
          description: SOURCE_ALREADY_SET — not the key the audiobook's source is stored under.
      security:
      - bearer: []
      summary: Confirm the source upload landed
      tags:
      - Reader
  /v1/reader/audiobooks/{audiobookId}/progress:
    get:
      description: The same position the Ditto apps sync, so a listener can move between your integration and their phone mid-chapter.
      operationId: ReaderController_getProgress
      parameters:
      - name: audiobookId
        required: true
        in: path
        schema:
          type: string
      responses:
        '200':
          description: ''
      security:
      - bearer: []
      summary: Where the listener is
      tags:
      - Reader
    put:
      description: 'Conflicts resolve on `lastReadAtMs`, not arrival order: an update older than the stored one is ignored rather than applied, so a device that was offline cannot rewind a position set somewhere else.'
      operationId: ReaderController_setProgress
      parameters:
      - name: audiobookId
        required: true
        in: path
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateProgressDto'
      responses:
        '200':
          description: ''
      security:
      - bearer: []
      summary: Update the listening position
      tags:
      - Reader
  /v1/reader/audiobooks/{audiobookId}/renders:
    post:
      description: 'Returns immediately with a render you poll — no audio in this response, and nothing to hold a connection open for. Narration is produced by your render fleet: your own devices registered as render nodes, plus the Ditto cloud renderer (English text, with the English Pocket and Kokoro voices and two Piper voices; no XTTS). The source must be uploaded first.


        `chapters` is required: each chapter by its EPUB spine index, and how many segments of it to narrate as Ditto''s book reader cuts it. Segments are addressed deterministically by audiobook, chapter, segment range, voice and the book reader''s version, so segments your account already holds are not rendered again. `speed` is accepted and ignored: narration is made at 1.0.


        Poll the render for progress; each segment is in the timeline the moment it is stored.'
      operationId: ReaderController_createRender
      parameters:
      - name: audiobookId
        required: true
        in: path
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateRenderDto'
      responses:
        '403':
          description: CLOUD_BACKUP_LIMIT — narration is stored in the cloud, and the free tier stores 10 books.
        '404':
          description: No such audiobook.
        '429':
          description: 'Rate limited: 20 renders a minute per account on Free, scaled by plan (Solo 2×, Plus 5×, Pro 10×).'
      security:
      - bearer: []
      summary: Start rendering this audiobook in the background
      tags:
      - Reader
    get:
      operationId: ReaderController_listBookRenders
      parameters:
      - name: audiobookId
        required: true
        in: path
        schema:
          type: string
      - name: status
        required: false
        in: query
        schema:
          type: string
          enum:
          - planning
          - queued
          - rendering
          - completed
          - canceled
      - name: limit
        required: false
        in: query
        description: 1–200. Default 50.
        schema:
          type: number
      responses:
        '200':
          description: ''
      security:
      - bearer: []
      summary: List renders for one audiobook
      tags:
      - Reader
  /v1/reader/renders:
    get:
      description: In queue order, then newest first. `limit` 1–200, default 50.
      operationId: ReaderController_listRenders
      parameters:
      - name: status
        required: false
        in: query
        schema:
          type: string
          enum:
          - planning
          - queued
          - rendering
          - completed
          - canceled
      - name: limit
        required: false
        in: query
        description: 1–200. Default 50.
        schema:
          type: number
      responses:
        '200':
          description: ''
      security:
      - bearer: []
      summary: List your renders
      tags:
      - Reader
  /v1/reader/renders/{renderId}:
    get:
      description: '`status` is `rendering` while the render is live (every render of yours runs at once), `completed` once every segment rendered or was given up after 4 failed attempts, or `canceled` (by you, or because Ditto''s book reader changed: create it again). `progress.percent` counts rendered segments. A render at `rendering` with 0% has nothing that can take its segments yet (no device online, a voice the cloud renderer lacks, no uploaded source); it resumes by itself, and no work is lost.'
      operationId: ReaderController_getRender
      parameters:
      - name: renderId
        required: true
        in: path
        schema:
          type: string
      responses:
        '400':
          description: A render id that is not a UUID.
        '404':
          description: No such render.
      security:
      - bearer: []
      summary: Poll a render
      tags:
      - Reader
  /v1/reader/renders/{renderId}/cancel:
    post:
      description: Stops scheduling new segments. Cancelling never deletes narration that already finished.
      operationId: ReaderController_cancelRender
      parameters:
      - name: renderId
        required: true
        in: path
        schema:
          type: string
      responses:
        '400':
          description: A render id that is not a UUID.
      security:
      - bearer: []
      summary: Cancel a render
      tags:
      - Reader
  /v1/reader/audiobooks/{audiobookId}/timeline:
    get:
      description: 'Every narration segment your account has stored for this audiobook in one voice, in playback order, each with a stable `url`: what your renders made so far, by whichever renderer. This is what you hand a player.


        `segmentCount` is the segments stored, not a render''s plan (compare `progress.totalSegments` on the render). Every listed segment plays, so `available` is always true and `complete` only says the list is not empty.


        One voice per timeline on purpose — merging two would change narrator mid-chapter. Omit `voice` and the audiobook''s dominant narration is used.


        `durationMs` is null when any segment never reported its sentence layout. Narration stored by clients older than the shared chunk store is not listed; a segment URL handed out before 2026-10-28 still plays.'
      operationId: ReaderController_timeline
      parameters:
      - name: audiobookId
        required: true
        in: path
        schema:
          type: string
      - name: voice
        required: false
        in: query
        description: Which narration to assemble. Defaults to the voice with the most stored segments for this audiobook.
        schema:
          type: string
      - name: speed
        required: false
        in: query
        description: Narration is made at 1.0 and a player applies the listener's speed, so stored narration has speed 1. Defaults to the most common speed for that voice.
        schema:
          type: number
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                type: object
                properties:
                  audiobookId:
                    type: string
                  voice:
                    type:
                    - string
                    - 'null'
                  speed:
                    type:
                    - number
                    - 'null'
                  segmentCount:
                    type: integer
                  availableSegments:
                    type: integer
                  durationMs:
                    type:
                    - integer
                    - 'null'
                  complete:
                    type: boolean
                    description: Every listed segment is downloadable right now (true whenever any is listed).
                  segments:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          description: The chunk id the audio is stored under (64 hex characters).
                        index:
                          type: integer
                        chapterIndex:
                          type: integer
                        paragraphIndex:
                          type: integer
                        voice:
                          type: string
                        speed:
                          type: number
                        format:
                          type: string
                          example: wav
                        sampleRate:
                          type: integer
                          example: 24000
                        sizeBytes:
                          type: integer
                        durationMs:
                          type:
                          - integer
                          - 'null'
                        contentHash:
                          type:
                          - string
                          - 'null'
                        available:
                          type: boolean
                        url:
                          type:
                          - string
                          - 'null'
                          example: /v1/reader/segments/{id}/audio
      security:
      - bearer: []
      summary: The playback manifest
      tags:
      - Reader
  /v1/reader/segments/{segmentId}/audio:
    get:
      description: '302s to a short-lived storage URL that serves the bytes (and honours Range requests). The redirect target expires; THIS url does not, so a manifest you cached last week still plays.


        Send `?redirect=false` for `{ url }` as JSON instead — some HTTP clients replay the `Authorization` header on redirect, and storage rejects a request carrying two credentials.'
      operationId: ReaderController_segmentAudio
      parameters:
      - name: segmentId
        required: true
        in: path
        schema:
          type: string
      - name: redirect
        required: false
        in: query
        description: Set `false` to receive `{ url }` as JSON instead of a 302. Useful when your HTTP client forwards the Authorization header on redirect (storage rejects a request carrying two credentials).
        schema:
          type: boolean
      responses:
        '302':
          description: Redirect to the audio bytes.
        '400':
          description: Not a segment id.
        '404':
          description: No narration of yours with this id.
      security:
      - bearer: []
      summary: Stream one narration segment
      tags:
      - Reader
components:
  schemas:
    UpsertAudiobookDto:
      type: object
      properties:
        id:
          type: string
          description: 'Your own stable id for this audiobook (an ISBN, a uuid, your primary key). Ignored on PUT — the path wins. Omit on POST and one is generated. Ids are per account: another account''s audiobook with the same id is another audiobook.'
          example: moby-dick
        title:
          type: string
          example: Moby-Dick
        author:
          type: string
          example: Herman Melville
        format:
          type: string
          description: Source document type.
          example: epub
        chapterCount:
          type: number
          description: How many chapters the source has.
        contentHash:
          type: string
          description: sha-256 of the source file, lowercase hex. Enables upload dedup. Set while the audiobook has no source; after that only the same hash is accepted (409 SOURCE_ALREADY_SET).
        coverImageUrl:
          type: string
          description: https:// or data:image/ URL.
      required:
      - title
    ConfirmSourceDto:
      type: object
      properties:
        s3Key:
          type: string
          description: The `s3Key` the presign response returned.
        contentHash:
          type: string
      required:
      - s3Key
    RenderChapterDto:
      type: object
      properties:
        chapterIndex:
          type: number
          description: 'The chapter''s EPUB spine index: its position among all the spine''s itemrefs, from 0.'
        paragraphCount:
          type: number
          description: How many segments of this chapter to narrate, counted the way Ditto's book reader cuts the chapter (runs of about 150-350 characters). A range the reader cannot find in the uploaded EPUB is refused by every renderer.
      required:
      - chapterIndex
      - paragraphCount
    CreateRenderDto:
      type: object
      properties:
        voice:
          type: string
          description: A voice id from `GET /v1/audio/voices`.
          example: kokoro-af-heart
        speed:
          type: number
          description: 'Accepted and ignored: narration is made at 1.0, and a player applies the listener''s speed.'
          example: 1
        chapters:
          description: 'The chapters to narrate and their segment counts. Required: the server does not parse your source document, so it cannot invent this.'
          type: array
          items:
            $ref: '#/components/schemas/RenderChapterDto'
      required:
      - voice
      - chapters
    UpdateProgressDto:
      type: object
      properties:
        chapterIndex:
          type: number
        paragraphIndex:
          type: number
          description: Segment within the chapter.
        cfi:
          type: string
          description: An epub.js CFI, if you have one.
        percentComplete:
          type: number
          description: 0..1
        totalReadingTimeMs:
          type: number
          description: Total listening/reading time, ms.
        lastReadAtMs:
          type: number
          description: Client clock, ms epoch. Last-write-wins is resolved on THIS, not on arrival order — an update older than the stored one is ignored, so a device coming back online cannot rewind a position set elsewhere.
    PresignSourceDto:
      type: object
      properties:
        fileName:
          type: string
          example: moby-dick.epub
        contentType:
          type: string
          example: application/epub+zip
        contentHash:
          type: string
          description: 'sha-256 of the file, lowercase hex. When the bytes are already in cloud storage the response is `alreadyUploaded: true` and you skip the PUT. Required once the audiobook has a stored source: only the same hash is accepted.'
      required:
      - fileName
  securitySchemes:
    bearer:
      scheme: bearer
      bearerFormat: ditto_sk_live_… API key, ditto_oat_… OAuth access token, or a session JWT
      type: http
      description: An API key (`ditto_sk_live_…`, made at https://ditto.audio/account/api-keys) authenticates /v1/audio/* and /v1/reader/*; an OAuth connection's one-hour access token (`ditto_oat_…`) holds the audio:read and audio:speech scopes only. Account routes (/v1/api-keys, /v1/usage, /v1/subscription) take the signed-in session's token (a JWT) only.