# Guide to Honeybadger Webhooks: Features and Best Practices

Honeybadger webhooks tell your systems when something changes in a monitored project: an error occurs or comes back after being resolved, an uptime check goes down, a scheduled job misses its check-in, or a deploy is recorded. If you use Honeybadger for error and uptime monitoring, webhooks are how those alerts reach your own incident tooling, ticket queues and dashboards.

This guide covers how Honeybadger's Webhook integration is set up and authenticated, the 13 events and their payloads, what Honeybadger leaves undocumented, and the best practices for running a receiver in production.

## What are Honeybadger webhooks?

Each Honeybadger project can have a Webhook integration that posts a JSON payload to a URL you choose. Every payload has an `event` field naming what happened and a human-readable `message`, plus objects that depend on the event.

Honeybadger does not sign webhook payloads. Instead, every outbound request from Honeybadger's servers (webhooks, source map fetches and uptime checks) carries a `Honeybadger-Token` header: a secret token derived from the project's API key. Your endpoint authenticates the request by comparing that header with the token it has stored.

## Honeybadger webhook features

| Feature | Details |
| --- | --- |
| Configuration | **Project Settings > Alerts & Integrations > Webhook**, for users with admin rights on the project |
| Authentication | `Honeybadger-Token: <token>`, sent raw with no prefix |
| Where the token lives | The **API Key** tab of the project settings |
| Token rotation | The token changes when the project API key is reset |
| Optional header | `Authorization: Bearer <value>`, if you fill in the integration's **Bearer token** field |
| Body signature | None |
| Routing | The body's `event` field |
| Events | 13 |
| Event ID | None documented |
| Retries and timeout | Not documented |
| IP allowlist | Not available. Honeybadger uses a dynamic range for outbound requests |

## Common events

| Event | Sent when | Top-level properties |
| --- | --- | --- |
| `occurred` | An error occurs | `event`, `message`, `project`, `fault`, `notice` |
| `resolved` | An error is marked as resolved | `event`, `message`, `project`, `fault` |
| `unresolved` | A resolved error occurs again | `event`, `message`, `project`, `fault` |
| `assigned` | An error is assigned to a user | `event`, `message`, `actor`, `fault`, `assignee` |
| `commented` | A comment is added to an error | `event`, `message`, `actor`, `fault`, `comment` |
| `rate_exceeded` | An error rate threshold is exceeded | `event`, `message`, `project`, `fault` |
| `volume_spike` | A project's error volume spikes above its learned baseline | `event`, `message`, `project`, `volume_spike` |
| `deployed` | A deployment is recorded | `event`, `message`, `project`, `deploy` |
| `down` | An uptime check fails | `event`, `message`, `project`, `site`, `outage` |
| `up` | An uptime check succeeds after being down | `event`, `message`, `project`, `site`, `outage` |
| `cert_will_expire` | An SSL certificate is about to expire | `event`, `message`, `project`, `site` |
| `check_in_missing` | An expected check-in is missing | `event`, `message`, `project`, `check_in` |
| `check_in_reporting` | A check-in reports successfully | `event`, `message`, `project`, `check_in` |

Here is Honeybadger's `check_in_missing` example, trimmed:

```json
{
  "event": "check_in_missing",
  "message": "[Voyager Test] MISSING: Voyager hasn't checked in for 4 years",
  "project": {
    "id": 67747,
    "name": "Voyager Test",
    "token": "abcd1234",
    "environments": [{ "id": 94721, "name": "development", "notifications": true }],
    "unresolved_fault_count": 3,
    "fault_count": 6
  },
  "check_in": {
    "id": "XYZLOL",
    "name": "Voyager",
    "state": "missing",
    "schedule_type": "simple",
    "reported_at": "2019-12-23T21:39:37.124397Z",
    "expected_at": "2023-10-30T19:42:28.853152Z",
    "missed_count": 33385,
    "report_period": "1 hour",
    "details_url": "https://app.honeybadger.io/projects/67747/check_ins"
  }
}

```

The `project` object is large in practice. It includes the project's environments, owner, users and counts, as well as a `token` field, so treat stored payloads as sensitive. The `fault` object on error events carries `id`, `klass`, `environment`, `resolved`, `notices_count` and `last_notice_at`, and `down` and `up` events include an `outage` with `down_at` and `up_at`.

## Setting up Honeybadger webhooks

1. Open the project in Honeybadger, go to **Project Settings**, and open the **API Key** tab. Copy the token shown there and store it server-side, for example as `HONEYBADGER_TOKEN`.
2. Go to the **Alerts & Integrations** tab and choose the **Webhook** integration, below the list of personal alert integrations.
3. Enter your URL, including the protocol, for example `https://example.com/webhooks/honeybadger`.
4. Leave **Bearer token** blank unless something in front of your endpoint rejects requests without an `Authorization` header.
5. Save.

If you receive Honeybadger webhooks through Hookdeck, enter the token from the **API Key** tab in the Hookdeck source. Hookdeck checks the `Honeybadger-Token` header, not the optional `Authorization` bearer token.

Honeybadger cannot reach `localhost`. For local development, `hookdeck listen 3000 honeybadger --path /webhooks/honeybadger` with the [Hookdeck CLI](/docs/cli) gives you a public URL that forwards to your machine.

## Securing Honeybadger webhooks

Read `Honeybadger-Token`, compare it with your stored token in constant time, and reject the request if it is missing or different. The header holds the raw token, so there is nothing to strip. Fail closed when the token is not configured.

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

function verifyHoneybadgerToken(headerValue, token) {
  if (typeof headerValue !== 'string' || !token) return false;
  const received = Buffer.from(headerValue, 'utf8');
  const expected = Buffer.from(token, 'utf8');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

app.post('/webhooks/honeybadger', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyHoneybadgerToken(req.get('honeybadger-token'), process.env.HONEYBADGER_TOKEN)) {
    return res.sendStatus(401);
  }

  const payload = JSON.parse(req.body.toString('utf8'));
  // Route on payload.event, then hand off to a queue.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac
import os

def verify_honeybadger_token(header_value, token) -> bool:
    if not header_value or not token:
        return False
    return hmac.compare_digest(header_value.encode("utf-8"), token.encode("utf-8"))

# verify_honeybadger_token(request.headers.get("honeybadger-token"), os.environ["HONEYBADGER_TOKEN"])

```

If one endpoint serves several Honeybadger projects, keep a token per project, because each project's token is different.

> **Make Honeybadger webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the `Honeybadger-Token` header on every request, keeps each error, uptime and check-in event in a durable queue, and lets you replay any of them into your incident tooling.

## Honeybadger webhook limitations and pain points

### Resetting the API key breaks every webhook

**The Problem:** All webhooks start failing authentication at once, and nothing changed in your code.

**Why It Happens:** `Honeybadger-Token` is derived from the project API key. Reset the key and the token changes, but your endpoint still holds the old one.

**Workarounds:**

* Make "update the webhook token" part of your API key rotation runbook.
* Alert on a run of authentication failures from Honeybadger, since a whole project failing together usually means a rotated key.

**How Hookdeck Can Help:** With a Hookdeck source, the token lives in one place. Update it there and every downstream service keeps working, and the requests rejected with `VERIFICATION_FAILED` in the meantime are visible in Hookdeck's request log.

### A static token instead of a signature

**The Problem:** Anyone who obtains the token can send a payload that passes the check, and nothing protects the body from being changed in transit.

**Why It Happens:** Honeybadger sends the same token on every request rather than signing each body. The same token also goes out with source map fetches and uptime checks.

**Workarounds:**

* Use an HTTPS endpoint and never log the token header.
* Before acting on high-impact events, such as auto-resolving tickets, confirm the fault through the Honeybadger API.

**How Hookdeck Can Help:** Hookdeck checks the header before anything is queued for your service, so forged requests never reach your handlers, and you can keep the token out of your application entirely by verifying Hookdeck's own signature at your destination instead.

### No documented retries

**The Problem:** If your endpoint is down when a `down` or `check_in_missing` alert fires, you do not know whether Honeybadger will try again.

**Why It Happens:** Honeybadger's webhook documentation states the method and body but no timeout, success criterion or retry policy.

**Workarounds:**

* Keep the endpoint fast and highly available, and return `200` as soon as the token is checked.
* For uptime and check-in alerts, keep Honeybadger's built-in email or chat alerts switched on as a second channel.

**How Hookdeck Can Help:** Hookdeck accepts each request as soon as it arrives and stores it, then delivers it to your service with [automatic retries](/docs/retries), so a deploy or outage on your side does not lose an alert.

### No event ID for deduplication

**The Problem:** If the same alert arrives twice, your handler can open two tickets.

**Why It Happens:** Payloads carry `event` and the related objects, but no unique delivery or event identifier.

**Workarounds:**

* Build a key from `event` and the object's own identity, such as `fault.id` with `fault.notices_count`, or `site.id` with `outage.down_at`.
* Make ticket creation an upsert keyed on the fault or site.

**How Hookdeck Can Help:** Hookdeck's [deduplication](/docs/deduplication) can drop repeated requests based on the fields you choose.

## Best practices

### Check `Honeybadger-Token`, not the bearer token

The `Honeybadger-Token` header is sent on every request. The bearer token is an optional extra for services that insist on an `Authorization` header. Verify the one Honeybadger always sends.

### Route on `event` and accept new ones

Switch on the body's `event`. Log and acknowledge any event you do not recognise rather than returning an error, so a new event type never shows up as a failed delivery.

### Acknowledge fast and process asynchronously

Return `200` once the token matches and the payload is stored, then create tickets or page people in the background. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Pair `down` with `up`, and `check_in_missing` with `check_in_reporting`

Use the matching recovery event to close what the failure event opened. Key incidents on `site.id` or `check_in.id` so the pair always finds each other. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

Honeybadger webhooks are unsigned JSON posts with an `event` field, authenticated by the `Honeybadger-Token` header that comes from the project's API key. Compare that header in constant time, update it whenever the API key is reset, route on `event`, and build your own deduplication key, because Honeybadger documents neither an event ID nor a retry policy.

[Hookdeck Event Gateway](https://hookdeck.com) checks the `Honeybadger-Token` header, stores every alert, and retries delivery to your service, so an outage on your side does not drop an error or downtime notification.

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