Guide to Honeycomb Webhooks: Features and Best Practices
Honeycomb webhooks carry your alerts out of the observability platform. When a Trigger fires, an SLO burns through its error budget too quickly, or Anomaly Detection spots something unusual in a service, Honeycomb can send a JSON notification to your own endpoint, so you can open incidents, page people through a tool Honeycomb does not integrate with, or record alerts in your own systems.
This guide covers how Honeycomb webhook integrations are set up and attached to alerts, how to authenticate requests with the Shared Secret, how to shape the body with a custom payload template, and the practices that keep a Honeycomb receiver reliable.
What are Honeycomb webhooks?
In Honeycomb, a webhook is an integration: a named recipient with a URL, an optional Shared Secret, and optional custom payloads, variables and headers. You create it once under Team Settings > Integrations, then choose it as a recipient on individual Triggers, SLO burn alerts and Anomaly Detection services. When one of those alerts fires, Honeycomb sends the result of the alert as JSON to the URL.
Honeycomb does not sign webhook requests. Instead, every request carries the integration's Shared Secret, as entered, in the X-Honeycomb-Webhook-Token header. Your endpoint authenticates the request by comparing that header with the secret you stored. The secret is optional in Honeycomb, and a webhook without one sends nothing that proves where a request came from.
Honeycomb webhook features
| Feature | Details |
|---|---|
| Configuration | Team Settings > Integrations > Trigger, SLOs, and Anomaly Detection Recipients > Add Integration, provider Webhook, or the Recipients API |
| Alert sources | Triggers, SLO Budget Rate burn alerts, SLO Exhaustion Time burn alerts, Anomaly Detection |
| Body | JSON (Content-Type: application/json). The standard body is not documented field by field |
| Custom payloads | An optional JSON template per alert type, with up to 10 webhook variables |
| Fixed headers | Content-Type, User-Agent: Honeycomb Triggers, X-Honeycomb-Webhook-Token, X-Honeycomb-Webhook-Delivery-ID |
| Custom headers | Up to 5. They cannot reuse Content-Type, User-Agent or X-Honeycomb-Webhook-Token |
| Authentication | The optional Shared Secret, sent as plain text in X-Honeycomb-Webhook-Token |
| Signature and timestamp | None |
| HTTP method | Not named in the docs |
| Response window | 15 seconds. A timeout is not retried |
| Retries | Up to three, with short increasing delays, only on 408, 502, 429 or 503 with Retry-After, and connection failures |
| Deduplication | X-Honeycomb-Webhook-Delivery-ID is unique per delivery and the same across retries |
| Source IPs | Not published |
Alerts and payloads
Honeycomb has no event-type field. What a notification is about depends on where you attached the webhook: a Trigger, one of the two kinds of SLO burn alert, or an Anomaly Detection service. The standard body contains "the result of the alert in JSON", but Honeycomb does not publish its fields, so check a real delivery before you depend on any of them.
If you want a body you control, open the integration's Payload tab and enable a template for each alert type you use. Templates use Go template syntax and Honeycomb's variables. Here is a small template for a Trigger:
{
"source": "honeycomb",
"kind": "trigger",
"instance_id": "{{ .Alert.InstanceID }}",
"status": "{{ .Alert.Status }}",
"summary": "{{ .Alert.Summary }}",
"is_test": {{ .Alert.IsTest }},
"trigger": "{{ .Name }}",
"environment": "{{ .Environment }}",
"datasets": {{ toJson .Datasets }},
"result_url": "{{ .Result.URL }}",
"evaluated_at": "{{ .Alert.Timestamp }}"
}
A few rules shape these templates:
.Alert.StatusisTRIGGEREDorOKfor Triggers and burn alerts, andDetectedorResolvedfor Anomaly Detection.- Array variables such as
.Datasetsand.Tagsmust be wrapped intoJson. Without it, Honeycomb fails to send with an error such asinvalid character 'e' in literal true. - A webhook that uses custom payloads can only be used by the alert types it has a payload for. A webhook with only a Trigger payload cannot be chosen for an SLO Budget Rate burn alert.
.Alert.InstanceIDidentifies one firing of a trigger or burn alert, and for Anomaly Detection one notification, which makes it useful for correlating related notifications..Recipient.Secretexposes the Shared Secret to the template. You do not need it when your receiver reads the header.
Inspect Honeycomb webhooks as they arrive. Point a webhook integration at a Hookdeck Inspect URL and fire a test alert to see the standard payload or your template's output, with no account or setup required.
Setting up Honeycomb webhooks
Generate a long random Shared Secret first, for example with openssl rand -hex 32.
- In Honeycomb, go to Team Settings and select Integrations.
- Under Trigger, SLOs, and Anomaly Detection Recipients, select Add Integration and choose Webhook as the provider.
- Enter a Name, your HTTPS Webhook URL and the Shared Secret.
- Optionally use the Payload, Variables and Headers tabs to customise the request.
- Select Add.
- Attach the webhook as a recipient: on a Trigger under Recipients > Add Recipient, on an SLO through Burn Alerts > New Burn Alert > Notify, or on an Anomaly Detection service with Add Recipient.
To route through Hookdeck, create a Hookdeck source with the Honeycomb type and use its URL as the Webhook URL. The source has one field, Shared Secret: paste the same value you set on the Honeycomb webhook. Hookdeck reads it from the X-Honeycomb-Webhook-Token header and compares the whole value.
Set a Shared Secret on the Honeycomb webhook. Webhooks without one send nothing to verify, so if you leave the secret empty you would have to leave verification off on the source too, and nothing would authenticate the requests.
Honeycomb's docs do not name the HTTP method its webhooks use, so the Hookdeck source pre-selects POST without managing the method. If your deliveries arrive with another method, add that method to the source's allowed HTTP methods.
Securing Honeycomb webhooks
Read X-Honeycomb-Webhook-Token, compare its whole value with your stored Shared Secret in constant time, and reject the request if either is missing.
const crypto = require('crypto');
const express = require('express');
const SHARED_SECRET = process.env.HONEYCOMB_SHARED_SECRET;
function safeEqual(received, expected) {
const a = Buffer.from(received, 'utf8');
const b = Buffer.from(expected, 'utf8');
// timingSafeEqual throws on unequal lengths, so check first.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const app = express();
app.post('/webhooks/honeycomb', express.raw({ type: '*/*' }), (req, res) => {
const received = req.get('x-honeycomb-webhook-token');
if (!SHARED_SECRET || typeof received !== 'string' || !safeEqual(received, SHARED_SECRET)) {
return res.sendStatus(401);
}
let alert;
try {
alert = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
const deliveryId = req.get('x-honeycomb-webhook-delivery-id');
// Deduplicate on deliveryId, store the alert, then process it asynchronously.
res.sendStatus(200);
});
The same check in Python:
import hmac
import os
SHARED_SECRET = os.environ.get("HONEYCOMB_SHARED_SECRET", "")
def verify_honeycomb_request(headers) -> bool:
if not SHARED_SECRET:
return False # never accept when unconfigured
received = headers.get("X-Honeycomb-Webhook-Token") # case-insensitive in Starlette and Flask
if not received:
return False
# Compare bytes: compare_digest raises TypeError on a non-ASCII str.
return hmac.compare_digest(received.encode("utf-8"), SHARED_SECRET.encode("utf-8"))
The Shared Secret is a static credential. It authenticates the sender, not the body, so a modified payload still passes, and with no timestamp a captured request can be replayed. Serve the endpoint over HTTPS only, use a long random secret, and change it on the integration and in your environment together if it might have leaked.
Make Honeycomb webhooks production-ready. Hookdeck Event Gateway checks the
X-Honeycomb-Webhook-Tokenheader, answers Honeycomb within its 15-second window, and queues every alert durably before your service sees it.
Honeycomb webhook limitations and pain points
A slow response loses the alert
The Problem: An alert never reaches your system, and Honeycomb does not try again.
Why It Happens: Honeycomb waits 15 seconds for a response. If none arrives, the delivery fails and Honeycomb does not retry it, because your endpoint may already have processed the request. It retries only 408, 502, 429 or 503 with Retry-After, and connection failures, at most three times. Every other 4xx and 5xx, including a 500 from a crashed handler, fails without a retry.
Workarounds:
- Respond straight after the credential check and do the real work afterwards.
- If your service is temporarily overloaded, answer
503with aRetry-Afterheader rather than a500, so Honeycomb treats it as retryable. Honeycomb does not wait for the duration you give. - Reconcile against Honeycomb's UI or API for alerts that matter, so a lost notification is noticed.
How Hookdeck Can Help: Hookdeck accepts each notification as soon as it arrives and stores it, so Honeycomb always gets a fast response. Hookdeck then delivers to your service with retries you configure, and you can replay any notification after a fix.
The standard payload is undocumented
The Problem: You cannot write a parser for the default body from the documentation alone, and you cannot tell from it which kind of alert fired.
Why It Happens: Honeycomb documents the template variables in detail but not the standard body, and there is no event-type field or header.
Workarounds:
- Enable a custom payload for each alert type you use, and include a constant such as
"kind": "trigger"alongside the variables you need. - Use one webhook integration per alert type if you prefer the standard body, so the receiving URL tells you what it is.
- Capture a real delivery before writing the handler.
How Hookdeck Can Help: Hookdeck keeps every request with its full headers and body, so you can see exactly what Honeycomb sent. Transformations can normalise the different shapes before they reach your service.
The Shared Secret is optional
The Problem: Requests arrive with no X-Honeycomb-Webhook-Token, or with an empty one, and anyone who learns the URL can send fake alerts.
Why It Happens: Honeycomb only asks for the Shared Secret as an optional field, and the header carries whatever was entered.
Workarounds:
- Always set a long random Shared Secret, and reject requests without a matching header.
- Do not copy the secret into the body with
.Recipient.Secret. The header already carries it, and bodies end up in more logs than headers do.
How Hookdeck Can Help: Hookdeck's Honeycomb source compares the header with the Shared Secret you enter. Hookdeck answers with a 200 either way, but requests that fail the check are recorded as failed verification and never reach your service.
Best practices
Deduplicate on the delivery ID
X-Honeycomb-Webhook-Delivery-ID stays the same across retries of the same delivery. Store it before processing and skip any delivery you have already handled. See our guide to webhook idempotency.
Put the state in the body
Include .Alert.Status, .Alert.InstanceID and .Alert.Timestamp in your templates. With them, your receiver can tell a firing alert from a recovery, group related notifications, and ignore one that arrives out of order.
Skip test alerts in production paths
Include .Alert.IsTest and route test notifications away from paging and ticketing, so testing a trigger never wakes anyone up.
Keep templates valid JSON
Wrap every array variable in toJson, and fire a test alert after each template change. A broken template fails the send, which you only find out about when an alert goes missing.
Acknowledge fast and process asynchronously
Verify the header, store the alert, return 200, and do the work afterwards. See why to process webhooks asynchronously.
Conclusion
Honeycomb sends alert notifications as JSON with the integration's Shared Secret in the X-Honeycomb-Webhook-Token header and no signature. Set a long random secret, compare the header in constant time over HTTPS, use custom payload templates so the body says what fired and in which state, deduplicate on X-Honeycomb-Webhook-Delivery-ID, and respond well inside 15 seconds, because a timeout is never retried.
Hookdeck Event Gateway checks the Shared Secret, answers Honeycomb immediately and keeps every alert until your service has processed it, so a slow deploy on your side does not cost you a notification.
Get started with Hookdeck for free and handle Honeycomb webhooks reliably in minutes.