Set Up Cuckoo Alerts: Email, Slack, Discord and Webhooks
Updated
Monitoring a cron job only helps if the alert reaches someone who can act on it. An alert in an inbox nobody reads is as useless as no monitoring. This guide explains Cuckoo's alert channels, who receives which alert, and how to keep the volume low enough that people still react to them.
When Cuckoo sends an alert
Your jobs send pings to https://cuckoo.metastic.ch/ping/YOUR-UUID (plus /start, /fail or an exit code).
Cuckoo sends a notification when:
- a ping is late: the period or cron schedule plus the grace time passed without a success ping,
- a failure is reported: a
/failping or a non-zero exit code arrived, - max runtime is exceeded: a
/startarrived, but no success or failure followed in time, - the check recovers: a success ping arrives after the check was down.
Each alert goes to every integration of the organisation. You get one alert per incident, not one per missed run, and one recovery message.
Email is the simplest channel and a good default for low-volume, non-urgent checks. Use a team address or a ticketing inbox rather than a personal mailbox, so alerts are not lost when someone is on holiday. Add a filter rule in your mail client so Cuckoo messages are not sorted into a newsletter or promotions folder.
Slack incoming webhook
Slack delivers messages from external services through incoming webhooks:
- In Slack, create an app at api.slack.com/apps (or use an existing internal app).
- Under Incoming Webhooks, turn the feature on and click Add New Webhook to Workspace.
- Choose the channel, for example
#ops-alerts, and copy the webhook URL. It starts withhttps://hooks.slack.com/services/. - In the Cuckoo dashboard, add a Slack channel and paste the URL.
Treat the webhook URL as a secret: anyone who has it can post to that channel. If it leaks, revoke it in Slack.
You can test a Slack webhook independently of Cuckoo:
curl -fsS -X POST -H 'Content-Type: application/json' \
--data '{"text":"Test from the ops team"}' \
"$SLACK_WEBHOOK_URL"
Discord webhook
In Discord, open Server Settings → Integrations → Webhooks → New Webhook, pick the channel, and copy the webhook URL. Add a Discord channel in the Cuckoo dashboard and paste it. As with Slack, the URL is a credential. Delete the webhook in Discord to revoke it.
Generic signed webhook
The generic webhook sends an HTTP POST with a JSON body to a URL you control, for example to open a ticket, page
an on-call tool, or trigger an automatic retry. The URL must be public https. When you add the integration,
Cuckoo shows a signing secret (whsec_…) once; store it in your receiver's secret store.
Every request carries these headers:
X-Cuckoo-Event:check.downorcheck.upX-Cuckoo-Delivery: a delivery ID; the same ID is reused when a delivery is retriedX-Cuckoo-Signature:t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>
A down alert looks like this:
{
"event": "check.down",
"occurred_at": "2026-11-05T02:47:00Z",
"text": "nightly-backup is DOWN (no ping within 30 minutes of the expected time)",
"reason": "no ping within 30 minutes of the expected time",
"org": {"id": 12, "name": "Acme"},
"check": {"id": 345, "name": "nightly-backup", "slug": "nightly-backup", "status": "down",
"schedule": "cron 15 2 * * * (UTC)", "grace_seconds": 1800,
"url": "https://cuckoo.metastic.ch/checks/345", "last_ping_at": "2026-11-04T02:16:03Z"}
}
Verifying the signature
Recompute the HMAC over the timestamp, a dot and the raw body, compare it in constant time, and reject old timestamps so a captured request cannot be replayed later:
import hashlib
import hmac
import os
import time
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["CUCKOO_WEBHOOK_SECRET"].encode()
@app.post("/hooks/cuckoo")
def receive():
raw = request.get_data() # raw bytes, before any JSON parsing
parts = dict(p.split("=", 1) for p in request.headers.get("X-Cuckoo-Signature", "").split(",") if "=" in p)
t, sig = parts.get("t", ""), parts.get("v1", "")
if not t.isdigit() or abs(time.time() - int(t)) > 300:
abort(401)
expected = hmac.new(SECRET, t.encode() + b"." + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
abort(401)
event = request.get_json()
# handle event["event"] == "check.down" / "check.up": open a ticket, notify on-call, ...
return "", 204
Rules for any receiver:
- Sign the raw body. Parsing and re-serialising JSON changes whitespace and key order, and the signature no longer matches.
- Compare in constant time with
hmac.compare_digest(Python),crypto.timingSafeEqual(Node.js) orhash_equals(PHP). A plain==can leak timing information. - Respond quickly. Return a 2xx status within 10 seconds and do slow work in the background. Other statuses are retried with backoff for several hours; 401, 403, 404 and 410 stop the retries.
- Expect duplicates. Use
X-Cuckoo-Deliveryto make the handler idempotent, so a retried delivery does not open two tickets.
Who gets which alert
Integrations belong to the organisation, and every down and recovery alert goes to all of them. Email goes to every member who has alert emails turned on in their account settings. To separate urgent from routine jobs today, use two organisations (for example "Production" and "Internal") with their own integrations. Per-check routing is on the roadmap.
One check per job, with a clear name such as prod-db-backup-nightly, makes every alert self-explanatory.
Avoiding alert fatigue
- Set realistic grace times. If a job sometimes runs 20 minutes longer, set the grace time above that. A check that goes down every few days for no reason teaches people to ignore it.
- Watch for flapping. A check that alternates between down and up usually points to a schedule mismatch, such as the wrong time zone, a period that is too short or overlapping runs. Fix the cause rather than muting it.
- Report retries carefully. If your scheduler retries failed runs, send the fail ping only on the final attempt, otherwise you get a failure and a recovery for a run that ended fine.
- Plan for maintenance. Pause the affected checks before planned downtime; the next ping resumes them.
Troubleshooting
- No Slack messages: the app may have been removed from the channel, or the webhook revoked. Test the URL with the curl command above.
- Webhook signature never matches: you are probably hashing parsed JSON instead of the raw body, or you
forgot the
<t>.prefix. - Emails missing: check spam folders, and allow-list the sender address in your mail system.
FAQ
Can one check alert several channels?
Yes. Every down and recovery alert goes to all integrations of the organisation, for example Slack plus a webhook, and by email to every member who has alert emails turned on.
Where do I find the webhook payload format?
The JSON body and the X-Cuckoo-Event, X-Cuckoo-Delivery and X-Cuckoo-Signature headers are shown
above and on the docs page. The signature covers the timestamp and the raw body, like Stripe's scheme.
Will I get a message when the job recovers?
Yes. When a success ping arrives after the check was down, Cuckoo sends a recovery notification to the same channels, so you know the issue is resolved without opening the dashboard.
Related guides
- Cron Job Start and Fail SignalsSend /start, success and /fail pings from cron jobs to measure run time and catch hung jobs. Includes a reusable bash wrapper script with trap and exit codes.
- Cron Schedule, Time Zone and Grace PeriodPick the right monitoring schedule for a cron job: period or cron expression, IANA time zones, how cron handles daylight saving time, and sizing grace time.
- Dead Man's Switch Monitoring ExplainedWhat dead man's switch monitoring is, how push-based heartbeat checks differ from pull-based uptime monitoring, and which jobs and systems benefit from each.
- How to Monitor Cron JobsLearn how to monitor cron jobs with heartbeat pings: why cron fails silently, how a dead man's switch catches missed runs, and how to set up a first check.