PodcastsToText
Webhooks

Setup and events

Register an endpoint, store the secret, and the three events with the envelope they arrive in.

Register an HTTPS endpoint and we POST to it when something finishes. This is the alternative to polling, and on a busy account it is most of your request budget back.

POST/api/v1/webhooksscope: shows:write
FieldTypeRequiredDescription
urlstringYesPublic HTTPS endpoint. Private and link-local addresses are refused, at registration and again at delivery time.
eventsarrayNoWhich events to receive. Omitted or empty subscribes to all of them.
descriptionstringNoFor your own benefit in the dashboard.
Create
curl -X POST https://podcaststotext.com/api/v1/webhooks \
  -H "Authorization: Bearer $PTT_KEY" -H 'Content-Type: application/json' \
  -d '{
    "url": "https://example.com/hooks/ptt",
    "description": "production worker",
    "events": ["transcription.completed"]
  }'
201 Created
{
  "id": "b21c…",
  "url": "https://example.com/hooks/ptt",
  "events": ["transcription.completed"],
  "secret": "whsec_ptt_9f2c1a…",
  "message": "Endpoint registered. Store `secret` now: it is never shown again."
}

Store the secret now. It appears in this response and nowhere else, ever. Without it you cannot verify a delivery, and the only remedy is to delete the endpoint and register a new one.

Managing endpoints

GET/api/v1/webhooksscope: shows:read
PATCH/api/v1/webhooks/{id}scope: shows:write
DELETE/api/v1/webhooks/{id}scope: shows:write

Listing also returns the full event catalogue, your endpoint allowance, and each endpoint’s last_error and last_success_at: the fastest answer to “why did we stop receiving these”. There is no separate webhooks:* scope; a subscription is account configuration, so reading needs shows:read and changing needs shows:write.

Events

EventFires when
transcription.completedA transcription finishes. A YouTube video with captions completes inline, so this can arrive moments after the 202: look the id up rather than assuming you have stored the job.
episode.detectedA new episode appears in a connected RSS feed.
episode.publishedAn episode is published to a public page.

The catalogue is deliberately small and closed: adding an event means committing to its payload shape indefinitely.

The envelope

POST body
{
  "id": "8c1f2d3e-…",
  "event": "transcription.completed",
  "created_at": "2026-08-18T09:12:44.117Z",
  "api_version": "2026-08-09",
  "data": {
    "transcription_id": "9f3c8e21-…",
    "status": "completed",
    "title": "The one about interest rates",
    "duration_seconds": 3312,
    "transcript_url": "https://cdn.podcaststotext.com/transcripts/….json"
  }
}

id is the delivery id and is stable across retries: dedupe on it. created_at is when the event happened, not when this attempt was sent. api_version changes only for a breaking change. data carries identifiers rather than content, because a transcript body in every payload is a retry storm: receivers routinely cap bodies at 1 MB and a three-hour episode exceeds that. transcript_url is the stored document and needs no key, so a receiver that cannot authenticate still has something to fetch; for a specific format use GET /api/v1/transcriptions/{id}?format=srt.

Carrying the transcript in the delivery

An endpoint can ask for the transcript inline. This is for a receiver that cannot call the API back — a no-code automation, a chat workflow — and it is off by default. Set it per endpoint, not per transcription: a webhook consumer is code we cannot migrate, and a payload whose shape depended on how each job was submitted would make one endpoint handle several shapes.

Registering one that carries SRT
curl -X POST "https://podcaststotext.com/api/v1/webhooks" \
  -H "Authorization: Bearer $PTT_KEY" -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/ptt",
    "events": ["transcription.completed"],
    "payload": { "text": true, "format": "srt" }
  }'

The delivery then carries text, text_format and text_bytes alongside the identifiers. format is one of txt, srt, vtt or json — docx and pdf are binary and have no place in a JSON body. Speaker labels are included whenever the transcript has them.

A transcript over 256 KB is left out, never trimmed. The delivery still arrives, with text_omitted set to too_large and text_bytes saying how big it was, so a receiver can decide what to do; half a transcript that does not announce itself as half is worse than none. text_omitted is also no_timings when a time-coded format was asked of a transcript stored as plain text, and unreadable or not_found when the stored document could not be loaded. Only transcription.completed is expanded; the other two events describe episodes.

Rendering happens when the delivery is sent, not when it is queued, so changing an endpoint’s format applies to deliveries already waiting.

HeaderContains
X-PTT-Signaturet=<unix>,v1=<hex hmac-sha256> (see Security and delivery).
X-PTT-EventThe event name, so you can route without parsing the body.
X-PTT-DeliveryThe delivery id, matching id in the envelope.
User-AgentPodcastsToText-Webhooks/1.0

Receiver checklist

  • Respond 2xx within 10 seconds: queue the real work and return. A receiver that works before answering turns every delivery into seven.
  • Deduplicate on the envelope id. At-least-once delivery means you will eventually see a duplicate.
  • Do not trust the payload without verifying the signature.
  • Return 410 Gone when an endpoint is genuinely retired. We stop and disable it rather than retrying for a day.
  • Point a throwaway endpoint at a request-inspection service first, to see the headers and the envelope before you write the handler.