Guide to Deepgram Webhooks: Features and Best Practices
Deepgram webhooks deliver the results of asynchronous speech and language jobs: a finished transcript, generated text-to-speech audio, or a text intelligence analysis. If you're transcribing long recordings or feeding transcripts into an LLM pipeline, callbacks let you submit the job, release the connection, and get the result pushed to you when Deepgram is done.
This guide covers how Deepgram's per-request callback model works, what arrives at your endpoint, how to authenticate deliveries with Basic Auth (Deepgram does not sign them), the platform's retry and port limits, and the best practices for production.
What are Deepgram webhooks?
Deepgram calls its webhooks "callbacks", and they work differently from the subscription model most providers use. There is no dashboard where you register an endpoint for a list of events. Instead, you add a callback query parameter to an individual API request. Deepgram responds immediately with a request_id, processes the job asynchronously, and then sends the result to the URL you supplied.
The callback body is the same JSON you would have received from a synchronous request. For a pre-recorded transcription that means a metadata object (holding request_id, created, duration, channels, model_info and any extra values you passed) and a results object with the transcript, broken down by channel and alternative. Each callback URL lives only as long as the request that carries it, so every call site in your code decides where its result goes.
Deepgram webhook features
| Feature | Details |
|---|---|
| Configuration | Per request, via the callback query parameter; no dashboard or subscription API |
| Supported endpoints | /v1/listen (pre-recorded and streaming), /v1/speak, /v1/read |
| HTTP method | POST by default; callback_method=put switches to PUT |
| Authentication | No signature; Basic Auth credentials embedded in the callback URL, plus an optional dg-token header carrying the API Key Identifier (not guaranteed on every request) |
| Envelope | Same body as the synchronous response, e.g. { "metadata": ..., "results": ... } for transcription |
| Correlation | request_id returned at submission and repeated in metadata.request_id; custom extra=KEY:VALUE pairs returned in metadata.extra (2048 characters per pair) |
| Retries | Up to 10 retries on a non-2xx response, with a 30-second delay between attempts |
| Ports | Callbacks are only sent to ports 80, 443, 8080, and 8443 |
| Protocols | Pre-recorded: http or https; streaming: http, https, ws, or wss |
Common events
Deepgram callbacks carry no event-type field. What arrives depends on which endpoint you called with the callback parameter:
| Event | Fires when |
|---|---|
Pre-recorded transcription result (/v1/listen) | Deepgram finishes transcribing a submitted audio file or URL |
Streaming transcription response (/v1/listen) | Each response is produced during a live stream; HTTP(S) callbacks receive one request per response |
Text-to-speech result (/v1/speak) | Deepgram finishes generating audio for a submitted text |
Text intelligence result (/v1/read) | Deepgram finishes analyzing a submitted text |
Since there is no discriminator in the body, route by endpoint (a separate callback path per API you call) and match each delivery to its job using metadata.request_id or the values you passed in extra.
Inspect Deepgram webhooks as they arrive. Point Deepgram at a Hookdeck Console URL to inspect and replay real deliveries — no account or setup required.
Setting up Deepgram webhooks
Setup happens in the request itself. Add callback to the query string of a transcription request, and optionally extra for correlation data you want echoed back:
curl -X POST \
-H "Authorization: Token YOUR_DEEPGRAM_API_KEY" \
-H "Content-Type: audio/wav" \
--data-binary @call-recording.wav \
"https://api.deepgram.com/v1/listen?callback=https://dg_user:dg_pass@your-app.com/webhooks/deepgram&extra=job_id:4821"
Deepgram replies straight away with a request_id. Store it against your own job record, because it is the key that ties the eventual callback back to the work you submitted. When the transcript is ready, Deepgram sends it to the callback URL, with the job_id:4821 pair available under metadata.extra.
A few details to get right:
- The
username:password@portion of the URL is how you authenticate deliveries. Deepgram sends those credentials as a standardAuthorization: Basicheader. Percent-encode any special characters in the credentials, and URL-encode the full callback value if your HTTP client doesn't do it for you. - The callback host must listen on port 80, 443, 8080, or 8443. A callback to any other port is not sent.
- To receive results as
PUTrequests, addcallback_method=putand make sure your route accepts that method. - The same
callbackandcallback_methodparameters work on/v1/speakand/v1/read, so text-to-speech and text intelligence jobs follow the same pattern.
For local development, use the Hookdeck CLI: hookdeck listen 3000 deepgram --path /webhooks/deepgram gives you a public HTTPS URL that forwards to your local server, plus a web UI for inspecting and replaying deliveries, with no account required. Use the generated URL as the callback value in your test requests. This also sidesteps the port restriction, since your local server on port 3000 can't receive callbacks directly.
Securing Deepgram webhooks
Deepgram does not sign or timestamp callbacks, so beyond HTTPS nothing proves the body wasn't altered in transit. Deepgram documents two ways to authenticate a delivery:
- Basic Auth (recommended). Embed credentials in the callback URL as
https://username:password@your-app.com/.... Deepgram sends them in anAuthorization: Basicheader on every callback. - The
dg-tokenheader. When present, it holds the API Key Identifier of the key that submitted the original request. Deepgram's documentation states that this header is not guaranteed on every callback request, which makes it less reliable than Basic Auth. Treat it as a secondary check, never the only one.
Check the Basic Auth header with a timing-safe comparison, and reject requests without it. If a dg-token header is present, you can also confirm it matches your API Key Identifier:
const crypto = require("crypto");
const express = require("express");
const app = express();
function safeEqual(a, b) {
const bufA = Buffer.from(a);
const bufB = Buffer.from(b);
return bufA.length === bufB.length && crypto.timingSafeEqual(bufA, bufB);
}
function verifyDeepgramCallback(req) {
const auth = req.headers["authorization"] || "";
if (!auth.startsWith("Basic ")) {
return false;
}
const expected = Buffer.from(
`${process.env.DEEPGRAM_CALLBACK_USER}:${process.env.DEEPGRAM_CALLBACK_PASS}`
).toString("base64");
if (!safeEqual(auth.slice(6), expected)) {
return false;
}
// dg-token is not sent on every callback; check it only when present
const dgToken = req.headers["dg-token"];
if (dgToken && !safeEqual(dgToken, process.env.DEEPGRAM_API_KEY_ID)) {
return false;
}
return true;
}
app.post("/webhooks/deepgram", express.raw({ type: "application/json" }), (req, res) => {
if (!verifyDeepgramCallback(req)) {
return res.status(401).send("Unauthorized");
}
const payload = JSON.parse(req.body.toString());
const requestId = payload.metadata?.request_id;
const jobId = payload.metadata?.extra?.job_id;
const transcript =
payload.results?.channels?.[0]?.alternatives?.[0]?.transcript || "";
// Hand off to a queue: store the transcript against jobId / requestId
res.status(200).send("OK");
});
Note the source of the credentials: they travel inside the callback URL you send to Deepgram, so anyone who can read your request logs can read them. Keep them out of shared logs and rotate them like any other secret.
Make Deepgram webhooks production-ready. Hookdeck Event Gateway checks Basic Auth credentials at the edge, deduplicates, and durably queues every callback with replay for anything that fails.
Deepgram webhook limitations and pain points
No signatures, and the token header is optional
The Problem: A Deepgram callback has nothing cryptographic attached to it. If your callback URL leaks, anyone can POST a fabricated transcript to it, and your handler has no signature to reject it on. The dg-token header looks like a solution but isn't present on every request.
Why It Happens: Deepgram's callback design relies on credentials embedded in the URL and an optional API Key Identifier header.
Workarounds:
- Always embed Basic Auth credentials in the callback URL and reject any delivery without a matching
Authorizationheader. - Use HTTPS callback URLs only, so the credentials and transcript are encrypted in transit.
- Validate the body before acting on it: confirm
metadata.request_idbelongs to a job you actually submitted.
How Hookdeck Can Help: A Hookdeck Deepgram source checks the Basic Auth credentials on every incoming callback, so requests without valid credentials are rejected before they reach your handler. Every accepted delivery is logged with its full headers and body for inspection.
A five-minute retry window for work you've already paid for
The Problem: If your endpoint returns a non-2xx response, Deepgram retries up to 10 times with a 30-second delay between attempts, which covers roughly five minutes. An outage or a bad deploy that lasts longer means the result never arrives, even though the audio was processed.
Why It Happens: Deepgram's documented retry policy is 10 attempts 30 seconds apart, and the documentation describes no way to request redelivery once those attempts are used up.
Workarounds:
- Return 200 as soon as the request is authenticated, and process the transcript in the background.
- Record every submitted
request_idwith a status, and alert on jobs that stay pending past an expected window. - Be ready to resubmit the audio for any job whose callback never arrived.
How Hookdeck Can Help: Hookdeck accepts the callback and durably queues it ahead of your endpoint, then delivers it with automatic retries on a schedule you control. Issues alert you when deliveries fail, and any event can be replayed once your service is back, so the result isn't lost.
No event type to route on
The Problem: Every callback is a bare result body. There is no type or event field, so a single endpoint receiving transcripts, text-to-speech output and text analysis can't tell them apart from the body alone, and matching a result to the job that produced it is your responsibility.
Why It Happens: Callbacks are tied to individual requests rather than to event subscriptions, so Deepgram returns the same response it would have returned synchronously, with no envelope around it.
Workarounds:
- Use a distinct callback path for each API you call, such as
/webhooks/deepgram/listenand/webhooks/deepgram/speak. - Pass routing data with
extra(for exampleextra=pipeline:summaries) and branch onmetadata.extrain your handler. - Keep a lookup from
request_idto job so every result can be attributed.
How Hookdeck Can Help: Point every callback at one Hookdeck source, then use filters on fields such as metadata.extra or the request path to route results to different destinations, keeping the routing in configuration rather than handler code.
Restricted ports complicate local development
The Problem: Callbacks are only sent to ports 80, 443, 8080, and 8443. A development server on localhost:3000 can't receive them, and neither can a staging service on a non-standard port.
Why It Happens: Deepgram limits outbound callback traffic to that fixed set of ports.
Workarounds:
- Run a tunnel that exposes your local server on port 443.
- Put staging services behind a reverse proxy on a permitted port.
- Test with short audio files so each round trip is quick.
How Hookdeck Can Help: The Hookdeck CLI gives you a public HTTPS URL on port 443 that forwards to any local port, and its web UI lets you replay a captured callback against your handler as many times as you need without paying for another transcription.
Best practices
Authenticate with Basic Auth first
Embed credentials in every callback URL and verify the Authorization header with a timing-safe comparison. Treat dg-token as an extra check when it's present, since Deepgram doesn't guarantee it on every request. Store the credentials and your API Key Identifier in environment variables.
Acknowledge fast, process asynchronously
Transcripts for long recordings can be large, and downstream work like summarizing with an LLM or writing to a search index takes time. Return 200 once the request is authenticated and hand the body to a queue or background job, so slow processing never turns into a failed delivery and a retry. See why to process webhooks asynchronously, and for AI pipelines with tight delivery timeouts and concurrency limits, see Hookdeck for AI agents and LLMs.
Make handlers idempotent on request_id
Retries mean the same result can arrive more than once. Key your processing on metadata.request_id and skip results you've already stored, so a retried callback never creates a duplicate transcript record or triggers a second summary. See our guide to webhook idempotency.
Attach correlation data with extra
Pass your own identifiers (a job ID, a user ID, a pipeline name) as extra=KEY:VALUE pairs when you submit the request. They come back in metadata.extra, so your handler can attribute the result without a database lookup, and you can route on them.
Match your route to callback_method
Callbacks are POST by default. If you set callback_method=put, register a PUT handler at the same path, or every delivery will fail and burn through Deepgram's retries.
Conclusion
Deepgram callbacks are a per-request feature: you add a callback URL to a /v1/listen, /v1/speak or /v1/read call, get a request_id back immediately, and receive the same body a synchronous request would have returned once the job finishes. There are no signatures and no event types, so authentication comes from Basic Auth credentials in the callback URL, and correlation comes from request_id and the extra values you attach.
Delivery is limited to 10 retries, 30 seconds apart, which leaves little room for downtime. Hookdeck Event Gateway checks the Basic Auth credentials, queues every callback durably, and lets you replay anything that failed, so your handlers only process authenticated results and an outage doesn't cost you a transcript.
Get started with Hookdeck for free and handle Deepgram webhooks reliably in minutes.