Documentation
Reference for Keel's webhooks, API, and monitor settings
Everything you need to integrate Keel with your stack. No framework lock-in — just HTTP and JSON.
Webhook alerts
When an incident opens, closes, or a certificate is about to expire, Keel POSTs a JSON payload to each subscribed channel.
Events
down— incident opened (two consecutive failed checks)up— incident closed (a check succeeded after an incident was open)cert_expiry— TLS certificate expires within 14 days (fires at most once per 24 hours per monitor)test— manual test from the console
Payload
Shape targets Discord-style consumers: content mirrors text for easy Slack/Discord ingestion. Plain JSON — works with Slack, Discord, curl, or any HTTP endpoint.
{
"event": "down",
"text": "Keel: example.com is DOWN - timeout (https://example.com/)",
"content": "Keel: example.com is DOWN - timeout (https://example.com/)",
"data": {
"monitor": { "id": "…", "name": "example.com", "url": "https://example.com/", "method": "GET", "expectedStatus": 200, "intervalSeconds": 300, "timeoutMs": 10000, "paused": false, "lastStatus": "down", "failCount": 2, "nextCheckAt": 1727892000000, "createdAt": 1727890800000, "sslExpiresAt": null, "sslIssuer": null, "sslWarnedAt": null },
"incident": { "id": "…", "monitorId": "…", "openedAt": 1727892000000, "closedAt": null, "reason": "timeout" },
"check": { "monitorId": "…", "ts": 1727892000000, "status": "down", "statusCode": null, "latencyMs": null, "error": "timeout" },
"sentAt": 1727892000123
}
}
Delivery semantics
- One retry on failure (timeout, network error, or non-2xx response)
- 10 second timeout per attempt
- Redirects are not followed — a 3xx response counts as a failed delivery
- HTTPS required for webhook URLs
- Headers:
Content-Type: application/json
Quick test with curl
curl -X POST "https://your-webhook.example.com" \
-H "Content-Type: application/json" \
-d '{"event":"test","text":"Keel: test alert","content":"Keel: test alert","data":{"monitor":{"name":"test"},"incident":null,"check":null,"sentAt":1234567890}}'
Public API
REST endpoints under /api/v1/ for programmatic access to monitors, checks, incidents, and API tokens.
Authentication
- Open the console at
/app.htmland sign in. - Create an API token via the API:
POST /api/v1/tokenswith a session Bearer token. The token is shown once only — copy it immediately. - Use it as a Bearer token:
Authorization: Bearer keel_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Tokens are prefixed with keel_; session tokens never carry this prefix. API tokens cannot create or list other tokens — use the console session for token management.
Endpoints
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/tokens | Create an API token (session auth required) |
| GET | /api/v1/tokens | List your API tokens (session auth required) |
| DELETE | /api/v1/tokens/:id | Revoke an API token (session auth required) |
| GET | /api/v1/monitors | List all monitors |
| POST | /api/v1/monitors | Create a monitor |
| GET | /api/v1/monitors/:id | Get a single monitor |
| PATCH | /api/v1/monitors/:id | Update a monitor |
| DELETE | /api/v1/monitors/:id | Delete a monitor |
| GET | /api/v1/monitors/:id/checks?limit=50 | List recent checks for a monitor (default 50, max 200) |
| GET | /api/v1/incidents?open=true&monitor_id= | List incidents; filter by open state and/or monitor |
Example: list monitors
# Request
curl -H "Authorization: Bearer keel_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
"https://keelmonitor.com/api/v1/monitors"
# Response
{
"monitors": [
{ "id": "abc123", "name": "Main site", "url": "https://example.com/", "method": "GET", "expectedStatus": 200, "intervalSeconds": 300, "timeoutMs": 10000, "paused": false, "createdAt": "2026-09-01T12:00:00.000Z" }
]
}
Example: create a monitor
# Request
curl -X POST "https://keelmonitor.com/api/v1/monitors" \
-H "Authorization: Bearer keel_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
-H "Content-Type: application/json" \
-d '{"name":"API health","url":"https://api.example.com/health","method":"GET","expectedStatus":200,"intervalSeconds":60,"timeoutMs":5000}'
# Response
{
"monitor": {
"id": "xyz789",
"name": "API health",
"url": "https://api.example.com/health",
"method": "GET",
"expectedStatus": 200,
"intervalSeconds": 60,
"timeoutMs": 5000,
"paused": false,
"createdAt": 1768465800000
}
}
Monitor knobs
Fields you can set when creating or updating a monitor via the console or API.
| Field | Type | Default | Constraints |
|---|---|---|---|
method | string | GET | GET or HEAD |
expectedStatus | integer | 200 | 100–599 (any valid HTTP status code) |
timeoutMs | integer | 10000 | 1000–30000 (milliseconds) |
intervalSeconds | integer | 300 (Free) / 60 (Pro) | Positive integer; plan-enforced minimum |
paused | boolean | false | When true, checks are skipped |
The console enforces the same validation. Free plan minimum interval is 300s (5 minutes); Pro is 60s.
Plan limits
Limits apply at the account level, not per monitor.
| Free | Pro (£5/month) | |
|---|---|---|
| Monitors | 3 | 50 |
| Check interval | Every 5 minutes (300s) | Every 60 seconds |
| Webhook channels | 2 | 10 |
| API tokens | 1 | 5 |
| Check history | 30 days | 30 days |
Pro plan is not yet purchasable — payments are wired to Creem but not live. All accounts currently run on the Free plan.
Certificate expiry monitoring
Keel checks TLS certificates on HTTPS monitors and fires a cert_expiry webhook event when a certificate is about to expire.
- Warning window: 14 days before expiry
- Re-warn interval: at most once per 24 hours per monitor
- Event type:
cert_expiry(subscribe in channel settings) - Payload includes
monitor.sslExpiresAt(ISO timestamp) andmonitor.sslIssuerwhen available - Only fires for HTTPS monitors where the certificate expiry can be determined
- Current Workers runtime provides no certificate access — cert_expiry events do not fire in production today. This is a silent gap, not an error; the code path is in place for when the platform adds cert surface.
Last updated: 3 October 2026.