Skip to content

Custom Webhooks

Send alerts to beeps from any system using HTTP POST requests.

beeps relays accept webhook POST requests at:

https://hooks.beeps.dev/YOUR_WEBHOOK_ID

Send a simple alert:

Terminal window
curl -X POST https://hooks.beeps.dev/YOUR_WEBHOOK_ID \
-H "Content-Type: application/json" \
-d '{
"title": "Database Connection Failed",
"message": "Unable to connect to production database",
"severity": "critical"
}'
  • title (string): Short alert title (max 255 characters)
  • message (string): Detailed alert description
  • severity (string): Alert severity level, case-insensitive
    • low / info
    • medium / warning
    • high / error
    • critical / fatal
    • Defaults to medium when omitted. Unrecognized values also default to medium — the alert is still created so nothing is dropped, and the response includes a warnings array describing the problem.
  • source (string): System generating the alert
  • timestamp (string): ISO 8601 timestamp, stored with the alert’s raw payload (alert time is set at ingestion)
  • metadata (object): Additional key-value pairs

beeps parses the request body as JSON regardless of the Content-Type header, so curl -d without an explicit header still produces structured alerts. Bodies that aren’t valid JSON create an alert titled from the first line of the text, with a warning in the response.

beeps auto-detects known monitoring payloads (Sentry, Datadog, Axiom, Prometheus) by shape. If your custom payloads use field names that collide with those formats (like alertType or receiver), pin the webhook to the custom format so detection never rewrites your alerts:

Terminal window
beeps webhook set-provider --relay-id rly_abc123 --webhook-id wh_def456 --provider custom

Pin to a specific provider (sentry, datadog, axiom, prometheus) to skip detection for that tool, or auto to restore detection.

Successful deliveries return HTTP 200 with the created alert IDs:

{
"success": true,
"alertsCreated": 1,
"alertIds": ["alr_abc123"]
}

If the payload had problems that beeps worked around (like an unrecognized severity), the response includes warnings:

{
"success": true,
"alertsCreated": 1,
"alertIds": ["alr_abc123"],
"warnings": [
"unknown severity \"sev1\"; defaulted to \"medium\". Valid values: critical, high, medium, low, info (aliases: warning, error, fatal)"
]
}

Batches are capped at 100 alerts per request; anything beyond the first 100 is dropped with a warning, so split large batches across requests.

{
"title": "High API Error Rate",
"message": "Production API error rate exceeded 5% threshold. Current rate: 8.2%",
"severity": "critical",
"source": "monitoring-system",
"timestamp": "2024-10-27T10:30:00Z",
"metadata": {
"environment": "production",
"service": "api",
"region": "us-east-1",
"error_rate": 8.2,
"threshold": 5.0,
"dashboard_url": "https://dashboard.example.com/api",
"runbook_url": "https://wiki.example.com/runbooks/high-error-rate"
}
}
import requests
from datetime import datetime
def send_alert(title, message, severity="high"):
webhook_url = "https://hooks.beeps.dev/YOUR_WEBHOOK_ID"
payload = {
"title": title,
"message": message,
"severity": severity,
"source": "python-app",
"timestamp": datetime.utcnow().isoformat() + "Z",
"metadata": {
"host": "app-server-01",
"pid": 12345
}
}
response = requests.post(webhook_url, json=payload)
response.raise_for_status()
return response.json()
send_alert(
"Database Connection Failed",
"Could not connect to PostgreSQL after 3 retries",
severity="critical"
)
async function sendAlert(title, message, severity = "high") {
const webhookUrl = "https://hooks.beeps.dev/YOUR_WEBHOOK_ID";
const payload = {
title,
message,
severity,
source: "nodejs-app",
timestamp: new Date().toISOString(),
metadata: {
host: process.env.HOSTNAME,
pid: process.pid,
},
};
const response = await fetch(webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Failed to send alert: ${response.statusText}`);
}
return response.json();
}
await sendAlert(
"High Memory Usage",
"Application memory usage exceeded 90%",
"warning",
);
package main
import (
"bytes"
"encoding/json"
"net/http"
"time"
)
type Alert struct {
Title string `json:"title"`
Message string `json:"message"`
Severity string `json:"severity"`
Source string `json:"source"`
Timestamp string `json:"timestamp"`
Metadata map[string]interface{} `json:"metadata"`
}
func sendAlert(title, message, severity string) error {
webhookURL := "https://hooks.beeps.dev/YOUR_WEBHOOK_ID"
alert := Alert{
Title: title,
Message: message,
Severity: severity,
Source: "go-app",
Timestamp: time.Now().UTC().Format(time.RFC3339),
Metadata: map[string]interface{}{
"host": "app-server-01",
"version": "1.2.3",
},
}
payload, err := json.Marshal(alert)
if err != nil {
return err
}
resp, err := http.Post(webhookURL, "application/json", bytes.NewBuffer(payload))
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
func main() {
sendAlert(
"Service Unavailable",
"Health check failed for payment service",
"critical",
)
}
require 'net/http'
require 'json'
require 'uri'
def send_alert(title, message, severity: 'high')
webhook_url = URI('https://hooks.beeps.dev/YOUR_WEBHOOK_ID')
payload = {
title: title,
message: message,
severity: severity,
source: 'ruby-app',
timestamp: Time.now.utc.iso8601,
metadata: {
host: `hostname`.strip,
ruby_version: RUBY_VERSION
}
}
http = Net::HTTP.new(webhook_url.host, webhook_url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(webhook_url)
request['Content-Type'] = 'application/json'
request.body = payload.to_json
response = http.request(request)
raise "Failed: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
JSON.parse(response.body)
end
send_alert(
'Cache Miss Rate High',
'Redis cache miss rate exceeded 50%',
severity: 'warning'
)
#!/bin/bash
WEBHOOK_URL="https://hooks.beeps.dev/YOUR_WEBHOOK_ID"
send_alert() {
local title="$1"
local message="$2"
local severity="${3:-high}"
curl -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d @- <<EOF
{
"title": "$title",
"message": "$message",
"severity": "$severity",
"source": "bash-script",
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"metadata": {
"host": "$(hostname)",
"user": "$USER",
"script": "$0"
}
}
EOF
}
send_alert \
"Backup Failed" \
"Nightly database backup failed with exit code 1" \
"critical"
#!/bin/bash
WEBHOOK_URL="https://hooks.beeps.dev/YOUR_WEBHOOK_ID"
JOB_NAME="database-backup"
run_job() {
# Run your job
/usr/local/bin/backup-database.sh
return $?
}
notify_failure() {
curl -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d @- <<EOF
{
"title": "Cron Job Failed: $JOB_NAME",
"message": "Job failed with exit code $1",
"severity": "critical",
"source": "cron",
"metadata": {
"job": "$JOB_NAME",
"exit_code": $1,
"host": "$(hostname)",
"time": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
}
EOF
}
if run_job; then
echo "Job succeeded"
else
exit_code=$?
notify_failure $exit_code
exit $exit_code
fi
const WEBHOOK_URL = "https://hooks.beeps.dev/YOUR_WEBHOOK_ID";
const SERVICES = [
{ name: "api", url: "https://api.example.com/health" },
{ name: "database", url: "https://db.example.com/ping" },
{ name: "cache", url: "https://cache.example.com/status" },
];
async function checkHealth() {
for (const service of SERVICES) {
try {
const response = await fetch(service.url, { timeout: 5000 });
if (!response.ok) {
await sendAlert({
title: `${service.name} health check failed`,
message: `HTTP ${response.status}: ${response.statusText}`,
severity: "critical",
source: "health-check",
metadata: {
service: service.name,
url: service.url,
status_code: response.status,
},
});
}
} catch (error) {
await sendAlert({
title: `${service.name} unreachable`,
message: error.message,
severity: "critical",
source: "health-check",
metadata: {
service: service.name,
url: service.url,
error: error.message,
},
});
}
}
}
async function sendAlert(payload) {
await fetch(WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...payload,
timestamp: new Date().toISOString(),
}),
});
}
setInterval(checkHealth, 60000);
import * as tls from "tls";
import * as https from "https";
const WEBHOOK_URL = "https://hooks.beeps.dev/YOUR_WEBHOOK_ID";
const WARNING_DAYS = 30;
const DOMAINS = ["api.example.com", "www.example.com", "app.example.com"];
interface CertificateInfo {
domain: string;
validTo: Date;
daysRemaining: number;
issuer: string;
subject: string;
}
async function checkCertificate(domain: string): Promise<CertificateInfo> {
return new Promise((resolve, reject) => {
const options = {
host: domain,
port: 443,
method: "GET",
rejectUnauthorized: false,
agent: false,
};
const req = https.request(options, (res) => {
const cert = res.socket.getPeerCertificate();
if (!cert || Object.keys(cert).length === 0) {
reject(new Error("No certificate found"));
return;
}
const validTo = new Date(cert.valid_to);
const now = new Date();
const daysRemaining = Math.floor(
(validTo.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),
);
resolve({
domain,
validTo,
daysRemaining,
issuer: cert.issuer.O || "Unknown",
subject: cert.subject.CN || domain,
});
});
req.on("error", reject);
req.end();
});
}
async function sendAlert(cert: CertificateInfo) {
const severity = cert.daysRemaining <= 7 ? "critical" : "warning";
const payload = {
title: `SSL Certificate Expiring Soon: ${cert.domain}`,
message: `SSL certificate for ${cert.domain} expires in ${cert.daysRemaining} days (${cert.validTo.toISOString()})`,
severity,
source: "ssl-monitor",
timestamp: new Date().toISOString(),
metadata: {
domain: cert.domain,
days_remaining: cert.daysRemaining,
expiration_date: cert.validTo.toISOString(),
issuer: cert.issuer,
subject: cert.subject,
},
};
try {
const response = await fetch(WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
console.error(`Failed to send alert: ${response.statusText}`);
} else {
console.log(`Alert sent for ${cert.domain}`);
}
} catch (error) {
console.error(`Error sending alert:`, error);
}
}
async function checkAllCertificates() {
console.log(`Checking ${DOMAINS.length} certificates...`);
for (const domain of DOMAINS) {
try {
const cert = await checkCertificate(domain);
console.log(
`${domain}: ${cert.daysRemaining} days remaining (expires ${cert.validTo.toLocaleDateString()})`,
);
if (cert.daysRemaining <= WARNING_DAYS) {
await sendAlert(cert);
}
} catch (error) {
console.error(`Error checking ${domain}:`, error);
await fetch(WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: `SSL Certificate Check Failed: ${domain}`,
message: `Unable to retrieve certificate: ${error.message}`,
severity: "high",
source: "ssl-monitor",
timestamp: new Date().toISOString(),
metadata: {
domain,
error: error.message,
},
}),
});
}
}
}
checkAllCertificates();
setInterval(checkAllCertificates, 24 * 60 * 60 * 1000);

Usage:

Terminal window
# Install dependencies
npm install
# Run the monitor
npx tsx ssl-monitor.ts
# Or run as a cron job (daily at 9 AM)
0 9 * * * cd /path/to/monitor && npx tsx ssl-monitor.ts

Successful webhook submission returns:

{
"success": true,
"alertsCreated": 1,
"alertIds": ["alr_abc123"]
}
  • 200 OK: Alert received successfully
  • 401 Unauthorized: Unknown webhook key — check the webhook URL (beeps webhook list)
  • 413 Payload Too Large: Body exceeds the 10MB limit
  • 429 Too Many Requests: Rate limit exceeded — retry after the Retry-After header (seconds)
  • 500 Internal Server Error: Ingestion failed — safe to retry the same delivery

Anything other than 200 means no alert was created, so configure your sender to retry on 429 and 5xx.

{
"error": {
"code": "invalid_webhook_key",
"message": "Unknown webhook key. Check the webhook URL for this relay (beeps webhook list)."
}
}
  1. Include Context: Add relevant metadata for triage
  2. Use Severity: Properly categorize alert urgency
  3. Add Timestamps: Include when issue occurred
  4. Link to Resources: Add dashboard/runbook URLs
  5. Handle Errors: Implement retry logic with backoff
  6. Rate Limit: Batch alerts when possible
  7. Validate Payloads: Check data before sending
  8. Use HTTPS: Always use secure connections
  • 100 requests per minute per webhook key — each request can carry up to 100 alerts, so batch during storms
  • Exceeding the limit returns 429 Too Many Requests with a Retry-After header (seconds); retry then and nothing is lost

Once beeps receives a POST, it creates an alert and runs your relay rules. The response includes alertIds so you can track them programmatically.