Send Honeycomb Trigger Alerts to Your Team's Phones
Authenticate one Honeycomb Trigger webhook, accept only firing or OK transitions, and send fixed privacy-minimal copy to a shared PingRoom Room.

To send one Honeycomb Trigger transition to your team's phones, create a custom Honeycomb webhook with a shared secret, accept only one Trigger and environment at a narrow relay, and forward fixed TRIGGERED or OK copy to a PingRoom incoming webhook. Keep the query, threshold, groups, descriptions, datasets, and investigation links in Honeycomb.
This is a webhook recipe written by PingRoom, not a native Honeycomb integration. The destination Room's owner needs PingRoom Pro. Honeycomb remains the source of truth, and PingRoom should not replace an established pager, escalation policy, or incident record for critical systems.
Choose what owns the response
Honeycomb can expose rich Trigger context to a webhook. A phone does not need all of it.
| The team needs | Better route | Why |
|---|---|---|
| One short shared signal for one production Trigger | Honeycomb webhook → relay → PingRoom Room | A stable group sees a bounded state change without copying observability details to lock screens. |
| Query results, groups, threshold, and investigation | Honeycomb | Honeycomb owns the Trigger evaluation and diagnostic context. |
| Acknowledgement, schedules, escalation, and paging | The team's incident platform | PingRoom is not a guaranteed pager or incident-management system. |
| Several Triggers with different owners or response rules | A maintained routing service | One small relay should not become an unreviewed alert policy engine. |
This guide uses the first route. It accepts one Trigger ID, one environment, and only the TRIGGERED and OK states.
For an illustrative setup, imagine a small team that wants one private Room to know when a production latency Trigger changes state. Pingo the Pigeon, PingRoom's fictional CMO, appears in the artwork beside the gate. He cannot access Honeycomb, enter the private Room, change the Trigger, or send Pings. The human team owns every setting and claim.
Create the PingRoom connection
Create or choose a private Room for the people who should receive this signal. Open Settings → Connections → From another app, add an incoming webhook, and leave Quick Ping on Custom message. Use Test once, then copy the complete webhook URL into protected server-side configuration.
The incoming-webhook walkthrough covers the Room setup. The webhook reference lists the current request fields, idempotency behavior, cooldown, rate limits, and errors.
Treat the complete PingRoom URL as a credential. Store it as PINGROOM_WEBHOOK_URL. Do not put it in source control, client-side code, screenshots, or logs.
The relay sends only one of these fixed messages:
{ "action": null, "title": "Honeycomb trigger firing", "message": "Open Honeycomb to investigate the selected production trigger" }
{ "action": null, "title": "Honeycomb trigger returned to OK", "message": "Open Honeycomb to verify the selected trigger and service health" }
OK is Honeycomb's Trigger state. It does not prove the service recovered, that every group is healthy, or that a person acknowledged the alert.
Create one Honeycomb webhook recipient
In Honeycomb, open Team Settings → Integrations. Under Trigger, SLOs, and Anomaly Detection Recipients, choose Add Integration, select Webhook, and enter:
- a recognizable name;
- the relay's public HTTPS URL; and
- a new high-entropy shared secret.
Honeycomb sends that secret in X-Honeycomb-Webhook-Token. Store the same value as HONEYCOMB_WEBHOOK_TOKEN at the relay. Generate at least 32 printable characters, keep it out of logs, and rotate it if exposed.
In the recipient's Payload tab, enable the Trigger payload and use this narrow template:
{ "trigger_id": "{{ .ID }}", "environment": "{{ .Environment }}", "instance_id": "{{ .Alert.InstanceID }}", "status": "{{ .Alert.Status }}", "is_test": {{ .Alert.IsTest }}, "timestamp": "{{ .Alert.Timestamp }}" }
This excludes the Trigger name, description, datasets, groups, result values, summary, and URLs. Honeycomb's webhook documentation explains recipient creation, custom payloads, headers, delivery timing, and retries. Its variable reference defines the fields used here.
Add the recipient to one existing Trigger: open Triggers, choose the Trigger, add the webhook in Recipients, then save. Record the Trigger's stable ID and exact environment name as EXPECTED_HONEYCOMB_TRIGGER_ID and EXPECTED_HONEYCOMB_ENVIRONMENT at the relay.
Do not attach the recipient to several Triggers just because the template works. Review one route, owner, threshold, and response first. Honeycomb's Trigger setup guide remains the authority for query and threshold design.
Authenticate, filter, and deduplicate at the relay
Honeycomb gives each delivery an X-Honeycomb-Webhook-Delivery-ID. The value stays the same across retries of that delivery. The relay hashes it with the expected Trigger ID to create a bounded PingRoom Idempotency-Key, so an exact Honeycomb retry reuses one delivery identity without exposing the source ID.
This dependency-free Node handler also compares the shared secret in constant time, rejects Honeycomb test notifications, refuses redirects, and hashes the alert instance into a correlation ID. It never maps arbitrary source text into a Ping.
import { createHash, timingSafeEqual } from "node:crypto"; const MAX_BODY_BYTES = 16 * 1024; const ALLOWED_STATUSES = new Set(["TRIGGERED", "OK"]); function required(name, env) { const value = env[name]; if (typeof value !== "string" || value.length === 0) { throw new Error(`Missing ${name}`); } return value; } function validSharedSecret(supplied, expected) { if (!/^[\x21-\x7E]{32,128}$/.test(expected)) { throw new Error( "HONEYCOMB_WEBHOOK_TOKEN must contain 32–128 printable ASCII characters", ); } const received = Buffer.from(supplied ?? "", "utf8"); const wanted = Buffer.from(expected, "utf8"); return received.length === wanted.length && timingSafeEqual(received, wanted); } function sha256(value) { return createHash("sha256").update(value, "utf8").digest("hex"); } function fixedPing(status) { return status === "TRIGGERED" ? { action: null, title: "Honeycomb trigger firing", message: "Open Honeycomb to investigate the selected production trigger", } : { action: null, title: "Honeycomb trigger returned to OK", message: "Open Honeycomb to verify the selected trigger and service health", }; } function ignored() { return new Response(null, { status: 204 }); } export async function handleHoneycombRequest(request, env = process.env) { if (request.method !== "POST") { return new Response("Method Not Allowed", { status: 405, headers: { Allow: "POST" }, }); } if ( !validSharedSecret( request.headers.get("x-honeycomb-webhook-token"), required("HONEYCOMB_WEBHOOK_TOKEN", env), ) ) { return new Response("Unauthorized", { status: 401 }); } const declaredLength = Number(request.headers.get("content-length")); if (Number.isFinite(declaredLength) && declaredLength > MAX_BODY_BYTES) { return new Response("Payload Too Large", { status: 413 }); } const rawBody = Buffer.from(await request.arrayBuffer()); if (rawBody.length > MAX_BODY_BYTES) { return new Response("Payload Too Large", { status: 413 }); } let payload; try { payload = JSON.parse(rawBody.toString("utf8")); } catch { return new Response("Bad Request", { status: 400 }); } if ( payload?.trigger_id !== required("EXPECTED_HONEYCOMB_TRIGGER_ID", env) || payload?.environment !== required("EXPECTED_HONEYCOMB_ENVIRONMENT", env) || payload?.is_test !== false ) { return ignored(); } if (!ALLOWED_STATUSES.has(payload?.status)) { return ignored(); } const deliveryId = request.headers.get("x-honeycomb-webhook-delivery-id"); const instanceId = payload?.instance_id; if ( typeof deliveryId !== "string" || deliveryId.length < 1 || deliveryId.length > 256 || /[\u0000-\u001F\u007F]/.test(deliveryId) || typeof instanceId !== "string" || instanceId.length < 1 || instanceId.length > 256 ) { return new Response("Missing delivery identity", { status: 422 }); } const webhookUrl = new URL(required("PINGROOM_WEBHOOK_URL", env)); if (webhookUrl.protocol !== "https:") { throw new Error("PINGROOM_WEBHOOK_URL must use HTTPS"); } const triggerId = payload.trigger_id; const idempotencyKey = `honeycomb:${sha256(`${triggerId}:${deliveryId}`)}`; const correlationId = `honeycomb:${sha256(`${triggerId}:${instanceId}`)}`; let response; try { response = await fetch(webhookUrl, { method: "POST", redirect: "error", headers: { "Content-Type": "application/json", "Idempotency-Key": idempotencyKey, }, body: JSON.stringify({ ...fixedPing(payload.status), correlation_id: correlationId, }), }); } catch { return new Response("Bad Gateway", { status: 502 }); } const result = await response.json().catch(() => null); if (response.ok && result?.success === true) { return Response.json({ delivered: true }, { status: 202 }); } if ([429, 503].includes(response.status)) { const retryAfter = response.headers.get("retry-after"); return new Response("Temporarily unavailable", { status: response.status, headers: retryAfter ? { "Retry-After": retryAfter } : {}, }); } if (response.status === 502) { return new Response("Bad Gateway", { status: 502 }); } return new Response("PingRoom delivery failed", { status: 500 }); }
Wire the handler to the HTTPS URL entered in Honeycomb. Use a runtime with the standard Request, Response, and fetch APIs, reject oversized bodies at the edge where possible, and keep raw bodies and headers out of logs.

Match Honeycomb's retry contract
Honeycomb expects a response within 15 seconds. It does not retry a timeout because the receiver may already have processed the request. Keep this handler fast; move slow work to a durable queue if your deployment needs it.
Honeycomb documents up to three retries for connection failures, 408, 502, and a 429 or 503 that includes Retry-After. It does not retry every other 4xx or 5xx. The handler therefore:
- returns
202only after PingRoom reports success; - returns
204for an authenticated event outside the narrow allowlist; - returns
502for a network or gateway failure; - preserves eligible
429/503retry permission andRetry-After; and - returns
500for a non-retryable PingRoom rejection.
Honeycomb says it treats Retry-After as permission but does not wait for that duration. Do not use this path as an unlimited delivery queue. If a missed shared signal is unacceptable, keep the established incident route as the reliable response owner.
Rehearse the boundaries
Use Honeycomb's test controls and a non-sensitive test Trigger. Because the production relay deliberately rejects .Alert.IsTest, first confirm at the relay log or a temporary isolated test route that Honeycomb reached the endpoint and the shared secret matched. Then use a controlled real evaluation or a local fixture to check the full production path.
Before enabling the recipient, verify all of these cases:
- One matching
TRIGGEREDdelivery produces the fixed firing Ping. - One matching
OKdelivery produces the fixed OK Ping, without claiming recovery. - Replaying one delivery reuses the same idempotency key and body.
- A wrong secret, method, oversized body, or malformed JSON is rejected.
- A different Trigger, environment, test flag, or status sends nothing.
- A missing delivery or alert-instance identity fails instead of inventing one.
- A PingRoom redirect is refused; eligible temporary failures preserve Honeycomb's retry signal.
- A locked test phone shows only the approved fixed copy.
The local fixture for this article passed the first seven deterministic checks against a mocked PingRoom endpoint. It also confirmed that representative Trigger descriptions, dataset names, Honeycomb URLs, and user text never entered the outbound body. No live Honeycomb organization, PingRoom Room, or physical phone was used.
Read the result conservatively
The useful claim is: Honeycomb sent an authenticated delivery for the selected Trigger, the relay matched its environment and state, and PingRoom accepted the bounded shared signal. That does not mean a particular phone displayed it, the service is unhealthy or recovered, someone acknowledged it, or an incident was resolved.
For ownership and severity design, use the broader monitoring-alert guide. Keep Trigger evaluation and investigation in Honeycomb, and keep critical paging in the system that owns escalation.
When the small shared-signal case fits, open PingRoom Rooms, create the private destination Room, deploy the relay, and pass the boundary checks before attaching the production Trigger.
PingRoom
Short-form communication.


