How to add audio to your articles with the Readerr API

Applies toAPI v1Customer panelUpdated 26 Oct 2026
Short answer

Add your site in the customer panel to get an API key. Send each article with POST /v1/ingest, using the key as a Bearer token. Ask GET /v1/articles/<id> until the state is done, then put the audio path into the Readerr player on your page.

The five required fields are external_id, lang, title, url and content_html. You do not need WordPress.

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

  1. Send the article

    Call POST /v1/ingest when 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_id is 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 same external_id again and you update that article.

    The answer is 200 with a small JSON body:

    { "client_id": "example-news", "article_id": 90317, "state": "queued" }
    

    queued means audio is being made. unchanged means you sent the same article as before, and nothing new is made. article_id is Readerr's own number for the article. You don't need it: every other call uses your external_id.

  2. Ask for the audio

    Audio takes a few minutes. Ask for the article by the external_id you sent:

    curl https://api.readerr.app/v1/articles/article-4812 \
      -H "Authorization: Bearer $READERR_KEY"
    

    While the audio is on its way, audio is null:

    { "external_id": "article-4812", "state": "running", "audio": null }
    

    When state is done, audio holds 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.app followed by the path, here https://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.

  3. Show the player

    Put the Readerr player on the article page, with the path from 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.

FieldRequiredWhat it is
external_idYesYour 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.
langYesThe language of the article: pl, de, fr or en. Any other value is refused.
titleYesThe headline, up to 1,000 characters. It is not read aloud, but a changed title counts as a change to the article.
urlYesThe address of the article page, up to 2,048 characters. The API requires it. It does not change the audio.
content_htmlYesThe article body as HTML, up to 1,000,000 characters. Readerr picks out the text that is read.
voiceNoA 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_idNoLeave 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.

  • state is one of the values in the table below, or null if there is nothing yet.
  • audio is null until there is a finished file. It holds path, voice and duration_ms (the length in milliseconds). When you send a new version, the previous file stays in audio until the new one is ready, so the player on your page never loses its audio.
StateWhat it meansWhat to do
queuedThe article waits for its turn.Ask again in a minute.
runningThe audio is being made.Ask again in a minute.
doneThe audio is ready.Use audio.path.
needs_reviewAn automatic quality check held the file, and a person checks it.Nothing. It usually clears within a working day, and state becomes done.
errorMaking the audio failed. Readerr tries again on its own for a while.Send the article again if it stays in error.
quota_exceededThe 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 as type="module".
  • data-article is your external_id. data-lang is the language of the article.
  • data-scheme is light, dark or auto (follows the reader's system). data-colour is blue, ink, green, red or yellow. Both are optional.
  • data-duration-ms is duration_ms from 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 pl it is "Audio generowane przez AI (Readerr) - może zawierać błędy", for de "KI-generiertes Audio (Readerr) - kann Fehler enthalten", for fr "Audio généré par IA (Readerr) - peut contenir des erreurs" and for en "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.

CodeWhenWhat to do
401The 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.
404GET /v1/articles/{external_id} for an article you never sent.Check the id.
409The 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.
413The 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.
422The 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.
429Too 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

Try it on your own posts.

Try Readerr on your site for 7 days. No card needed.

  • No card
  • Plugin or API
  • No ads in the audio