Webhook Alerts & JSON Payload Guide
Stream real-time certificate events, expiry milestones, renewals, and DNS outages directly into your DevOps channels, Slack, Discord, or custom backend services.
Standard JSON & Auto-Presets
Clean, predictable JSON envelope. When you paste a Slack or Discord webhook URL, ExpiryHawk automatically formats the payload natively.
HMAC-SHA256 Signatures
Protect your receiver from tampering and replay attacks using cryptographically signed headers (X-ExpiryHawk-Signature).
Strict Timeouts & Retries
10-second total request timeout with automatic background retries on server errors (5xx) and built-in SSRF private-network protection.
1. Adding a Webhook in 2 Minutes
Configure your destination endpoint in ExpiryHawk settings.
-
1
Navigate to Alert Channels
Go to your Profile > Alerts Settings and click Configure under the Webhook card.
-
2
Paste Target URL & Choose Format
Enter your HTTPS endpoint. If you paste a Slack (
hooks.slack.com) or Discord (discord.com/api/webhooks) URL, ExpiryHawk will automatically switch the payload preset for you. -
3
Generate a Secret & Send a Live Test
Click Generate Secret to enable HMAC-SHA256 authentication, then click Send Test Webhook to verify connectivity before saving.
2. Supported Payload Formats
Select the format matching your ingestion receiver.
Standard envelope containing event metadata, domain identifiers, and full certificate status:
{
"id": "evt_9f82a17cb03de458",
"event": "ssl.expiring",
"created_at": "2026-11-01T15:30:00Z",
"timestamp": 1793547000,
"data": {
"target": "https://api.example.com:443",
"domain": "api.example.com",
"protocol": "https",
"port": 443,
"monitor_type": "ssl_certificate",
"status": "expiring",
"milestone": "advance",
"days_left": 7,
"expiry_date": "2026-11-08T12:00:00Z",
"issuer": "Let's Encrypt E6",
"subject": "api.example.com",
"message": "Advance Notice: The SSL certificate for https://api.example.com:443 is approaching its expiration date (7 days remaining).",
"certificate": {
"issuer": "Let's Encrypt E6",
"subject": "api.example.com",
"serial_number": "04A1B2C3D4E5F67890",
"valid_from": "2026-08-10T12:00:00Z",
"valid_to": "2026-11-08T12:00:00Z"
},
"links": {
"domain_url": "https://expiryhawk.com/domain/42",
"dashboard_url": "https://expiryhawk.com/dashboard"
}
}
}
Formatted with color-coded severity borders (🟢 Green on renewed, ⚠️ Amber on 7-day reminder, 🔴 Red on expiration/NXDOMAIN) and one-click action buttons:
{
"text": "⚠️ [SSL Certificate Advance Notice] https://api.example.com:443 expires in 7 days",
"attachments": [
{
"color": "#F59E0B",
"blocks": [
{
"type": "header",
"text": { "type": "plain_text", "text": "⚠️ [SSL Certificate Advance Notice] https://api.example.com:443", "emoji": true }
},
{
"type": "section",
"text": { "type": "mrkdwn", "text": "Advance Notice: The SSL certificate for https://api.example.com:443 will expire in 7 days." }
},
{
"type": "section",
"fields": [
{ "type": "mrkdwn", "text": "*Target:*\n`https://api.example.com:443`" },
{ "type": "mrkdwn", "text": "*Status:*\n*EXPIRING*" },
{ "type": "mrkdwn", "text": "*Days Remaining:*\n7 days" },
{ "type": "mrkdwn", "text": "*Issuer:*\nLet's Encrypt E6" }
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": { "type": "plain_text", "text": "View in ExpiryHawk", "emoji": true },
"url": "https://expiryhawk.com/domain/42",
"style": "primary"
}
]
}
]
}
]
}
Formatted with clean visual embeds, inline fields, and color tags:
{
"username": "ExpiryHawk Alerts",
"avatar_url": "https://expiryhawk.com/assets/images/expiry-hawk-mark.png",
"embeds": [
{
"title": "⚠️ [SSL Certificate Advance Notice] https://api.example.com:443",
"description": "Advance Notice: The SSL certificate for https://api.example.com:443 is approaching expiration.",
"url": "https://expiryhawk.com/domain/42",
"color": 16096779,
"fields": [
{ "name": "Target", "value": "`https://api.example.com:443`", "inline": true },
{ "name": "Status", "value": "**EXPIRING**", "inline": true },
{ "name": "Days Remaining", "value": "7 days", "inline": true },
{ "name": "Issuer", "value": "Let's Encrypt E6", "inline": true },
{ "name": "Expires At", "value": "2026-11-08 12:00 UTC", "inline": true },
{ "name": "Monitor Type", "value": "SSL Certificate", "inline": true }
],
"footer": { "text": "ExpiryHawk • SSL & Domain Expiry Monitoring" },
"timestamp": "2026-11-01T15:30:00Z"
}
]
}
3. Event Types Matrix
Every webhook request includes an X-ExpiryHawk-Event header.
| Event Identifier | Trigger Condition | Default Severity |
|---|---|---|
| ssl.expiring | Certificate expiration within threshold (e.g. 30 days, 7 days, 1 day). | Warning (Amber) |
| ssl.expired | Certificate validity period has ended (days_left <= 0). |
Critical (Red) |
| ssl.renewed | New certificate detected with renewed serial number and extended validity. | Success (Green) |
| ssl.revoked | Certificate reported as revoked by its issuing CA via OCSP or CRL. | Critical (Red) |
| ssl.untrusted | Trust failure detected (self_signed, untrusted_root, or hostname_mismatch). |
Critical (Red) |
| ssl.connection_failed | TLS handshake or TCP connection to the endpoint failed after retries. | Critical (Red) |
| domain.expiring | Domain name registration expiration approaching via WHOIS/RDAP. | Warning (Amber) |
| domain.expired | Domain registration has expired. | Critical (Red) |
| dns.nxdomain | Domain resolution failure (domain does not resolve or NS missing). | Critical (Red) |
| ping.test | Triggered when clicking "Send Test Webhook" in settings. | Info (Blue) |
4. Verifying HMAC-SHA256 Signatures
Authenticate payloads and ensure they originated strictly from ExpiryHawk.
When a secret is configured, every outgoing HTTP POST request contains the following header:
X-ExpiryHawk-Signature: t=1793547000,v1=9b3a726ef341c28c8928498f...
t: Unix epoch timestamp when the webhook was signed.v1: Hexadecimal HMAC-SHA256 signature calculated over the string:${t}.${raw_payload_json}using your shared secret.- Replay Defense: Always reject requests where
|current_time - t| > 300(5 minutes).
const crypto = require('crypto');
function verifyExpiryHawkWebhook(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(signatureHeader.split(',').map(kv => kv.split('=')));
const timestamp = parseInt(parts.t, 10);
const signature = parts.v1;
if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > toleranceSeconds) {
throw new Error('Webhook timestamp outside allowed tolerance window.');
}
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const trusted = Buffer.from(expected, 'hex');
const provided = Buffer.from(signature, 'hex');
if (trusted.length !== provided.length || !crypto.timingSafeEqual(trusted, provided)) {
throw new Error('Invalid webhook HMAC signature.');
}
return true;
}
import hmac
import hashlib
import time
def verify_expiryhawk_webhook(raw_body: bytes, signature_header: str, secret: str, tolerance_seconds: int = 300) -> bool:
pairs = dict(item.split("=", 1) for item in signature_header.split(","))
timestamp = int(pairs.get("t", 0))
provided_sig = pairs.get("v1", "")
if abs(time.time() - timestamp) > tolerance_seconds:
raise ValueError("Webhook timestamp is expired or outside tolerance window.")
to_sign = f"{timestamp}.".encode("utf-8") + raw_body
expected_sig = hmac.new(secret.encode("utf-8"), to_sign, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected_sig, provided_sig):
raise ValueError("Invalid webhook HMAC signature.")
return True
package main
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"errors"
"fmt"
"strconv"
"strings"
"time"
)
func VerifyWebhook(rawBody []byte, header string, secret string) error {
parts := make(map[string]string)
for _, item := range strings.Split(header, ",") {
kv := strings.SplitN(item, "=", 2)
if len(kv) == 2 { parts[kv[0]] = kv[1] }
}
tInt, err := strconv.ParseInt(parts["t"], 10, 64)
if err != nil || time.Now().Unix()-tInt > 300 || tInt-time.Now().Unix() > 300 {
return errors.New("timestamp drift exceeded")
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(fmt.Sprintf("%d.", tInt)))
mac.Write(rawBody)
expected := hex.EncodeToString(mac.Sum(nil))
if subtle.ConstantTimeCompare([]byte(expected), []byte(parts["v1"])) != 1 {
return errors.New("invalid signature")
}
return nil
}
5. Delivery Guarantees & Timeouts
Understanding response codes, timeouts, and background worker queue behavior.
Response Expectations
Your endpoint must return an HTTP status code in the 2xx range (200 - 204) within 10 seconds.
Retries & Error Handling
If your receiver returns a transient error (500, 502, 503, 504 or connection timeouts), ExpiryHawk's Minion queue will retry delivery with exponential backoff up to 3 times (+1m, +5m, +15m).
Client errors (400, 401, 403, 404) are treated as permanent configuration errors and will not be retried.
Ready to integrate your first Webhook?
Configure your Slack, Discord, or custom microservice endpoint in your alerts dashboard now.