01What you need
- A Readerr account. Start a free trial first if you do not have one.
- A site added in the customer panel. Open Sites and click Add site, type your domain and copy the API key. The key is shown once. It belongs to that one site, so every article you send with it is counted for that site. How to install the WordPress plugin shows the same screen. You do not need WordPress for the API.
- The address of the API:
https://api.readerr.app. Every request carries your key in this header:
Authorization: Bearer YOUR_KEY
Keep the key on your server. Never put it in a page that readers load. In the examples below it is the shell variable $READERR_KEY.
02Quick start
Send the article
Call
POST /v1/ingestwhen you publish an article, and again when you change it. The body is JSON. Five fields are required.curl -X POST https://api.readerr.app/v1/ingest \ -H "Authorization: Bearer $READERR_KEY" \ -H "Content-Type: application/json" \ -d '{ "external_id": "article-4812", "lang": "en", "title": "Council approves new bus stops", "url": "https://example.com/news/bus-stops", "content_html": "<p>The council voted on Tuesday to add twelve new bus stops...</p>" }'external_idis your own id for the article, for example the id it has in your CMS. It only has to be unique within your site: the key tells Readerr which site is calling, so another site using the same id has a separate article. Send the sameexternal_idagain and you update that article.The answer is
200with a small JSON body:{ "client_id": "example-news", "article_id": 90317, "state": "queued" }queuedmeans audio is being made.unchangedmeans you sent the same article as before, and nothing new is made.article_idis Readerr's own number for the article. You don't need it: every other call uses yourexternal_id.Ask for the audio
Audio takes a few minutes. Ask for the article by the
external_idyou sent:curl https://api.readerr.app/v1/articles/article-4812 \ -H "Authorization: Bearer $READERR_KEY"While the audio is on its way,
audioisnull:{ "external_id": "article-4812", "state": "running", "audio": null }When
stateisdone,audioholds the file:{ "external_id": "article-4812", "state": "done", "audio": { "path": "/example-news/01JC8Z3Q7V5N2K4M6P8R0T1W2X.mp3", "voice": "F4", "duration_ms": 176000 } }The audio file lives at
https://cdn.readerr.appfollowed by thepath, herehttps://cdn.readerr.app/example-news/01JC8Z3Q7V5N2K4M6P8R0T1W2X.mp3. Ask again after a minute, then less often. Asking does not count against the limit on sending articles.Show the player
Put the Readerr player on the article page, with the
pathfrom the answer. The full markup is in the section "Show the player on your page" below.
03Send an article
POST /v1/ingest takes these fields. Anything not listed is ignored.
| Field | Required | What it is |
|---|---|---|
external_id | Yes | Your own id for the article, unique within your site. You use it again to ask for the article, to update it and to delete it. Use letters, digits, hyphens and underscores, up to 64 characters: that is the shape the player needs to report plays, and an id with a slash cannot be used in the address of the other calls. Longer ids, up to 200 characters, are accepted but break those two things. |
lang | Yes | The language of the article: pl, de, fr or en. Any other value is refused. |
title | Yes | The headline, up to 1,000 characters. It is not read aloud, but a changed title counts as a change to the article. |
url | Yes | The address of the article page, up to 2,048 characters. The API requires it. It does not change the audio. |
content_html | Yes | The article body as HTML, up to 1,000,000 characters. Readerr picks out the text that is read. |
voice | No | A voice id such as F4 or M3. Leave it out and the article gets your site's default voice, which you set in the customer panel. Up to 64 characters of letters, digits and : _ . -. GET /v1/voices lists the ids. |
client_id | No | Leave it out. Your key decides which site the article belongs to, and a value here is ignored. |
What is read. Paragraphs, headings, lists and quotes. Navigation, headers and footers, side boxes, forms, embedded frames, scripts, styles and figures with their captions are left out. Elements whose class or id looks like a related-articles box, a newsletter box, an ad, a gallery or a breadcrumb are left out too. To skip any other element, give it the class readerr-skip, or the attribute data-readerr-skip. WordPress block markup is read block by block: text blocks are read, and everything else is skipped.
When new audio is made. Readerr compares the language, the title, the text and the voice with the last version it received. If any of them differs, the API makes new audio, and that uses the article's characters in full again. If nothing differs, you get "state": "unchanged" and no characters are used. An article whose last attempt failed, or stopped for lack of characters, is queued again when you send it again, even if it has not changed. Characters and limits explains what counts.
Size. The text Readerr reads from your HTML can be up to 40,000 characters. A longer article is refused with 413 and nothing is stored. Split it into parts, each with its own external_id.
04Check an article
GET /v1/articles/{external_id} returns the state of the newest attempt, and the audio you can use right now.
stateis one of the values in the table below, ornullif there is nothing yet.audioisnulluntil there is a finished file. It holdspath,voiceandduration_ms(the length in milliseconds). When you send a new version, the previous file stays inaudiountil the new one is ready, so the player on your page never loses its audio.
| State | What it means | What to do |
|---|---|---|
queued | The article waits for its turn. | Ask again in a minute. |
running | The audio is being made. | Ask again in a minute. |
done | The audio is ready. | Use audio.path. |
needs_review | An automatic quality check held the file, and a person checks it. | Nothing. It usually clears within a working day, and state becomes done. |
error | Making the audio failed. Readerr tries again on its own for a while. | Send the article again if it stays in error. |
quota_exceeded | The characters ran out for the period. | Choose a plan or wait for the next period, then send the article again. |
An article you never sent returns 404.
05Show the player on your page
Load the player script once per page, then place one <readerr-player> where the audio should appear. Put the path from the answer in data-path, and the same path after https://cdn.readerr.app in the audio src.
<script src="https://cdn.readerr.app/player.js" defer></script>
<readerr-player data-v="2" data-lang="en" data-scheme="light" data-colour="blue"
data-path="/example-news/01JC8Z3Q7V5N2K4M6P8R0T1W2X.mp3" data-article="article-4812"
data-duration-ms="176000" style="display:block">
<div class="readerr-stage"
style="box-sizing:border-box;height:128px;display:flex;align-items:center;padding:0 12px;border-radius:16px;background:rgba(128,128,128,.08)">
<audio controls preload="none" style="width:100%"
src="https://cdn.readerr.app/example-news/01JC8Z3Q7V5N2K4M6P8R0T1W2X.mp3"></audio>
</div>
<p class="readerr-label" lang="en" slot="label"
style="margin:8px 0 0;font-size:13px;line-height:20px">AI-generated audio (Readerr) - may contain errors</p>
</readerr-player>
- The script is a plain script with
defer. Do not load it astype="module". data-articleis yourexternal_id.data-langis the language of the article.data-schemeislight,darkorauto(follows the reader's system).data-colourisblue,ink,green,redoryellow. Both are optional.data-duration-msisduration_msfrom the answer. It is optional and shows the length before the file loads.- The audio does not download until the reader presses play. The
<audio>element is there so the page plays even if the script is blocked. - The label stays. The player always shows that the audio is AI-generated, in the language of the article, and puts the line back if a page script removes it. Keep it in your markup so it shows before the script runs. The wording is fixed. For
plit is "Audio generowane przez AI (Readerr) - może zawierać błędy", forde"KI-generiertes Audio (Readerr) - kann Fehler enthalten", forfr"Audio généré par IA (Readerr) - peut contenir des erreurs" and foren"AI-generated audio (Readerr) - may contain errors".
Show the player only when state is done. If the player does not appear, the player doesn't show on my site has a checklist.
06Delete an article
DELETE /v1/articles/{external_id} removes the article and its audio files, for example when you unpublish it. The answer is always 204 with no body, also for an article Readerr does not know, so you can safely repeat it. The audio address stops working within minutes. The characters the article used stay counted for the period.
curl -X DELETE https://api.readerr.app/v1/articles/article-4812 \
-H "Authorization: Bearer $READERR_KEY"
07Check your characters and voices
GET /v1/quota shows how many characters your account has used in the current period.
{
"used": 2074,
"limit": 25000,
"remaining": 22926,
"period_start": "2026-10-10T00:00:00Z",
"period_end": "2026-11-10T00:00:00Z",
"plan": "trial",
"access": "active"
}
limit and remaining are null when your plan has no fixed limit. used and limit cover all the sites of your account together. The answer has a few more fields about your plan and your trial.
GET /v1/voices lists the voices you can pass in voice, and your site's default:
{
"voices": [{ "id": "F1", "gender": "female" }, { "id": "M3", "gender": "male" }],
"default": "F4"
}
08Errors
Every answer to a refused request has a status code and a JSON body. A detail field says what is wrong.
| Code | When | What to do |
|---|---|---|
401 | The Authorization header is missing, is not Bearer YOUR_KEY, or the key is unknown or no longer active. A key stops working when you make a new one for the site, or when the site or account is switched off. | Check the key. Make a new one on the site page if you lost it. |
404 | GET /v1/articles/{external_id} for an article you never sent. | Check the id. |
409 | The characters ran out, or the trial or plan has ended. The body is {"error": "quota_exceeded", ...} with used, limit, remaining, the period dates and access. Nothing is stored. | Choose a plan or wait for the next period, then send the article again. |
413 | The request is too big (body_too_large), or the text Readerr reads is over 40,000 characters (text_too_long, with chars and max_chars). | Send less, or split the article. |
422 | The body is not valid JSON, a required field is missing or too long, or lang is not one of pl, de, fr, en. The detail names the field. | Fix the request and send it again. |
429 | Too many articles in a minute. The body is {"error": "rate_limited", "retry_after": 12} and the Retry-After header holds the same number of seconds. | Wait that many seconds. |
The rate limit is 60 requests to POST /v1/ingest per minute for each key, counted over a sliding minute. It is meant to stop a loop that sends the same article over and over, not to slow a normal site. If you publish in bulk, send the articles at a steady pace. A refused request never queues audio, so after a 429 you send the article again.
09Where to go next
- How to choose a voice and language for audio explains voices.
- How to fix a word or number that is read wrong covers pronunciation.
- Characters and limits explains what counts and what happens when the characters run out.