Custom Webhooks
Custom Webhooks let Chglog send alert notifications to any HTTPS endpoint you control. Unlike Slack, Teams, or Discord integrations (which use platform-specific formats), Custom Webhooks deliver a clean JSON payload that you can parse with any language or framework.
Use Custom Webhooks to build your own integrations: forward alerts to SMS providers, trigger PagerDuty incidents, post to Telegram, or feed data into your internal tools.
Setting up
- Go to Notifications → Webhook Integrations.
- Click Add Webhook and choose Custom Webhook.
- Enter your HTTPS endpoint URL.
- Optionally add a Signing Secret to verify payloads via HMAC-SHA256.
- Click Add, then use the Test button to verify delivery.
How it works
When a check changes state (goes down, recovers, or remains down), Chglog sends an HTTP POST request to your endpoint with the event details as a JSON body.
Request headers
| Header | Description |
|---|---|
Content-Type | application/json |
User-Agent | Chglog/1.0 |
X-Chglog-Event | The event type (e.g. check.down) |
X-Chglog-Signature | HMAC-SHA256 signature (only if a signing secret is configured). Format: sha256=<hex-digest> |
Event types
| Event | Trigger |
|---|---|
check.down | Check confirmed down (after 3-strike verification) |
check.up | Check recovered after being down |
check.degraded | Check is still down (reminder alert) |
check.test | Test notification sent from the UI |
Payload format
{
"event": "check.down",
"title": "🔴 example.com is DOWN",
"message": "⬇️ *example.com* is down\nhttps://example.com\nDetected at Apr 4, 2026 2:15 PM from 🇩🇪 Frankfurt, DE",
"status": "down",
"color": "#ef4444",
"timestamp": "2026-04-04T14:15:00Z",
"details_url": "https://app.chglog.com/checks/example-com",
"check": "example.com",
"error": "Connection timeout after 10s",
"detected_at": "2026-04-04T14:15:00Z",
"confirmation_results": [
{
"status": "down",
"location": "Frankfurt, DE",
"checked_at": "2026-04-04T14:15:00Z",
"response_time": 0,
"error": "Connection timeout"
},
{
"status": "down",
"location": "New York, US",
"checked_at": "2026-04-04T14:15:02Z",
"response_time": 0,
"error": "Connection timeout"
}
]
}
Field reference
| Field | Type | Description |
|---|---|---|
event | string | Event type identifier |
title | string | Human-readable alert title |
message | string | Full alert message with context |
status | string|null | Check status: down, up, degraded |
color | string|null | Hex color code for the alert severity |
timestamp | string | ISO 8601 UTC timestamp |
details_url | string|null | Link to the check detail page |
check | string|null | Check name / label |
error | string|null | Error reason (for down events) |
detected_at | string|null | When the issue was first detected |
resolved_at | string|null | When the issue was resolved (recovery events) |
duration_seconds | int|null | Downtime duration in seconds (recovery events) |
confirmation_results | array|null | Multi-location verification results |
Verifying signatures (HMAC-SHA256)
If you configured a signing secret, every request includes an X-Chglog-Signature header. Use it to verify the payload was sent by Chglog and hasn't been tampered with:
- Read the raw request body (do not parse JSON first).
- Compute
HMAC-SHA256of the raw body using your signing secret. - Compare the result to the signature in the header (after stripping the
sha256=prefix). - Use a constant-time comparison to prevent timing attacks.
Receiver examples
PHP
<?php
// webhook-receiver.php
$secret = getenv('CHGLOG_WEBHOOK_SECRET');
$payload = file_get_contents('php://input');
$sigHeader = $_SERVER['HTTP_X_CHGLOG_SIGNATURE'] ?? '';
// Verify signature
$expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
if (!hash_equals($expected, $sigHeader)) {
http_response_code(403);
exit('Invalid signature');
}
$data = json_decode($payload, true);
$event = $data['event'] ?? '';
if ($event === 'check.down') {
// Handle down alert — send SMS, create incident, etc.
error_log("DOWN: {$data['check']} — {$data['error']}");
}
http_response_code(200);
echo json_encode(['ok' => true]);
Node.js (Express)
const crypto = require('crypto');
const express = require('express');
const app = express();
app.post('/webhook/chglog', express.raw({ type: '*/*' }), (req, res) => {
const secret = process.env.CHGLOG_WEBHOOK_SECRET;
const signature = req.headers['x-chglog-signature'] || '';
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
return res.status(403).json({ error: 'Invalid signature' });
}
const data = JSON.parse(req.body);
const event = req.headers['x-chglog-event'];
if (event === 'check.down') {
console.log(`DOWN: ${data.check} — ${data.error}`);
// Send SMS, page on-call, etc.
}
res.json({ ok: true });
});
app.listen(3000);
Python (Flask)
import hmac, hashlib, os, json
from flask import Flask, request, jsonify
app = Flask(__name__)
SECRET = os.environ['CHGLOG_WEBHOOK_SECRET']
@app.post('/webhook/chglog')
def chglog_webhook():
payload = request.get_data()
sig = request.headers.get('X-Chglog-Signature', '')
expected = 'sha256=' + hmac.new(
SECRET.encode(), payload, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, sig):
return jsonify(error='Invalid signature'), 403
data = json.loads(payload)
event = request.headers.get('X-Chglog-Event', '')
if event == 'check.down':
print(f"DOWN: {data['check']} — {data.get('error')}")
# Forward to SMS, PagerDuty, etc.
return jsonify(ok=True)
.NET (ASP.NET Core Minimal API)
using System.Security.Cryptography;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/webhook/chglog", async (HttpContext ctx) =>
{
var secret = Environment.GetEnvironmentVariable("CHGLOG_WEBHOOK_SECRET")!;
using var reader = new StreamReader(ctx.Request.Body);
var body = await reader.ReadToEndAsync();
var signature = ctx.Request.Headers["X-Chglog-Signature"].FirstOrDefault() ?? "";
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = Convert.ToHexStringLower(hmac.ComputeHash(Encoding.UTF8.GetBytes(body)));
var expected = $"sha256={hash}";
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(signature)))
{
ctx.Response.StatusCode = 403;
return Results.Json(new { error = "Invalid signature" });
}
// Parse and handle event
var data = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(body);
var evt = ctx.Request.Headers["X-Chglog-Event"].FirstOrDefault();
return Results.Json(new { ok = true });
});
app.Run();
SMS forwarding
Custom Webhooks are ideal for forwarding alerts to SMS services. Here are recipes for popular providers.
Twilio (PHP)
<?php
// After verifying the signature (see PHP example above):
$data = json_decode($payload, true);
if ($data['event'] === 'check.down') {
$ch = curl_init('https://api.twilio.com/2010-04-01/Accounts/'
. getenv('TWILIO_SID') . '/Messages.json');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_USERPWD => getenv('TWILIO_SID') . ':' . getenv('TWILIO_TOKEN'),
CURLOPT_POSTFIELDS => http_build_query([
'From' => getenv('TWILIO_FROM'),
'To' => getenv('ONCALL_PHONE'),
'Body' => "🔴 {$data['check']} is DOWN: {$data['error']}",
]),
CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
curl_close($ch);
}
Twilio (Node.js)
// After verifying the signature (see Node.js example above):
if (event === 'check.down') {
const twilio = require('twilio')(process.env.TWILIO_SID, process.env.TWILIO_TOKEN);
await twilio.messages.create({
from: process.env.TWILIO_FROM,
to: process.env.ONCALL_PHONE,
body: `🔴 ${data.check} is DOWN: ${data.error}`
});
}
Vonage / MessageBird
The same pattern works with any SMS API. After verifying the signature, extract the alert details and call the provider's HTTP API. Vonage and MessageBird both accept simple POST requests with to, from, and text fields.
Other integration ideas
- PagerDuty — POST to the PagerDuty Events API v2 with
routing_key,event_action: "trigger", and the check details as the summary. - Telegram Bot — Call
https://api.telegram.org/bot{token}/sendMessagewith your chat ID and the alert message. - Email-to-SMS gateways — Many carriers support email-to-SMS (e.g.
5551234567@txt.att.net). Forward the alert body via your mail server. - Incident management — Feed alerts into Opsgenie, Incident.io, or your own ticketing system via their webhook/API endpoints.
Security best practices
- Always verify the signature — never process unsigned payloads if you configured a secret.
- Use HTTPS — Chglog only sends to HTTPS endpoints. Never expose a plaintext HTTP receiver.
- Respond quickly — return a
2xxresponse within 10 seconds. Offload heavy processing to a queue. - Store secrets securely — use environment variables, not hardcoded strings.