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.
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).
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-Signatureheader using a constant-time comparison function. - Filter by event type — Use the
X-Beacon-Eventheader or theeventfield 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
idandupdated_atto 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 }),
});
});