title: "Generic webhooks" description: "POST BoxWatch alert events to any HTTPS endpoint. Custom headers, alert-type filtering, no platform lock-in." last_updated: "2026-05-24"
Generic webhooks
Generic webhooks POST alert events as JSON to any HTTPS endpoint you control. Use them to wire BoxWatch into PagerDuty, Opsgenie, internal alert routers, JIRA ticket creators, or a custom Discord bot that needs more than one webhook can deliver.
Set up
- Go to Dashboard → Account → Notifications → Webhook endpoints.
- Click New endpoint.
- Give it a Name (e.g. "PagerDuty events v2").
- Paste the URL — must be HTTPS.
- Optionally add Headers as a JSON object, e.g.
{"Authorization": "Bearer xyz", "X-Source": "boxwatch"}. Sent on every request. - Optionally restrict Alert types — a comma-separated list of types this endpoint should receive. Leave empty to receive everything.
- Save. Click Test to fire a synthetic alert at the URL.
You can configure as many endpoints as you want. Each one is filtered independently.
Payload format
BoxWatch POSTs JSON with Content-Type: application/json and a User-Agent: BoxWatch-Webhook/1.0 header (plus any custom headers you configured).
{
"event": "alert.triggered",
"timestamp": "2026-05-24T03:14:22.418Z",
"alert": {
"type": "uptime_down",
"message": "api.example.com: uptime_down"
},
"server": {
"id": 14,
"hostname": "db-prod-1",
"ip": "10.0.1.14",
"group": "production"
},
"account": {
"email": "[email protected]"
}
}Fields:
event— alwaysalert.triggeredfor real alerts,alert.testfor the Test button.timestamp— ISO 8601 UTC.alert.type— one of the alert types listed below.alert.message— human-readable line, "<entity>: <type>".server— the server context for the alert. For cron and uptime alerts that aren't tied to a specific server,idmay be null andhostnamefalls back to the entity name.server.group— the server group's name, or null if unassigned.account.email— the account email that owns the alert.
Alert types
| Surface | Types |
|---|---|
| Server metrics | server_offline, cpu_high, memory_high, disk_high |
| Cron heartbeats | cron_missed, cron_failing, cron_stuck, cron_running_long |
| Processes | process_down, process_restarted, process_cpu_high, process_memory_high |
| Uptime | uptime_down, uptime_recovery, uptime_cert_expiring |
To filter a webhook to a subset, set the Alert types field to a JSON array via the API or comma-separated list in the dashboard. Example: ["uptime_down", "uptime_recovery"] for a status-page integration.
Authentication
Add a header in the endpoint config:
{ "Authorization": "Bearer YOUR_TOKEN" }BoxWatch sends it verbatim on every request. There's no per-payload signing today — auth is bearer-token via header.
HTTPS only. Plain http:// URLs are rejected at save time. Your endpoint should validate the bearer token (or use a path token in the URL) — there's no IP allowlist for BoxWatch's outbound webhooks.
Delivery semantics
- Timeout: 10 seconds. Anything slower is treated as a failure.
- Retries: none. BoxWatch logs the error and moves on. Build your receiver to be reliable, or use the alert history API to backfill.
- Concurrency: each endpoint is delivered sequentially within an alert dispatch. Multiple alerts firing in the same tick are not batched.
- Response handling: any
2xxis success. BoxWatch recordslast_triggeredon success. Non-2xxresponses are logged but don't block other channels.
Example receivers
Node.js / Express
import express from 'express';
const app = express();
app.use(express.json());
app.post('/boxwatch-webhook', (req, res) => {
const { event, alert, server } = req.body;
if (req.headers.authorization !== `Bearer ${process.env.BW_TOKEN}`) {
return res.status(401).end();
}
console.log(`[${event}] ${alert.type} on ${server.hostname}: ${alert.message}`);
// forward to PagerDuty / write to DB / post to Slack bot / etc.
res.status(200).json({ ok: true });
});
app.listen(8080);Python / Flask
from flask import Flask, request, abort
import os
app = Flask(__name__)
@app.post("/boxwatch-webhook")
def receive():
if request.headers.get("Authorization") != f"Bearer {os.environ['BW_TOKEN']}":
abort(401)
body = request.json
print(f"{body['alert']['type']} on {body['server']['hostname']}")
return {"ok": True}, 200Forwarding to PagerDuty Events API v2
app.post('/boxwatch-webhook', async (req, res) => {
const { alert, server } = req.body;
await fetch('https://events.pagerduty.com/v2/enqueue', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
routing_key: process.env.PD_ROUTING_KEY,
event_action: alert.type.endsWith('_recovery') ? 'resolve' : 'trigger',
dedup_key: `boxwatch-${server.id}-${alert.type}`,
payload: {
summary: alert.message,
source: server.hostname,
severity: alert.type.includes('down') ? 'critical' : 'warning',
},
}),
});
res.status(200).end();
});API
Manage endpoints programmatically:
Create body:
{
"name": "PagerDuty events v2",
"url": "https://hooks.example.com/boxwatch",
"headers": { "Authorization": "Bearer abc123" },
"alert_types": ["uptime_down", "uptime_recovery"],
"enabled": true
}(API reference pages are coming soon — see /docs/api for the overview.)
Troubleshooting
- Test button returns 4xx/5xx — BoxWatch displays the status code. 401/403 = your auth check is rejecting the test. 502/504 = your service isn't running. 200 with a body but failure = check the path / method.
- Timeouts — your endpoint must reply within 10s. If you're doing heavy work, accept the webhook into a queue and process asynchronously.
- Missing alerts — verify the endpoint's
alert_typesfilter actually includes what you expect. An empty list means no filtering; a populated list is an allow-list.
See also
- Alerts overview
- Slack and Discord — for one-line setup with no receiver to write