Integrations

Outgoing Webhooks

When an incident is created or updated, Beacon sends HTTP POST requests to your configured webhook URLs with the full event payload. Use webhooks to integrate with Slack, PagerDuty, Discord, or any custom system that accepts HTTP callbacks.

Setting up webhooks

Navigate to your status page, open the Webhooks tab, and click Add Webhook. Enter the URL where Beacon should send event payloads.

After creation, Beacon generates a unique signing secret for the webhook. Copy and store this secret securely -- you will need it to verify webhook signatures.

The signing secret is only displayed once at creation time. If you lose it, delete the webhook and create a new one.

Events

Beacon sends webhooks for the following events:

Event Triggered when
incident.opened A new incident is created (manually or by auto-detection)
incident.updated An incident receives a new update, is resolved, or has its properties changed

Payload format

Every webhook request includes a JSON body with the full event context:

{
  "event": "incident.opened",
  "sent_at": "2026-04-04T14:32:00Z",
  "workspace": {
    "slug": "acme",
    "name": "Acme Inc"
  },
  "status_page": {
    "slug": "acme-cloud",
    "name": "Acme Cloud",
    "url": "https://usebeacon.pro/status/acme/acme-cloud"
  },
  "incident": {
    "id": "inc_01HX9Z...",
    "title": "API Health is down",
    "impact": "major",
    "state": "investigating",
    "resolved": false,
    "created_at": "2026-04-04T14:32:00Z",
    "updated_at": "2026-04-04T14:32:00Z",
    "updates": [
      {
        "id": "upd_01HX9Z...",
        "message": "Monitor detected failure: HTTP 503 returned",
        "created_at": "2026-04-04T14:32:00Z"
      }
    ]
  },
  "change": {
    "field": "state",
    "from": null,
    "to": "investigating"
  }
}

The change object describes what triggered the webhook. For incident.opened events, the from field is null. For updates, it shows the previous value.

Request details

Each webhook delivery is an HTTP POST with the following headers:

Header Value
Content-Type application/json
User-Agent Beacon-Webhook/1
X-Beacon-Event The event name, e.g. incident.opened
X-Beacon-Signature sha256={hex_signature}

Signature verification

Beacon signs every webhook payload with HMAC-SHA256 using the webhook's signing secret. The signature is sent in the X-Beacon-Signature header as sha256={hex_signature}. Always verify signatures to ensure the request came from Beacon.

PHP

$payload = file_get_contents('php://input');
$secret  = env('BEACON_WEBHOOK_SECRET');
$header  = $_SERVER['HTTP_X_BEACON_SIGNATURE'] ?? '';

$expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);

if (! hash_equals($expected, $header)) {
    http_response_code(403);
    exit('Invalid signature');
}

Node.js

import { createHmac, timingSafeEqual } from 'node:crypto';

function verifySignature(payload, secret, header) {
  const expected = 'sha256=' + createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(header),
  );
}

Ruby

require 'openssl'

payload  = request.body.read
secret   = ENV['BEACON_WEBHOOK_SECRET']
header   = request.env['HTTP_X_BEACON_SIGNATURE']

expected = "sha256=#{OpenSSL::HMAC.hexdigest('sha256', secret, payload)}"

unless Rack::Utils.secure_compare(expected, header)
  halt 403, 'Invalid signature'
end

Python

import hmac
import hashlib

payload  = request.get_data()
secret   = os.environ['BEACON_WEBHOOK_SECRET']
header   = request.headers.get('X-Beacon-Signature', '')

expected = 'sha256=' + hmac.new(
    secret.encode(), payload, hashlib.sha256
).hexdigest()

if not hmac.compare_digest(expected, header):
    abort(403)

Retry behavior

Beacon makes up to 3 delivery attempts per webhook event:

Response Behavior
2xx Success. No retry.
4xx Client error. No retry. Fix the issue on your end.
5xx Server error. Retried with exponential backoff.
Timeout No response within 10 seconds. Retried with backoff.

After 3 failed attempts, the delivery is abandoned. There is no manual retry mechanism at this time.

Timeout

Each webhook delivery has a 10-second timeout. If your server does not respond within 10 seconds, the attempt is considered failed and will be retried (if retries remain).

Tip: Respond with a 200 immediately and process the payload asynchronously. This avoids timeout issues even for complex integrations.

Best practices

  • Respond quickly — Return a 2xx status as fast as possible. Queue the payload for async processing if your handler needs to do significant work.
  • Verify signatures — Always check the X-Beacon-Signature header using a constant-time comparison function.
  • Filter by event type — Use the X-Beacon-Event header or the event field in the body to route different events to different handlers.
  • Handle duplicates — In rare cases (e.g., network issues), you may receive the same event twice. Use the incident id and updated_at to deduplicate.

Example: Slack integration

Forward Beacon webhook events to a Slack channel using a simple proxy script. First, create a Slack Incoming Webhook URL in your Slack workspace settings.

Then set up a lightweight endpoint that receives Beacon webhooks and forwards them to Slack:

// Express.js example
import express from 'express';

const app = express();
app.use(express.json());

app.post('/webhooks/beacon', async (req, res) => {
  // Respond immediately
  res.sendStatus(200);

  const { event, incident, status_page } = req.body;
  const emoji = event === 'incident.opened' ? ':red_circle:' : ':large_blue_circle:';
  const text = `${emoji} *${incident.title}*\n`
    + `Impact: ${incident.impact} | State: ${incident.state}\n`
    + `Page: ${status_page.url}`;

  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ text }),
  });
});