Custom Webhooks
Overview
Custom Webhooks in ToBeAlert allow external systems to send alerts using their own data format. Instead of adapting your monitoring tools to ToBeAlert's format, you can create a custom webhook that maps incoming data to ToBeAlert's alert structure.
How It Works
- Create a webhook configuration (field mappings are optional; a disabled webhook still receives payloads into history without creating alerts)
- Get a unique webhook URL for your configuration
- Send data in your format to the webhook URL
- ToBeAlert maps your data into standardized alerts when the webhook is enabled and required fields are covered
Webhook URL Format
Each custom webhook configuration gets a unique URL:
https://your-tobealert.com:9000/api/webhooks/custom/{webhook-id}
This endpoint accepts POST requests with a JSON body. Authentication is optional and configured per webhook (authMode; existing configs default to none). See Inbound authentication.
Creating a Custom Webhook
1. Define Your Input Format
First, understand what data your monitoring system sends. For example:
{
"hostname": "web-server-01",
"check": "cpu_usage",
"status": "CRITICAL",
"message": "CPU usage is 95%",
"timestamp": "2025-04-15T10:30:00Z",
"metrics": {
"cpu": 95.2,
"memory": 67.8
},
"tags": [
"production",
"linux"
],
"source": "nagios"
}
2. Create the Webhook Configuration
You may omit fieldMappings or send {} to create a webhook that only receives payloads into history (no alerts until mappings cover required columns and you enable it). Omitting enabled creates the webhook disabled. Setting enabled: true without covering resource, event, environment, type, text, and severity (via mapping or a non-empty default; whitespace-only does not count) returns 400. origin is not required. Incomplete mappings are allowed while the webhook stays disabled. A disabled webhook still accepts inbound POST, stores the payload, and does not create alerts.
The webhook ID may contain only letters, digits, - and _. Creating needs scope write:webhook_configs.
curl -X POST http://your-tobealert.com:9000/api/webhook_config \
-H "Content-Type: application/json" \
-H "Authorization: Key YOUR_API_KEY" \
-d '{
"id": "nagios-monitoring",
"name": "Nagios Monitoring Integration",
"enabled": true,
"fieldMappings": {
"resource": "hostname",
"event": "check",
"text": "message",
"value": "metrics.cpu",
"origin": "source",
"tags": "tags",
"attributes": {
"source_status": "status",
"memory_usage": "metrics.memory",
"original_timestamp": "timestamp"
}
},
"defaultValues": {
"environment": "Production",
"type": "nagiosAlert",
"severity": "warning",
"group": "infrastructure",
"service": [
"monitoring"
]
}
}'
Each value in fieldMappings is a path into the payload; fixed values go in defaultValues. The sender's status (CRITICAL) is not a ToBeAlert severity, so it is kept in attributes.source_status and severity defaults to warning until a policy translates it — see Transforming values.
3. Use Your Webhook URL
The response (201) includes the stored configuration and your webhook URL:
{
"status": "ok",
"id": "nagios-monitoring",
"webhook_config": { "id": "nagios-monitoring", "name": "Nagios Monitoring Integration", "...": "..." },
"webhook_url": "https://your-tobealert.com:9000/api/webhooks/custom/nagios-monitoring"
}
Now send your data to this URL (add auth headers when the config requires them):
curl -X POST https://your-tobealert.com:9000/api/webhooks/custom/nagios-monitoring \
-H "Content-Type: application/json" \
-d '{
"hostname": "web-server-01",
"check": "cpu_usage",
"status": "CRITICAL",
"message": "CPU usage is 95%",
"timestamp": "2025-04-15T10:30:00Z",
"metrics": {
"cpu": 95.2,
"memory": 67.8
},
"tags": ["production", "linux"],
"source": "nagios"
}'
The mapping produces this alert:
[
{
"resource": "web-server-01",
"event": "cpu_usage",
"text": "CPU usage is 95%",
"value": 95.2,
"origin": "nagios",
"tags": [
"production",
"linux"
],
"attributes": {
"source_status": "CRITICAL",
"memory_usage": 67.8,
"original_timestamp": "2025-04-15T10:30:00Z"
},
"environment": "Production",
"type": "nagiosAlert",
"severity": "warning",
"group": "infrastructure",
"service": [
"monitoring"
]
}
]
The inbound response lists the alerts created:
{
"status": "ok",
"webhook_id": "nagios-monitoring",
"webhook_name": "Nagios Monitoring Integration",
"alerts_created": 1,
"alerts": [
{ "id": "6f1c…", "resource": "web-server-01", "event": "cpu_usage", "severity": "warning" }
]
}
Field Mappings
Field mappings say where each alert field is read in the payload. They copy values as they are: there are no ${…} templates, expressions, or functions. The full syntax is in the Field Mapping Reference.
| Mapping value | Meaning |
|---|---|
"hostname", "server.name", "checks[0]", "events[1].type" | Path into the payload (dot notation, array index) |
"tags" / "tags[]" | Whole array, copied as a list |
{"path": "errors", "join": ", "} | Array joined into one string |
{"path": "hosts[]", "expand": true} | One alert per array element ([N] in other mappings reads the same index) |
{"path": "is_critical", "if_true": "critical", "if_false": "warning"} | One of two fixed values depending on a boolean |
{"cpu": "metrics.cpu", …} under attributes | Custom attributes, same syntax |
3600, true | Numbers and booleans are copied literally |
A string is always a path: "environment": "production" reads a key called production and, when it is missing, leaves the field out. Put fixed text in defaultValues:
{
"defaultValues": {
"environment": "Production",
"group": "general",
"origin": "webhook",
"severity": "warning"
}
}
A default is used when the field is not mapped or its path is missing in the payload.
Set arrayPath (for example alerts) when the payload carries several events in one array: each element becomes one alert, and every mapping is read inside that element.
Transforming values
To change case, trim text, translate a source status into a severity, or react to a numeric threshold, keep the mapping as a plain copy and add a policy triggered On alert creation and any change type. Its modifiers apply text transforms (upper, lower, capitalize, trim), set a severity when a condition matches (including numeric comparisons on value such as Greater than 90%), and a custom tool can build values from several fields. Details and examples: Transforming values.
Inbound authentication
Each webhook has one auth mode. Configs created before auth existed keep authMode: "none" (open inbound URL). Set auth on create or update with write:webhook_configs.
| Field | In create/update body | In GET response |
|---|---|---|
authMode | none | header | api_key | Same |
authHeaderScheme | static | generic | github | slack (header mode only) | Same |
authHeaderName | Custom header name for static / generic | Same (server-fixed for github/slack) |
authSecret | Shared secret (write only) | Never returned under this name |
authSecretSet | — | true / false |
authSecretPlaintext | — | HMAC schemes (generic, github, slack) only: the decrypted secret, so the configuration can be read back; null for static (stored one-way) and other modes |
authSecretStale | — | true when the secret was saved under another server SECRET_KEY and can no longer verify; enter it again |
apiKeyId | Id of a ToBeAlert API key (not the key secret) | Same |
On PUT, omit authSecret, send "", or send ****** to keep the stored secret. Changing authMode clears credentials from the previous mode.
If SECRET_KEY is empty on the server, saving authMode: "header" returns 400. none and api_key still work.
authMode: none
No inbound credentials. Anyone who knows the URL can POST.
authMode: header
static
Caller sends a custom header (for example Cloudflare cf-webhook-auth) whose value must match the configured secret. The server stores only an HMAC-SHA256 hash of the secret (keyed with SECRET_KEY) and compares in constant time.
{
"authMode": "header",
"authHeaderScheme": "static",
"authHeaderName": "cf-webhook-auth",
"authSecret": "your-shared-secret"
}
HMAC schemes (generic, github, slack)
The shared secret is stored encrypted (Fernet derived from SECRET_KEY), never in plaintext; GET returns it decrypted in authSecretPlaintext to callers with read:webhook_configs. Signatures are over the raw request body.
| Scheme | Header | Signature |
|---|---|---|
generic | Your authHeaderName | HMAC-SHA256 of the raw body, hex, with or without a sha256= prefix |
github | X-Hub-Signature-256 (fixed by server) | sha256=<hex> of the raw body |
slack | X-Slack-Signature (fixed) | v0=<hex> of v0:{X-Slack-Request-Timestamp}:{raw_body}; window 300 seconds |
authMode: api_key
Bind one ToBeAlert API key by id. The key must exist, not be expired, and include write:alerts (or admin / admin:alerts). Non-admin users may only bind keys they own (key.user = their login); a global admin may bind any key. Validation runs on create/update when the key id is set or changed, and again on every inbound request.
Inbound callers must send the key secret as Authorization: Key <key> or X-API-Key. Query string ?api-key= is not accepted.
If the key is deleted or loses write:alerts, inbound requests return 401 and the webhook stays in api_key mode (it does not fall open). Editing name or mappings without changing apiKeyId does not re-check key ownership.
{
"authMode": "api_key",
"apiKeyId": "my-alerts-key-id"
}
Auth failure responses
Failed auth returns 401 with a short message. When the stored secret no longer decrypts because SECRET_KEY changed, the sender only gets authentication failed; the real reason is in errorHistory, and the list response flags such webhooks in staleSecrets. Responses never echo the received secret or signature. Typical messages: missing header, invalid header, invalid signature, invalid API key, API key does not match this webhook, API key lacks write:alerts.
Complete Examples
Ready-to-adapt configurations for Prometheus Alertmanager, Datadog, Grafana, PagerDuty, AWS CloudWatch, Kubernetes, and a custom application are in Integration Examples, each with its sample payload.
Managing Webhooks
List All Webhooks
curl -H "Authorization: Key YOUR_API_KEY" \
http://your-tobealert.com:9000/api/webhook_configs
Supports page, page-size, sort-by, and query filters on id, name, description, sourceId, enabled, and createdBy. List items include summary fields only (no bodies): payloadHistoryCount, lastPayloadAt, errorHistoryCount, lastErrorAt. Timestamps are ISO-8601 or null. Auth fields (authMode, authSecretSet, apiKeyId, …) are included; authSecret is never listed.
Get Webhook Details
curl -H "Authorization: Key YOUR_API_KEY" \
http://your-tobealert.com:9000/api/webhook_config/nagios-monitoring
Requires scope read:webhook_configs. The response includes full payloadHistory and errorHistory (each up to 5 entries, newest first), plus auth metadata (authMode, authSecretSet, apiKeyId, …). There is no separate public endpoint to read history. The inbound POST /api/webhooks/custom/{id} does not require a user scope.
Example payloadHistory entry (valid JSON kept for mapping):
{
"receivedAt": "2026-09-22T13:45:00.000Z",
"httpStatus": 200,
"sizeBytes": 1234,
"oversized": false,
"payload": { "hostname": "web-01" },
"message": null
}
Example errorHistory entry:
{
"receivedAt": "2026-09-22T13:46:00.000Z",
"httpStatus": 413,
"sizeBytes": 300000,
"oversized": true,
"payload": null,
"message": "Payload exceeded the size limit (received 300000 bytes, limit 262144 bytes).",
"traceback": null
}
Update Webhook
curl -X PUT http://your-tobealert.com:9000/api/webhook_config/nagios-monitoring \
-H "Content-Type: application/json" \
-H "Authorization: Key YOUR_API_KEY" \
-d '{
"name": "Updated Nagios Integration",
"fieldMappings": {
"resource": "hostname",
"event": "check",
"text": "message",
"attributes": { "source_status": "status" }
}
}'
PUT merges the body into the stored configuration: fields you omit keep their value, and fieldMappings / defaultValues are replaced as a whole when sent. Validation runs on the result, so enabling a webhook whose mappings do not cover the required fields returns 400.
POST and PUT ignore any payloadHistory or errorHistory sent by the client. Editing name or mappings does not clear either history.
Delete Webhook
curl -X DELETE http://your-tobealert.com:9000/api/webhook_config/nagios-monitoring \
-H "Authorization: Key YOUR_API_KEY"
Testing Webhooks
Send a sample POST to the inbound URL (include auth headers when configured). Inspect stored payloads and errors with GET /api/webhook_config/{id} (read:webhook_configs).
curl -X POST https://your-tobealert.com:9000/api/webhooks/custom/nagios-monitoring \
-H "Content-Type: application/json" \
-d '{
"hostname": "test-server",
"check": "cpu_test",
"status": "WARNING",
"message": "Test alert"
}'
Payload history, error history, and size limit
Inbound POST /api/webhooks/custom/{id} maintains two server-owned histories (max 5 each, newest first). Clients cannot write them. Updating name or mappings does not clear them. If the config id does not exist (404), nothing is recorded.
Processing order when the config exists:
- If the raw body exceeds 256 KiB → 413,
errorHistoryonly (auth is not checked) - Auth
- Parse / mapping / alert creation
payloadHistory
Kept for field mapping and testing. Requires valid JSON within the size limit (non-empty object or array). Includes successful posts, disabled configs (auth OK), mapping failures, and 401 when the JSON was valid enough to keep. Does not include oversized bodies, invalid JSON, or empty bodies.
Each entry: receivedAt, httpStatus, sizeBytes, oversized (always false here), payload, message.
messageisnullwhen auth succeeded.- On 401 with a keepable payload,
messageis exactly:
Authentication failed. This payload is kept for testing and field mapping, but it will not be converted into an alert.
That 401 does not create an alert. There is no special purge: older entries drop when five newer ones arrive.
errorHistory
Records failures: 400, 401, 403, 413, 500, and partial success (HTTP 200 with some alerts failed). Each entry: receivedAt, httpStatus, sizeBytes, oversized, payload (or null), message, traceback.
tracebackis set only when an exception occurred (last 20 lines).- Oversized / invalid JSON / empty body: only
errorHistory, withpayload: null. - Disabled + auth OK:
payloadHistory(message: null) anderrorHistory403 (no traceback); no alert. - Auth failure:
errorHistorygets the short authmessage; if the body was valid keepable JSON,payloadHistoryalso gets an entry with the fixed auth-failed text above.
| Case | HTTP | payloadHistory | errorHistory | Alerts |
|---|---|---|---|---|
| Success | 200 | Yes (message: null) | No | Yes |
| Partial (some alerts fail) | 200 | Yes | Yes | Some |
| Disabled (auth OK, valid JSON) | 403 | Yes (message: null) | Yes (403) | No |
| Auth failed, valid keepable JSON | 401 | Yes (fixed auth message) | Yes (auth reason) | No |
| Auth failed, invalid/empty body | 401 | No | Yes (payload: null) | No |
| Invalid JSON / empty | 400 | No | Yes (payload: null) | No |
| Mapping / no alerts | 400 | Yes | Yes | No |
| Oversized (> 256 KiB) | 413 | No | Yes (oversized: true, payload: null) | No |
| Unexpected server error | 500 | Yes if keepable JSON | Yes (+ traceback) | No |
| All alerts failed during processing | 400 | Yes | Yes | No |
Error Handling
Common Errors
- Missing required fields — enabling the webhook (or saving it enabled) without covering
resource,event,environment,type,text,severityreturns 400Invalid field mappings: Missing required alert columns: … - Field silently missing — a mapping whose path is not in the payload (or a fixed string or
${…}template written infieldMappings) leaves the field out; usedefaultValues - Alert rejected — the mapped data is not a valid alert:
An environment must be provided,service must be a list,tags must be a list,Severity (…) is not one of … - No alerts from Array Path — 400
No alerts generated from payload: array path "…" was not foundor… is an empty list. - Authentication failures (401) — check inbound auth mode, headers, signatures, or bound API key; see Inbound authentication
- Payload too large (413) — keep the raw body at or under 256 KiB; see Payload history, error history, and size limit
Error Response Format
Rejected requests (401, 403, 404, 413, invalid JSON, mapping errors) return a status and a message:
{
"status": "error",
"message": "No alerts generated from payload: array path \"alerts\" was not found. Top-level keys: receiver, status."
}
When the payload was mapped but some alerts could not be created, the response keeps the alert summary and adds errors with the mapped data and the reason. status is partial (HTTP 200) when at least one alert was created, error (HTTP 400) when none was:
{
"status": "error",
"webhook_id": "nagios-monitoring",
"webhook_name": "Nagios Monitoring Integration",
"alerts_created": 0,
"alerts": [],
"errors": [
{
"alert_data": { "resource": "web-01", "event": "cpu", "severity": "UNKNOWN", "...": "..." },
"error": "Severity (UNKNOWN) is not one of security, critical, major, minor, warning, …"
}
]
}
Best Practices
- Use descriptive webhook IDs — make them easy to identify and manage
- Provide default values — every required field should have a default or a path that is always present
- Validate input format — test with real data from your monitoring system (the webhook keeps the last five payloads)
- Keep source values in attributes — policies can then translate statuses and priorities
- Monitor webhook health — check
errorHistoryfor rejected or failed deliveries - Version your configurations — use IDs like
nagios-v1,nagios-v2
Security Considerations
- Prefer
authMode: headerorapi_keyon production inbound URLs (noneis open to anyone with the URL) - Use HTTPS for all webhook traffic
- Never log or store inbound auth headers/signatures in your own tooling beyond what you need
- Rotate shared secrets and API keys periodically; on
PUT, send a newauthSecretto replace the stored one - Inspect
errorHistoryfor repeated 401/413 patterns - Consider network controls (IP allowlists) in front of ToBeAlert when possible