Contract version 1
Generic JSON payload
Generic webhook channels receive this envelope with Content-Type: application/json. Consumers should ignore unknown fields so additive version 1 updates remain compatible.
{
"version": "1",
"id": "evt_1842",
"type": "job.failed",
"created_at": "2026-07-25T08:09:10Z",
"job": {
"id": 7,
"name": "Nightly invoice sync",
"url": "https://example.com/tasks/invoices",
"history_url": "https://cronengine.com/jobs/history?id=7"
},
"run": {
"id": 9124,
"success": false,
"http_status": 503,
"duration_ms": 812,
"error": "The endpoint returned HTTP 503.",
"fired_at": "2026-07-25T08:09:10Z"
}
}
http_status is null when no HTTP response was received. duration_ms and error may also be null.
Event types
job.failed
A configured failure or failure-threshold notification.
job.recovered
The first successful run after an open failure incident.
job.run.succeeded
A successful every-run notification.
job.run.failed
A failed every-run notification.
Request headers
X-CronEngine-Webhook-Version: 1
X-CronEngine-Event-Id: evt_1842
X-CronEngine-Event-Type: job.failed
X-CronEngine-Timestamp: 1784966950
X-CronEngine-Signature: v1=<hex HMAC-SHA256>
The signature header is present only when the generic channel has a signing secret.
Verify webhook signatures
Calculate HMAC-SHA256 over <timestamp>.<raw request body>. Compare the result with a constant-time function and reject timestamps outside a short tolerance, such as five minutes.
$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_CRONENGINE_TIMESTAMP'] ?? '';
$received = $_SERVER['HTTP_X_CRONENGINE_SIGNATURE'] ?? '';
$expected = 'v1=' . hash_hmac(
'sha256',
$timestamp . '.' . $body,
getenv('CRONENGINE_WEBHOOK_SECRET')
);
if (!hash_equals($expected, $received)) {
http_response_code(401);
exit;
}
Delivery and retries
- Acknowledge with 2xx. Any 2xx response marks the event delivered.
- At-least-once delivery. Store the stable event
idand ignore duplicates. - Retryable failures. Network errors, HTTP 408, 425, 429, and 5xx use exponential backoff. A bounded
Retry-Afterheader is honored. - Permanent failures. Other 3xx and 4xx responses are not retried. Redirects are deliberately disabled.
Slack, Discord, and Telegram
Slack
Create an incoming webhook in Slack and paste its complete hooks.slack.com URL.
Discord
Create a webhook for the destination channel and paste its discord.com API webhook URL.
Telegram
Create a bot with @BotFather, add it to the chat, then enter the bot token and chat ID.
Provider channels receive provider-native messages containing the outcome, status, duration, error, history link, and event ID. The generic JSON contract applies only to Generic JSON webhooks.
Delivery security
Destinations must use HTTPS. CronEngine rejects its own hosts, private and reserved networks, and deployment-blocked addresses. DNS is resolved and checked immediately before every attempt, then the connection is pinned to a vetted address. TLS verification remains enabled and redirects are not followed.