Build with the
PodNex API.
Generate studio-quality podcasts programmatically, get notified the moment an episode is ready, and integrate PodNex into your own product.
Introduction
The PodNex API lets you turn text content into a natural-sounding, two-voice podcast episode with a single request, and be notified via webhook when it's ready.
All requests are made over HTTPS to the base URL below. All request and response bodies are JSON.
https://api.podnex.techAuthentication
API requests are authenticated with an API key, passed as a bearer token in the Authorization header:
Authorization: Bearer pk_live_...Create a key from your dashboard — Settings → API Keys. The full key is shown once, at creation — store it somewhere safe.
Keys are issued with one or both of the following scopes. Requests made with a key missing the required scope get a 403.
| Field | Type | Description |
|---|---|---|
| podcasts:read | scope | List and fetch podcasts, check generation status, download finished audio. |
| podcasts:write | scope | Create, update, delete, and retry podcasts. |
API keys can only be created, listed, or revoked from an authenticated dashboard session — not via the API itself, and not with another API key. This means a leaked key can never be used to mint itself broader access. The same applies to managing webhooks.
Quickstart
Generate your first episode:
curl -X POST https://api.podnex.tech/api/v1/podcasts \
-H "Authorization: Bearer pk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"noteContent": "The history of coffee, from Ethiopian highlands to a global industry — origins, the spread through the Ottoman Empire and Europe, and how it shaped trade routes for centuries.",
"duration": "SHORT",
"hostVoice": "Sierra",
"guestVoice": "Daniel"
}'Generation runs asynchronously. Poll GET /api/v1/podcasts/:id/status or register a webhook (see below) to know when it's done, rather than blocking on the request above.
Voices
Every episode is a conversation between a host and a guest voice. Pass any of the names below as hostVoice / guestVoice — both are optional and default to Sierra / Daniel.
Host voices
- Sierra— Warm & Friendly
- Hannah— Professional
- Melody— Casual
- Lauren— Authoritative
- Emily— Bright
- Kaitlyn— Energetic
- Luna— Calm
Guest voices
- Daniel— Engaging
- Noah— Energetic
- Ethan— Thoughtful
- Jasper— Curious
- Caleb— Calm
- Ronan— Deep
- Zane— Dynamic
You can preview every voice before you build against it on the podcast creation page.
Endpoints
Create a podcast
/api/v1/podcastsrequires scope: podcasts:write| Field | Type | Description |
|---|---|---|
| noteContentrequired | string | 100–50,000 characters. The source content your episode is generated from. |
| duration | "SHORT" | "LONG" | SHORT is 3-5 minutes, LONG is 8-10 minutes. Defaults to "SHORT". |
| title | string | Up to 200 characters. Left blank, a title is generated from your content. |
| hostVoice / guestVoice | string | See Voices above. Default to Sierra / Daniel. |
| webhookUrl | string (url) | Optional one-off override — most integrations register a persistent webhook instead (see below). |
Returns 201 with the created podcast, status QUEUED.
List podcasts
/api/v1/podcastsrequires scope: podcasts:read| Field | Type | Description |
|---|---|---|
| page | number | Default 1. |
| limit | number | Default 20, max 100. |
| status | "QUEUED" | "PROCESSING" | "COMPLETED" | "FAILED" | Filter by status. |
| sort / order | "createdAt" | "updatedAt" / "asc" | "desc" | Default createdAt, desc. |
Get / check status / download
/api/v1/podcasts/:idrequires scope: podcasts:read/api/v1/podcasts/:id/statusrequires scope: podcasts:read/api/v1/podcasts/:id/downloadrequires scope: podcasts:read/status returns a lightweight { status, progress, currentStep } — cheaper to poll than the full object. /download returns a time-limited, presigned { url } for the finished audio file (also embedded directly in the podcast object once COMPLETED).
Update / delete / retry
/api/v1/podcasts/:idrequires scope: podcasts:write/api/v1/podcasts/:idrequires scope: podcasts:write/api/v1/podcasts/:id/retryrequires scope: podcasts:write/retry re-queues a FAILED podcast with the same content and settings.
Usage stats
/api/v1/podcasts/statsrequires scope: podcasts:readReturns your all-time and this-month podcast counts and total generated minutes.
Webhooks
Register a webhook from Settings → Webhooks to get notified as a podcast moves through its lifecycle, instead of polling. Webhook management requires a dashboard session — it isn't available via API key.
Events
| Field | Type | Description |
|---|---|---|
| PODCAST_CREATED | event | Fired immediately once a podcast is queued. |
| PODCAST_PROCESSING | event | Fired when generation actually starts. |
| PODCAST_COMPLETED | event | Fired when audio is ready. Payload includes a presigned, downloadable audioUrl valid for 1 hour. |
| PODCAST_FAILED | event | Fired if generation errors out. |
Delivery
Each event is POSTed to your URL as JSON, with these headers:
Content-Type: application/json
X-Webhook-Event: PODCAST_COMPLETED
X-Webhook-Timestamp: 1784360230628
X-Webhook-Signature: <hex-encoded HMAC-SHA256 of the raw request body>Failed deliveries (network error or non-2xx response) are retried up to 3 times with exponential backoff.
Example payload — PODCAST_COMPLETED
{
"podcastId": "cmrq2fh6w000110eyvl8qngx4",
"status": "COMPLETED",
"audioUrl": "https://podnex-audio.s3.eu-north-1.amazonaws.com/...(presigned)",
"audioDuration": 8,
"timestamp": "2026-07-18T07:49:58.254Z"
}Verifying signatures
The secret shown when you create a webhook signs every delivery with HMAC-SHA256 over the raw JSON body. Verify it using the raw request body — not a re-serialized object, which can produce a different byte sequence and a signature mismatch.
import crypto from "crypto";
function verifyWebhook(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody) // raw string, not JSON.parse(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(expected)
);
}Errors
Every response follows the same envelope:
{ "success": true, "data": { ... } }| Field | Type | Description |
|---|---|---|
| 400 | status | Validation failed — check the error message. |
| 401 | status | Missing, invalid, or expired API key / session. |
| 403 | status | Valid key, but missing the required scope, or the action requires a dashboard session. |
| 404 | status | The resource doesn't exist, or isn't yours. |
| 500 | status | Something broke on our end — safe to retry. |