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: 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.