Errors use one JSON shape:
{
"error": {
"code": "audio_not_ready",
"message": "Audio is still processing.",
"param": "audio"
}
}
param may be null when the error applies to the whole request. Use code for application logic; message is readable detail.
Some validation errors also include fields, with the messages for each field:
{
"error": {
"code": "invalid",
"message": "Name can't be blank",
"param": "name",
"fields": { "name": ["can't be blank"] }
}
}
| HTTP status | Code | What to do |
|---|---|---|
400 | bad_request | Supply a required parameter or body. |
401 | unauthorized | Send Authorization: Bearer with a valid token; create or replace it if needed. |
403 | forbidden | Use an admin account for this podcast. |
404 | not_found | Check the hashid or ID and account access. |
422 | invalid | Correct the field named by param, if present. |
422 | upload_invalid | Check the upload ID, audio type, file size, artwork format, or remote URL. |
422 | audio_not_ready | Poll the episode until audio.status is ready. |
422 | plan_limit_reached | A plan limit (episodes a month, or episode length) stopped the publish; the message says which. The episode stays a draft. |
422 | channels_unsupported | Omit channels for a legacy podcast. |
422 | immutable_live_release | A channel release is already live; unpublish before making a new release. |
422 | time_zone_unconfirmed | Confirm the podcast time zone before using local-time channel scheduling. |
A remote audio import or an audio conversion can fail after a draft was created. In that case, GET /episodes/:id returns audio.status: "failed" and an audio.error message. Correct the URL and PATCH the episode with a new audio_url, or attach a new upload.
For network failures, retry reads. For a retried POST /episodes, reuse the same Idempotency-Key so the retry returns the original draft. Do not retry a validation error unchanged.