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

  1. Go to Dashboard → Account → Notifications → Webhook endpoints.
  2. Click New endpoint.
  3. Give it a Name (e.g. "PagerDuty events v2").
  4. Paste the URL — must be HTTPS.
  5. Optionally add Headers as a JSON object, e.g. {"Authorization": "Bearer xyz", "X-Source": "boxwatch"}. Sent on every request.
  6. Optionally restrict Alert types — a comma-separated list of types this endpoint should receive. Leave empty to receive everything.
  7. 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 — always alert.triggered for real alerts, alert.test for 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, id may be null and hostname falls 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

SurfaceTypes
Server metricsserver_offline, cpu_high, memory_high, disk_high
Cron heartbeatscron_missed, cron_failing, cron_stuck, cron_running_long
Processesprocess_down, process_restarted, process_cpu_high, process_memory_high
Uptimeuptime_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 2xx is success. BoxWatch records last_triggered on success. Non-2xx responses 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}, 200

Forwarding 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:

GET/webhooks
Auth: bearer
POST/webhooks
Auth: bearer
PUT/webhooks/:id
Auth: bearer
DELETE/webhooks/:id
Auth: bearer
POST/webhooks/:id/test
Auth: bearer

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_types filter actually includes what you expect. An empty list means no filtering; a populated list is an allow-list.

See also

Was this page helpful?