Documentation · 19 endpoints

The API, route by route

19 endpoints, 24 operations. For each one: what it reads, what it returns, and the errors you have to handle. Everything is unfolded, because you arrive here looking for a field name.

Base: https://wedispatch.fr/api/v1 · The WordPress plugin

Read this before you start

What the API cannot do

6 features of the product have no public route. Finding that out in the middle of a build costs half a day; reading it here costs two minutes.

Create a shareable clip

The feature exists, in the dashboard. It has no public route: a clip is made by hand, by listening, and that is precisely what a program cannot do for you.

Search for a spoken word across your audio

Available in the dashboard, not by key. What the API gives you is the position of every word within ONE given article.

Create or list your sites

A key BELONGS TO a site, it does not manage sites. The attachment happens when the key is created, in the dashboard or through the code exchange.

Register a webhook once and for all

There is no account-level webhook setting. The address is passed ON EVERY generation call, and it is not kept. If you were looking for where to configure it, the answer is: nowhere, and that is deliberate.

Declare an RSS feed to watch

Feed discovery exists as an integration mode, but it is configured in the dashboard, not through the API.

Filter or paginate the article list

`GET /podcasts` reads no parameter and returns the last thirty. No status filter, no cursor, no date. It is the limit most likely to surprise you, so it is written here and on the route itself.

Getting started

What you need to have understood before the first call.

Authentication

Header on every request

One key per site, sent as a header. No route is served without it, except the voice catalogue and the code exchange, both flagged as such.

Authorization: Bearer wd_live_xxxxxxxxxxxxxxxx

Worth knowing: With the WordPress plugin you will never handle this key: it is exchanged server to server. The key in clear text only exists for bespoke integrations, and it is never shown again after it is created.

Language of error messages

Accept-Language header

Error messages are returned in French by default, in English if you ask. The HTTP CODE never changes: branch your logic on the code, not on the wording.

Accept-Language: en

Worth knowing: Two routes do not yet translate their messages, the recommendations among them. Handle the code, not the sentence.

Rate limits

Three routes carry one

Ten generations per minute, four batches per minute, thirty voice previews per hour. The other fourteen routes have no rate limit. Going over returns a 429.

429 { "error": "…maximum 10 générations par minute…" }

Worth knowing: Two of those three counters live in instance memory: they are safeguards, not contractual guarantees. Only the generation limit is counted in the database.

The refusal codes

What you need to handle

Ten codes cover everything the API can refuse. Branch your logic on them, never on the text of the message.

400 requête mal formée, champ absent ou hors bornes
401 clé absente, inconnue ou révoquée
402 la retouche dépasse le budget inclus et n'a pas été confirmée
403 le compte est bloqué, ou la capacité n'est pas ouverte
404 la ressource visée n'existe pas, ou n'est pas la vôtre
409 l'état actuel interdit l'opération demandée
429 une limite de débit ou le volume mensuel est atteint
500 un défaut de notre côté, à signaler
502 un service dont nous dépendons a répondu de travers
503 indisponibilité temporaire, réessayez plus tard

Worth knowing: A 409 IS NOT AN ERROR ON YOUR SIDE: it says the order of operations is wrong, a regeneration already running, audio produced before word-level alignment. Replaying it unchanged later often works. A 400 replayed unchanged, never.

Producing

Send a text and get audio back. Synchronous mode publishes the article already voiced.

POST

Generate the audio

/api/v1/podcasts

The main route. With `wait` set to true, the response waits for synthesis to finish and contains the file address: the article publishes already voiced, with no visible delay.

What the route reads
  • textrequiredThe full text, from 100 to 100,000 characters.
  • titleoptionalUsed by the podcast feed and the provenance page.
  • canonical_urloptionalThe article's address on your side.
  • external_idoptionalYour own identifier. This is what guarantees deduplication: the same one sent twice produces a single audio.
  • rubriqueoptionalDetermines the voice, the branding and the per-section statistics. Accepted alias: `section`.
  • authoroptionalFeeds the per-byline statistics. Accepted alias: `byline`.
  • voiceoptionalVoice identifier. An unknown voice is refused with a 400, never silently replaced.
  • languageoptionalfr, en, es, de, it, hi, zh, ar, pt, ja, he, ko, nl, sv, pl, ta, te, ru, tr, th, tl, cs, fi.
  • triggeroptional`auto` to submit to the account's switch, `manual` to force it.
  • publish_atoptionalFuture ISO date: prepares the audio for a scheduled publication.
  • webhook_urloptionalPublic https address, called when the audio is ready. It applies TO THIS CALL and is never kept as a setting.
  • auto_regenerateoptionalRegenerate automatically when the text changes.
  • waitoptionalTrue to receive the audio in the response. No effect beyond 20,000 characters.

Idempotency-Key Replaying the same call with the same key does not produce a second audio.

POST https://wedispatch.fr/api/v1/podcasts
Content-Type: application/json

{
  "text": "Le conseil municipal a adopte hier soir...",
  "title": "Le budget 2027 adopte",
  "external_id": "id-dans-votre-cms",
  "rubrique": "Politique",
  "wait": true
}

What it returns: `article_id`, `job_id`, `status`, `embed`, and with `wait`: `audio_url`, `duration_s`, `version`, `engine`, `voice`, `fallback`.

To handle: 400 invalid text, voice, language or date · 403 account blocked · 429 monthly quota reached, or ten per minute exceeded

Worth knowing: An article over 1,000 characters costs one more credit per started block, and a started block is spent.

POST

Batch generation

/api/v1/podcasts/batch

Up to twenty-five articles in one request, useful for an archive. Every per-item field is accepted, and `language`, `trigger` and `auto_regenerate` also accept one value for the whole batch.

What the route reads
  • itemsrequiredA non-empty array, twenty-five entries at most. Each entry accepts the same fields as a single generation.
POST https://wedispatch.fr/api/v1/podcasts/batch
{ "items": [
  { "text": "...", "external_id": "a1", "rubrique": "Sports" },
  { "text": "...", "canonical_url": "https://.../article-2" }
] }

What it returns: `accepted`, `total`, and `results[]` where each entry carries its own `status`: `processing`, `ready`, `stale`, `quota`, `blocked`, `skipped` or `error`.

To handle: 400 items missing, empty or over twenty-five · 429 four batches per minute exceeded

Worth knowing: `wait` does NOT exist on this route, and the idempotency header is not read here. The remaining monthly volume bounds the batch before the rate limits do: twenty-five articles with ten credits left processes ten and reports the rest as `quota`.

POST

Regenerate in full

/api/v1/podcasts/{article_id}/regenerate

For an article changed substantially. Costs a credit like a generation, and resets the edit budget, since this is a new audio.

What the route reads
  • voiceoptionalChange voice on the way through.
  • languageoptionalChange language on the way through.
  • publish_atoptionalSchedule when the new version goes live.
  • webhook_urloptionalCalled when the new version is ready.
POST https://wedispatch.fr/api/v1/podcasts/{article_id}/regenerate

What it returns: `article_id`, `job_id`, `status`, `regens_included`, `regens_used`.

To handle: 404 unknown article · 409 no stored text to regenerate · 429 regeneration ceiling or monthly quota

Worth knowing: This is the leading cause of accidental spending, usually after a small text correction. Check first whether an edit is enough: it only bills the rewritten passage, where a regeneration pays for the whole article again.

Tracking

The state of one article, and the list of the latest ones.

GET

Track one article

/api/v1/podcasts/{article_id}

The state of an article and, once it is ready, the address of its audio. Call it after sending without `wait`, or when a webhook arrives.

GET https://wedispatch.fr/api/v1/podcasts/{article_id}

What it returns: `article_id`, `title`, `status` (`processing`, `ready`, `stale` or `failed`), `regen_count`, `audio` (`null` or the full object with `url`, `duration_s`, `version`), `embed`, `subtitles`.

To handle: 404 unknown article · 503 audio link temporarily unavailable

GET

List your articles

/api/v1/podcasts

The last thirty articles on the account, most recently modified first.

GET https://wedispatch.fr/api/v1/podcasts

What it returns: `articles[]`: `id`, `title`, `canonical_url`, `audio_status`, `char_count`, `regen_count`, `updated_at`, and depending on the state of the database `rubrique` and `author`.

Worth knowing: THIS ROUTE ACCEPTS NO PARAMETER. No status filter, no pagination, no date. Thirty rows, always. If you need to find the articles to regenerate, compare `audio_status` yourself on what you receive.

DELETE

Remove an audio

/api/v1/podcasts/{article_id}

Deletes an article's audio files. The article, its source text and its listening statistics are kept.

DELETE https://wedispatch.fr/api/v1/podcasts/{article_id}

What it returns: `ok`, `article_id`, `status`, `removed_versions`, and `kept`, which says explicitly what was not deleted.

To handle: 400 invalid identifier · 404 unknown article · 409 a generation is running

Worth knowing: No credit is refunded, and the response says so rather than keeping quiet about it.

GET

Related articles

/api/v1/recommendations

The articles closest to a given one, to offer a next listen.

What the route reads
  • article_idrequiredAs a query parameter, in UUID format.
  • limitoptionalFive by default, between one and twenty.
GET https://wedispatch.fr/api/v1/recommendations?article_id={uuid}&limit=5

What it returns: `article_id`, `count`, `recommendations[]` with `id`, `title`, `rubrique`, `created_at`, `player_url`.

To handle: 400 article_id missing or malformed · 404 article not found

Displaying

The player on your page, and the subtitles.

Display the player

One line in your template

The player drops in with one line. Automatic mode finds the audio matching the page on its own, from its canonical address.

<script src="https://wedispatch.fr/p.js" async></script>
<div data-wd-player data-wd-external-id="12345"></div>

Worth knowing: If your theme filters content through an unusual entry point, automatic insertion may fail. The explicit container gives you control over the exact position.

GET

Subtitles

/api/v1/podcasts/{article_id}/subtitles

A subtitle file aligned on the ACTUAL reading, not on the source text. That is what avoids drift from the very first spelled-out number.

What the route reads
  • formatoptional`srt` by default, or `vtt`.
GET https://wedispatch.fr/api/v1/podcasts/{article_id}/subtitles?format=vtt

What it returns: The file itself, as plain text. This is not JSON.

To handle: 400 unknown format · 404 article or alignment not found · 409 audio produced before word-level alignment

Editing

Acting on audio already produced without regenerating all of it.

GET

The position of every word

/api/v1/podcasts/{article_id}/words

The first of the two steps in an edit: a passage is designated by word indexes, so you first need to know where each word falls in the audio.

GET https://wedispatch.fr/api/v1/podcasts/{article_id}/words

What it returns: `article_id`, `title`, `duration_s`, `words[]` with the start and end of each word.

To handle: 403 editing is not open on this account · 409 the engine used produces no word-level alignment · 409 audio predates the alignment

POST

Edit a passage

/api/v1/podcasts/{article_id}/correct

You rewrite the faulty passage, that passage alone is resynthesised and stitched back into the existing audio. The file address does not change, so your pages have nothing to update.

What the route reads
  • from_wordrequiredIndex of the first word to replace, in the array returned by the previous route.
  • to_wordrequiredIndex of the last word, inclusive.
  • textrequiredWhat should be heard instead.
  • accept_credit_useoptionalConfirms that you accept spending the credits. Every edit costs at least one: without this field the request is refused with a 402 stating its price.
POST https://wedispatch.fr/api/v1/podcasts/{article_id}/correct
{
  "from_word": 142,
  "to_word": 149,
  "text": "deux virgule quatre millions d'euros"
}

What it returns: `ok`, `version`, `duration_s`, `patch_chars`, `patch_chars_used`, `credits_used`, `review`.

To handle: 402 the spend was not confirmed · 403 editing is not open on this account · 409 audio with no word-level alignment

Worth knowing: The price is returned ON THE REFUSAL, in `credits_required`: the moment you are refused is exactly when you need to know what the request would cost. A credit opens a thousand characters and each edit opens its own, so there is no such thing as a zero-credit edit. `review` set to true flags a tight stitch worth listening to before publication.

Configuring

The account settings a key can read and write.

GET

Read the settings

/api/v1/profile

Every account setting, grouped by area, plus what your plan actually opens.

GET https://wedispatch.fr/api/v1/profile

What it returns: `player`, `reading` (including the pronunciation lexicon), `sound`, `gate`, `capabilities`, `player_templates`, and `editable`: the exact list of fields a key can write.

Worth knowing: `editable` is the source of truth. Rather than copying the list below into your code, read it: it will tell you what is writable on the day that changes.

GET

Read the pronunciation lexicon

/api/v1/pronunciations

The account's pronunciation corrections, applied to every article before synthesis.

GET https://wedispatch.fr/api/v1/pronunciations

What it returns: `pronunciations` (the `{ de, vers }` list), `count`, `max`.

Worth knowing: READING is not gated: an account that loses the option must still be able to read and export what it entered. Writing is what the plan gates.

POST

Add or correct a pronunciation

/api/v1/pronunciations

Merges into the existing lexicon: what you do not send is left alone. A word already present has its pronunciation replaced, never duplicated.

POST https://wedispatch.fr/api/v1/pronunciations
{ "de": "Ploërmel", "vers": "Plo-air-mel" }

What it returns: The full list after the change, plus `added`, `replaced`, `ignored`.

To handle: 400 no usable entry (`de` and `vers` required, 80 characters at most) · 403 the lexicon is not open on this account · 409 lexicon full (200 entries)

Worth knowing: A call that posted NOTHING returns an error, never a 200: otherwise your interface would show "saved" for a correction that does not exist. Case does not create duplicates, accents do: « Ploërmel » and « Ploermel » are two entries, deliberately.

DELETE

Remove a pronunciation

/api/v1/pronunciations

By source word, case-insensitive. Removing an absent word is not an error.

DELETE https://wedispatch.fr/api/v1/pronunciations
{ "de": "Ploërmel" }

What it returns: The full list after the change, plus `removed`.

To handle: 400 neither `de`, nor `pronunciations`, nor `all: true`

Worth knowing: Clearing everything REQUIRES `all: true`. A bodyless DELETE is what an HTTP client sends when it calls the wrong thing; letting it erase two hundred unrecoverable entries would be a trap.

PUT

Change the settings

/api/v1/profile

Twenty-five fields are writable by key, including the pronunciation lexicon, the default voice, the voice rules and the player's appearance.

PUT https://wedispatch.fr/api/v1/profile
{ "default_voice": "...", "player_template": "..." }

What it returns: `ok`, plus `ignored[]` if you sent fields that are not writable by key.

To handle: 400 no writable field in the body sent · 403 the requested setting is not open on this account · 409 the setting requires a word-level alignment that is missing

Worth knowing: A non-writable field is not an error: it is set aside and RETURNED in `ignored`, so that you see it instead of assuming it was applied. Conditional access mode is never writable by key, deliberately.

Measuring

Consumption and plays. Counts, never people.

GET

The state of the account

/api/v1/account

The subscribed volume, this month's consumption, and what the plan includes.

GET https://wedispatch.fr/api/v1/account

What it returns: `plan_label`, `volume`, `usage` (including `credits_used`, `credits_quota`, `articles_left`), `included`, `access`, `offre`, `urls`.

Worth knowing: `plans` and `next_plan` are empty and will stay so: they date from the old plans and are kept only so no existing integration breaks. The structure of `offre` is not described here, for want of an audit: read it rather than assume it.

GET

This month's consumption

/api/v1/usage

What has been spent over the current period, in credits and in characters.

GET https://wedispatch.fr/api/v1/usage

What it returns: `period`, `quota_credits`, `credits_used`, `credits_left`, `percent_used`, `warning`, `blocked`, `generations`, `regenerations`, `listens`, `audio_minutes`.

GET

Plays

/api/v1/stats

Aggregate counts, per article, per section and per byline. Never per person: that information does not exist.

GET https://wedispatch.fr/api/v1/stats

What it returns: `generated_at`, `totals`, `last_30_days`, `articles`, `authors`.

Worth knowing: No parameters: no period filter, no pagination. The internal structure of the aggregates is not detailed here, for want of an audit: it is stable, but we would rather have you read it than describe it from memory.

POST

Telling us what went wrong

/api/v1/feedback

A short note, sent by the WordPress plugin when someone deactivates it, and ONLY if they clicked "Send". Documented here because it exists: a route nobody writes down is a route nobody remembers to secure.

POST https://wedispatch.fr/api/v1/feedback
{ "type": "desactivation", "motif": "affichage", "texte": "…", "version": "1.27.0" }

What it returns: `ok`. Always 200: the caller is deactivating a plugin, and their action must not depend on any failure of ours.

Worth knowing: Never contains article content, and nothing about a listener. Unknown fields are ignored without error, so that a future version of the plugin does not lose its feedback over a detail of form.

Voices

The catalogue, and an audio preview before you choose.

GET

The voice catalogue

/api/v1/voices

The voices available to your account, including your signature voices.

GET https://wedispatch.fr/api/v1/voices

What it returns: `engine`, `default`, `voices[]` with `id`, `label`, `gender`, `style`, `langs[]`, and `sample` where a clip exists.

Worth knowing: This route is served WITHOUT a key, because the documentation and the demo depend on it. On the other hand, a key that is PRESENTED and refused makes the call fail: it never falls back to the public catalogue, which would hide a dead key.

POST

Preview a voice

/api/v1/voices/preview

An audio clip on your own text, before you choose. Costs no credit.

What the route reads
  • textrequiredFrom 100 to 3,000 characters.
  • voiceoptionalOtherwise the account's default voice.
  • languageoptionalOtherwise the default language.
POST https://wedispatch.fr/api/v1/voices/preview
{ "text": "...", "voice": "..." }

What it returns: The audio file itself. The `X-WeDispatch-Billed` header reads zero, and the `X-WeDispatch-Voice` header says which voice actually spoke.

To handle: 400 text too short or too long · 429 thirty previews per hour exceeded

Worth knowing: An unknown voice is NOT refused here: it is replaced by the default voice. Read the response header to know which one served, otherwise you will believe you listened to the one you asked for.

Conditional access

For restricted content: verifying a listening token server side.

GET

The access configuration

/api/v1/access

The account's access mode and the secret used to sign listening tokens.

GET https://wedispatch.fr/api/v1/access

What it returns: `mode`, `preview_seconds`, `token_ttl_s`, `secret`, `client_id`.

Worth knowing: The secret never leaves the server. Call this route from your back office, never from a page.

POST

Verify a listening token

/api/v1/access/check

For restricted content: your server checks that a token really grants access to this article.

What the route reads
  • articlerequiredThe identifier of the article requested.
  • tokenoptionalThe token presented. Its absence simply returns a reasoned refusal.
POST https://wedispatch.fr/api/v1/access/check
{ "article": "{article_id}", "token": "..." }

What it returns: `valid`, `status`, `reason`, `detail`, `mode`. The reason says precisely why: token missing, malformed, invalid signature, wrong article, or expired.

To handle: 400 article field missing

POST

Exchange a code for a key

/api/v1/connect/exchange

What the WordPress plugin does on installation: it exchanges a short-lived code for its own key, server to server. The user never sees the key.

What the route reads
  • coderequiredThe code obtained on the authorisation screen.
  • secretrequiredYour installation's secret.
POST https://wedispatch.fr/api/v1/connect/exchange
{ "code": "...", "secret": "..." }

What it returns: `api_key`, `plan`, `tier`, `site`, `label`.

To handle: 400 code expired or invalid · 400 maximum of ten active keys reached · 404 account not found

Worth knowing: This is the only route, along with the voice catalogue, that does not ask for a key: its whole purpose is to obtain one.

A case that is not covered?

This documentation describes what the code does, not what we would like it to do. If you are looking for a route that is not here, it probably does not exist: tell us what you were trying to do, it is useful even when the answer is no.

Would you rather not write code? The WordPress plugin does the same thing with no key to handle.