
Webhooks vs Polling for Transcripts: The Async Choice
Summarize this article with:
Poll when you have under 50 jobs per day or a user is actively watching a progress bar. Switch to webhooks when volume crosses roughly 100 jobs per day, when jobs run in the background, or when results need to fan out to multiple destinations. The math is clear: at 200 jobs per day, webhooks cut API calls by 18x. The tricky part is doing them safely: HMAC verification, idempotency, and knowing which vendors actually support a callback mechanism.
Use polling when you have fewer than 50 jobs a day or a user is actively waiting. Switch to webhooks above roughly 100 jobs a day when jobs run in the background. That is the decision in two sentences. Everything below is the implementation detail that makes the difference between a webhook that works and one that quietly loses transcripts at 2 AM.
The Resource Math
A concrete example. You transcribe 200 jobs per day. Each takes 3 minutes on average.
Polling every 5 seconds:
- Each job: 36 poll requests on average (3 min x 12 polls/min)
- Daily: 7,200 poll requests
- Useful results in those requests: roughly 1%
Webhooks:
- Each job: 1 submit + 1 webhook delivery
- Daily: 200 submits + 200 webhooks = 400 events
- 100% of events carry a result
Webhooks are 18x more efficient at this volume. At 2,000 jobs per day, polling becomes genuinely expensive on both sides of the API, and most providers start rate-limiting you on status checks before they rate-limit you on submissions.
When Polling Is Still the Right Call
You have no public HTTP endpoint. Local development environments, mobile-first apps without a backend, and internal tools behind a VPN cannot receive webhook deliveries. Polling requires only outbound HTTP.
Volume is under 50 jobs per day. The resource cost is negligible. The simplicity of a polling loop outweighs the infrastructure of a webhook receiver.
The user is actively watching. A user uploaded an interview and is staring at a progress bar. Polling gives you something to update the UI with. Webhooks would require additional plumbing (WebSocket, server-sent events, or a push service) to relay the completion signal back to the browser. See building with transcription APIs for a working polling loop.
When to Switch to Webhooks
Three signals:
Volume passes the 100-job-per-day mark. The infrastructure cost of a webhook receiver pays for itself in API call savings within a week.
Latency does not matter because the user is not watching. Background processing of recordings that land in a database for later querying. The user will see the result whenever they next open the dashboard.
Results need to fan out. A webhook can trigger your database write, a Slack notification, and a HubSpot update simultaneously. Polling has to do this serially in your application code. See integrating transcription with Slack, Notion, or HubSpot for the fan-out recipes.
Which Transcription APIs Actually Support Webhooks
This varies more than the documentation implies.
| Vendor | Async mechanism | Delivery param | Signature verification | Retries |
|---|---|---|---|---|
| AssemblyAI | Webhook (POST) | webhook_url in body | Custom header (you configure name + value) | 10 attempts, 10s between each |
| Deepgram | Callback (POST) | callback query param | dg-token header (API key identifier) | 10 attempts, 30s between each |
| Rev.ai | Webhook (POST) | notification_config object | Auth header via config | Every 30 min for up to 24 hr |
| AWS Transcribe | Poll only | GetTranscriptionJob | N/A | N/A |
| Google Cloud STT | Poll only (long-running operation) | Operations.get | N/A | N/A |
| OpenAI Whisper | Synchronous only | N/A | N/A | N/A |
AWS Transcribe has no webhook delivery at all. You submit a job, then poll GetTranscriptionJob until TranscriptionJobStatus equals COMPLETED. Google Cloud Speech-to-Text's LongRunningRecognize returns an operation handle that you poll with Operations.get until done is true. OpenAI's /v1/audio/transcriptions endpoint is fully synchronous: you block until the response returns or you time out.
My take: Deepgram's callback mechanism is the most production-friendly of the three that support async delivery. AssemblyAI's payload delivers only a transcript_id and a status field on completion, meaning you always need a second API call to fetch the actual transcript. Deepgram sends the full result to your callback URL.
For a deeper look at how the major APIs price their usage, see speech-to-text API pricing 2026.
Setting Up a Webhook (ConvertAudioToText API)
The registration call:
curl -X POST https://api.convertaudiototext.com/api/v1/dashboard/webhooks \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/transcription",
"events": ["job.completed", "job.failed"]
}'
The response includes a secret field. Save it. The API generates the secret server-side and returns it once at creation time. It is not stored in a recoverable form.
Available events: job.queued, job.processing, job.completed, job.failed. Most integrations subscribe to job.completed and job.failed only.

HMAC Signature Validation
Every delivery includes an X-Webhook-Signature header with the value format sha256=<hex>. The signature is HMAC-SHA256(secret, raw_body).
In Node:
import crypto from 'crypto';
import express from 'express';
const app = express();
app.post('/webhooks/transcription',
express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.header('X-Webhook-Signature');
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body.toString());
handleTranscript(event);
res.sendStatus(200);
}
);
In Python:
import hmac
import hashlib
def verify(signature: str, body: bytes, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(),
body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
Use timingSafeEqual (Node) or hmac.compare_digest (Python). Naive string comparison leaks information through response timing.
Parse the body after validating, never before. Middleware that parses JSON before your handler runs will consume the raw bytes, making signature computation impossible. The express.raw() example above is deliberate: you get the raw buffer first, then parse.
Idempotency
The delivery worker may fire more than once for the same event. Networks fail. Your handler may process an event and crash before returning 200. The vendor retries. Your handler runs again.
Each event has a unique event_id (available in the payload). The safe pattern:
async function handleTranscript(event) {
const inserted = await db.query(
`INSERT INTO processed_events (event_id) VALUES ($1)
ON CONFLICT DO NOTHING RETURNING id`,
[event.data.job_id + ':' + event.event]
);
if (inserted.rowCount === 0) return;
await saveTranscript(event.data.job_id, event.data.result_url);
}
ON CONFLICT DO NOTHING means duplicate deliveries silently exit. The first delivery wins; duplicates are ignored without throwing errors.
Retry Behavior
The ConvertAudioToText delivery worker retries failed deliveries on this schedule (5 attempts total):
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 15 minutes |
| 5 | 1 hour |
| 6 | 6 hours |
After 5 failed attempts, the delivery is marked failed. You can view delivery history and trigger manual retries from the webhook dashboard, or use the retry endpoint:
curl -X POST https://api.convertaudiototext.com/api/v1/dashboard/webhooks/deliveries/{delivery_id}/retry \
-H "Authorization: Bearer $API_KEY"
For your handler to behave correctly: return 2xx for success, 4xx for permanent failures (no retry needed), 5xx for transient failures (retry wanted). Respond within 30 seconds. If your processing is slow, ack immediately and queue the real work:
app.post('/webhooks/transcription', (req, res) => {
validateSignature(req);
jobQueue.push(req.body);
res.sendStatus(200); // ack first, process after
});
Local Development
Webhooks require a public HTTPS URL. Two approaches that work:
ngrok or cloudflared tunnel. Exposes your local server to the internet with a temporary URL.
ngrok http 3000
# Use the resulting https URL as your webhook URL
Dual-mode handler. Webhooks in production, polling in local dev. An environment variable switches the behavior. This keeps the local feedback loop simple without changing production code.
Production Patterns
Direct-to-database. The handler writes the transcript to Postgres or MongoDB and returns 200. Downstream code queries the database. Simplest pattern, works at any scale.
Direct integration. The handler posts to Slack, creates a Notion page, or updates a HubSpot contact. Good when the integration is the primary destination. Fails loudly (5xx) if the downstream is down, which triggers retries automatically.
Event-bus. The handler publishes to Kafka, SNS, or an internal pub/sub topic. Multiple consumers process the same event independently. Decouples webhook reception from downstream processing. The right pattern for large pipelines where multiple systems consume the same transcript. See batch transcription for large projects for the pipeline architecture.
Mixing Both Patterns
Sometimes you want both. The user uploads a file. Your backend submits the job and starts polling for UI updates. The webhook fires when done and becomes the canonical record in your database. The user gets immediate feedback; the backend gets a reliable async signal that does not depend on the UI staying open.
This dual approach requires more code but delivers the best user experience when users actively wait for results on files they care about while the system also handles background jobs reliably.
If you just need clean transcripts without building a backend integration, ConvertAudioToText handles the whole pipeline in the browser with no API key required.
FAQ
When should I use polling instead of webhooks for transcription?
Poll when you have no public HTTP endpoint (local dev, VPN-only tools, mobile apps without a backend), when volume is under 50 jobs per day, or when a user is actively waiting and you need to update a progress bar in real time. Webhooks add infrastructure cost that polling does not justify at low volume.
Which major transcription APIs support webhooks?
AssemblyAI supports webhooks via a webhook_url parameter (10 retry attempts, 10-second timeout). Deepgram supports a callback query parameter with dg-token header verification and 10 retries every 30 seconds. Rev.ai uses a notification_config object with up to 24 hours of retries. AWS Transcribe and Google Cloud Speech-to-Text require polling: AWS via GetTranscriptionJob and Google via the Operations.get long-running operation interface. OpenAI Whisper is fully synchronous with no async delivery at all.
How do I validate that a webhook came from the right source?
The standard pattern is HMAC-SHA256: the provider signs the raw request body with your shared secret and puts the result in a header. Your handler recomputes the same HMAC and compares using a timing-safe equal function. Never use a plain string comparison, which leaks information through timing. Deepgram uses a different mechanism (a dg-token header tied to your API key), so the approach varies by vendor.
What should my webhook handler return and how fast?
Return a 2xx status code to acknowledge receipt, and do it within your vendor's timeout window (30 seconds is common). If your actual processing is expensive, ack immediately and queue the work. Return 5xx for transient failures you want retried, 4xx for permanent failures you do not. Slow responses trigger retry storms that can back up your delivery queue.
Sources
- AssemblyAI webhook docs: https://www.assemblyai.com/docs/getting-started/webhooks (checked 2026-07-02)
- Deepgram callback docs: https://developers.deepgram.com/docs/callback (checked 2026-07-02)
- Rev.ai webhook docs: https://docs.rev.ai/api/asynchronous/webhooks/ (checked 2026-07-02)
- AWS Transcribe GetTranscriptionJob: https://docs.aws.amazon.com/transcribe/latest/APIReference/API_GetTranscriptionJob.html (checked 2026-07-02)
- Google Cloud STT LongRunningRecognize: https://cloud.google.com/speech-to-text/docs/reference/rest/v1/speech/longrunningrecognize (checked 2026-07-02)
- OpenAI speech-to-text API: https://developers.openai.com/api/docs/guides/speech-to-text (checked 2026-07-02)
Try transcription free
Convert any audio or video to clean, unwatermarked text — speaker labels, timestamps, and AI summaries included. First 30 minutes free, no account.
Related Articles

Best Transcription Tools with API Access (2026)
Which transcription SaaS tools actually give you API keys, and on which plan? Verified pricing and plan gates for Descript, Sonix, Fireflies, Happy Scribe, AssemblyAI, and more.

Internal Transcription Tool: Build vs Buy, Then Build
A practical architecture guide for building an internal transcription tool: when to build vs buy, SSO, access control, data retention, and real API integration code.