# 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.

- Published: 2026-11-02
- Author: PingRoom
- Category: Developers
- Topics: Honeycomb push notifications, Honeycomb webhook phone alert, Honeycomb Trigger notifications, webhook push notifications
- Canonical: https://pingroom.io/blog/honeycomb-trigger-push-notifications
- Cover image: https://pingroom.io/blog/honeycomb-trigger-push-notifications-cover.webp
- Cover description: Two PingRoom avatars inspect one terracotta signal at a brass gate while Pingo watches beside an amber light.

**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](/blog/webhook-to-push-notification) covers the Room setup. The [webhook reference](/documents/webhooks) 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:

```json
{
  "action": null,
  "title": "Honeycomb trigger firing",
  "message": "Open Honeycomb to investigate the selected production trigger"
}
```

```json
{
  "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:

```json
{
  "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](https://docs.honeycomb.io/notify/webhooks/) explains recipient creation, custom payloads, headers, delivery timing, and retries. Its [variable reference](https://docs.honeycomb.io/notify/webhooks/variables/) 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](https://docs.honeycomb.io/notify/triggers/create/) 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.

```js
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.

![Two PingRoom avatars sort firing, OK, duplicate, and rejected signal tokens at a wooden bench while Pingo observes.](/blog/honeycomb-trigger-push-notifications-1.webp)

## 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 `202` only after PingRoom reports success;
- returns `204` for an authenticated event outside the narrow allowlist;
- returns `502` for a network or gateway failure;
- preserves eligible `429`/`503` retry permission and `Retry-After`; and
- returns `500` for 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:

1. One matching `TRIGGERED` delivery produces the fixed firing Ping.
2. One matching `OK` delivery produces the fixed OK Ping, without claiming recovery.
3. Replaying one delivery reuses the same idempotency key and body.
4. A wrong secret, method, oversized body, or malformed JSON is rejected.
5. A different Trigger, environment, test flag, or status sends nothing.
6. A missing delivery or alert-instance identity fails instead of inventing one.
7. A PingRoom redirect is refused; eligible temporary failures preserve Honeycomb's retry signal.
8. 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](/blog/monitoring-alerts-for-teams). 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](/app/rooms), create the private destination Room, deploy the relay, and pass the boundary checks before attaching the production Trigger.
