# Guide to Trigger.dev Webhooks: Features and Best Practices

Trigger.dev webhooks tell you when something in a background jobs project needs attention: a run has failed for good, a deployment failed or succeeded, or an error group is new, has regressed, or has broken through an ignore rule. If you run tasks on Trigger.dev, alert webhooks are how those signals reach your incident tooling, chat bots, status pages and ticket queues.

This guide covers how Trigger.dev alert webhooks are set up and signed, the four alert types and their payloads, the behaviour of error group alerts that catches teams out, and the best practices for running a receiver in production.

## What are Trigger.dev webhooks?

Trigger.dev's outbound webhooks are alert notifications. They are not a stream of run lifecycle events: there is no webhook for a run starting or completing successfully. You add a webhook as an alert channel, and Trigger.dev sends a signed JSON `POST` to your URL whenever a matching alert fires.

Each request carries an `x-trigger-signature-hmacsha256` header holding the lowercase hex HMAC-SHA256 of the raw body, keyed with the webhook secret shown on the Alerts page. Trigger.dev's docs show only the SDK helper, `webhooks.constructEvent(request, secret)`, for verifying it. The header name, algorithm and encoding come from that function's source in the Trigger.dev SDK.

## Trigger.dev webhook features

| Feature | Details |
| --- | --- |
| Configuration | **Alerts** page for run and deployment alerts. **Errors** page (**Configure alerts...**) for error group alerts |
| Alert types | `alert.run.failed`, `alert.deployment.success`, `alert.deployment.failed`, `alert.error` |
| Signature header | `x-trigger-signature-hmacsha256` |
| Scheme | HMAC-SHA256, lowercase hex, no prefix |
| Signed content | The raw request body |
| Secret | Shown on the Alerts page when you create the webhook alert, used as a UTF-8 string |
| Replay window | None. No timestamp is signed |
| Envelope | `id`, `created`, `webhookVersion`, `type`, `object` |
| SDK helper | `webhooks.constructEvent()` in `@trigger.dev/sdk` |
| Retries and timeout | Not documented |
| IP allowlist | None published |

## Common events

| Type | Sent when | Main `object` fields |
| --- | --- | --- |
| `alert.run.failed` | A run fails once its retries are exhausted | `task` (`id`, `filePath`, `exportName`, `version`), `run` (`id`, `number`, `status`, `error`, `isOutOfMemoryError`, `machine`, `tags`, `dashboardUrl`) |
| `alert.deployment.success` | A deployment succeeds | `deployment` (`id`, `status`, `version`, `shortCode`, `deployedAt`), `tasks`, optional `git` and `vercel` |
| `alert.deployment.failed` | A deployment fails | `deployment` (with `failedAt`), `error` (`name`, `message`, optional `stack` and `stderr`), optional `git` and `vercel` |
| `alert.error` | An error group alert fires | `classification` (`new_issue`, `regression` or `unignored`), `error` (`fingerprint`, `type`, `message`, `stackTrace`, `firstSeen`, `lastSeen`, `occurrenceCount`, `taskIdentifier`) |

Every alert also includes `object.environment`, `object.organization` and `object.project`. On run and deployment alerts, `environment.type` is `PRODUCTION`, `STAGING` or `PREVIEW`, which lets you send production failures to on-call and preview failures somewhere quieter. Error group alerts carry the environment's `id` and `name` instead. On deployment alerts, `git` carries the branch, short commit SHA, commit message and URLs, plus pull request details when there is one.

Trigger.dev documents these fields in tables and publishes no example JSON body, so the shape below is assembled from those tables rather than copied from a delivery:

```json
{
  "id": "<unique webhook event ID>",
  "created": "<datetime>",
  "webhookVersion": "<payload format version>",
  "type": "alert.run.failed",
  "object": {
    "task": { "id": "...", "filePath": "...", "exportName": "...", "version": "..." },
    "run": { "id": "...", "number": 42, "status": "...", "error": {}, "dashboardUrl": "..." },
    "environment": { "id": "...", "type": "PRODUCTION", "slug": "..." },
    "organization": { "id": "...", "slug": "...", "name": "..." },
    "project": { "id": "...", "ref": "...", "slug": "...", "name": "..." }
  }
}

```

The value of `webhookVersion` is not documented either. Log it from your first delivery so you notice if it changes.

> **Inspect Trigger.dev webhooks as they arrive.** Point a webhook alert at a [Hookdeck Inspect](https://inspect.hookdeck.com) URL to inspect and replay real deliveries, with no account or setup required.

## Setting up Trigger.dev webhooks

1. Open your project in the Trigger.dev dashboard and click **Alerts** in the left-hand menu.
2. Click **New alert**, choose the webhook method, and enter your endpoint URL.
3. Choose which alerts it receives: a run fails, a deployment fails, or a deployment succeeds.
4. Copy the webhook secret shown on the Alerts page and store it server-side, for example as `TRIGGER_DEV_WEBHOOK_SECRET`.

Error group alerts are created separately, from the **Configure alerts...** button on the **Errors** page. Once created, they appear in the Alerts table labelled "Error group". To disable or delete an alert, use the menu at the end of its row.

If you receive Trigger.dev alerts through Hookdeck, paste the webhook secret into the Hookdeck source.

## Securing Trigger.dev webhooks

If your receiver runs on a runtime with Web `Request` objects, such as Next.js or Remix, the SDK does the work:

```typescript
import { webhooks, WebhookError } from "@trigger.dev/sdk";

export async function POST(request: Request) {
  try {
    const event = await webhooks.constructEvent(request, process.env.TRIGGER_DEV_WEBHOOK_SECRET!);
    // switch (event.type) { case "alert.run.failed": ... }
    return Response.json({ received: true });
  } catch (err) {
    if (err instanceof WebhookError) {
      return Response.json({ error: err.message }, { status: 400 });
    }
    throw err;
  }
}

```

Elsewhere, verify manually. Compute the hex HMAC-SHA256 of the raw body with the secret as a UTF-8 string, and compare it with the header in constant time:

```javascript
const crypto = require('crypto');
const express = require('express');

function verifyTriggerSignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const received = Buffer.from(signatureHeader, 'utf8');
  const computed = Buffer.from(expected, 'utf8');
  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}

const app = express();

app.post('/webhooks/trigger-dev', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyTriggerSignature(req.body, req.get('x-trigger-signature-hmacsha256'), process.env.TRIGGER_DEV_WEBHOOK_SECRET)) {
    return res.sendStatus(400);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Route on event.type, deduplicate on event.id, then process asynchronously.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hashlib
import hmac

def verify_trigger_signature(raw_body: bytes, signature_header, secret: str) -> bool:
    if not signature_header or not secret:
        return False
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature_header.encode("utf-8"), expected.encode("utf-8"))

```

The secret is a plain string: do not base64-decode it or strip a prefix. Nothing but the body is signed, so there is no timestamp to check.

> **Make Trigger.dev webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies the `x-trigger-signature-hmacsha256` header, stores every run, deployment and error alert, and routes each one by environment or type to the right destination.

## Trigger.dev webhook limitations and pain points

### Error group alerts go quiet

**The Problem:** A task has been failing in production for days, and no alert has arrived.

**Why It Happens:** An error group alert fires only when a group's status changes: a new issue, a regression after it was resolved, or a breach of an ignore condition. After it fires, the group is Unresolved and stays silent however many more times the error occurs. Trigger.dev documents this as intended behaviour.

**Workarounds:**

* Create a run fails alert as well. It fires for every run that fails once its retries are exhausted.
* Resolve or ignore error groups once you have dealt with them, so a recurrence alerts again.

**How Hookdeck Can Help:** Send both alert types to one Hookdeck source and use [filters](/docs/filters) on `type` and `object.environment.type` to route run failures, error groups and deployment results to different destinations.

### No documented delivery guarantees

**The Problem:** If your endpoint is down when a deployment fails, you may never hear about it.

**Why It Happens:** Trigger.dev documents no retry policy, timeout or success criterion for alert webhooks.

**Workarounds:**

* Keep the endpoint simple and fast, and return `200` as soon as the signature checks out.
* Keep email or Slack alerts enabled for the alerts that matter most.

**How Hookdeck Can Help:** Hookdeck accepts each alert as soon as it arrives and stores it, then delivers to your service with [automatic retries](/docs/retries) and [issue notifications](/docs/issues) when delivery fails.

### Verification details live in the SDK, not the docs

**The Problem:** You cannot find the header name or algorithm in Trigger.dev's documentation when writing a verifier outside Node.

**Why It Happens:** The docs show only `webhooks.constructEvent()`. The header (`x-trigger-signature-hmacsha256`), the algorithm and the hex encoding are defined in the SDK source.

**Workarounds:**

* Use the manual verifiers above, which match the SDK.
* Test against a captured delivery: `printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET"` should print the header value.

**How Hookdeck Can Help:** Hookdeck's Trigger.dev source verifies the header for you, so services in any language can trust what arrives from Hookdeck.

## Best practices

### Verify against the raw body

In Express, use `express.raw({ type: '*/*' })`. In Next.js, use `await request.text()` or pass the untouched `Request` to the SDK, which consumes the body itself. In FastAPI, use `await request.body()`.

### Deduplicate on `id`

Every alert has a unique `id`. Store it before you open a ticket or page someone, so a repeated delivery does not create a second incident. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Route by environment

Use `object.environment.type` on run and deployment alerts to treat `PRODUCTION` failures differently from `STAGING` and `PREVIEW`.

### Acknowledge fast and process asynchronously

Return `200` once the signature matches and the alert is stored, then create tickets or post to chat afterwards. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Handle unknown types

Log and acknowledge any `type` you do not recognise rather than failing, so a new alert type never shows up as a failed delivery.

## Conclusion

Trigger.dev alert webhooks cover failed runs, deployments and error groups, and each is signed with a bare hex HMAC-SHA256 of the raw body in `x-trigger-signature-hmacsha256`. Verify with the SDK or by hand, route on `type` and environment, deduplicate on `id`, and pair error group alerts with run failure alerts so a persistent failure does not go unnoticed.

[Hookdeck Event Gateway](https://hookdeck.com) verifies every Trigger.dev alert, stores it, and routes it to the right team, so an alert is not lost when your receiver is down.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Trigger.dev webhooks reliably in minutes.