Developer documentation

Notification webhooks

Receive a stable JSON event whenever a job fails, recovers, or completes. Version 1 includes an idempotency ID, job and run details, and optional HMAC verification.

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"
  }
}
Nullable values: 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 id and ignore duplicates.
  • Retryable failures. Network errors, HTTP 408, 425, 429, and 5xx use exponential backoff. A bounded Retry-After header 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.

Webhook URLs, bot tokens, and signing secrets are currently stored as entered and may appear in database backups. Use dedicated, revocable credentials and never reuse an account password.