# 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](https://console.hookdeck.com) 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:

```bash
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 standard `Authorization: Basic` header. 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 `PUT` requests, add `callback_method=put` and make sure your route accepts that method.
* The same `callback` and `callback_method` parameters work on `/v1/speak` and `/v1/read`, so text-to-speech and text intelligence jobs follow the same pattern.

For local development, use the [Hookdeck CLI](/docs/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 an `Authorization: Basic` header on every callback.
* The `dg-token` header. 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:

```javascript
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](/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 `Authorization` header.
* Use HTTPS callback URLs only, so the credentials and transcript are encrypted in transit.
* Validate the body before acting on it: confirm `metadata.request_id` belongs 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_id` with 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](/docs/retries) on a schedule you control. [Issues](/docs/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/listen` and `/webhooks/deepgram/speak`.
* Pass routing data with `extra` (for example `extra=pipeline:summaries`) and branch on `metadata.extra` in your handler.
* Keep a lookup from `request_id` to job so every result can be attributed.

How Hookdeck Can Help: Point every callback at one Hookdeck source, then use [filters](/docs/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](/docs/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](/webhooks/guides/why-implement-asynchronous-processing-webhooks), and for AI pipelines with tight delivery timeouts and concurrency limits, see [Hookdeck for AI agents and LLMs](/docs/use-cases/ai-agents).

### 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](/webhooks/guides/implement-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](/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](https://dashboard.hookdeck.com/signup) for free and handle Deepgram webhooks reliably in minutes.