Webhooks
How we call your endpoints, handle failures, and keep things reliable.
How It Works
When a task triggers, Runlater makes an HTTP request to your configured URL. Your endpoint processes the request and returns a response.
- Task triggers (cron schedule or one-time)
- Runlater sends HTTP request to your URL
- Your server processes the request
- Return 2xx status for success
- We log the result and schedule retries if needed
Request Format
Every webhook request includes these headers:
POST /your-endpoint HTTP/1.1 Host: your-app.com Content-Type: application/json X-Runlater-Task-Id: 01JJXYZ... X-Runlater-Execution-Id: 01JJXYZ... X-Runlater-Signature: sha256=a1b2c3d4... Authorization: Bearer your-token (if configured) // Your configured body (if any) {"action": "sync"}
| Header | Description |
|---|---|
X-Runlater-Task-Id |
The ID of the task being executed |
X-Runlater-Execution-Id |
Unique ID for this execution (use for deduplication) |
X-Runlater-Signature |
HMAC-SHA256 signature of the request body |
Verifying Signatures
Every request is signed with your organization's webhook secret. Verify the signature to ensure requests are from Runlater.
Find your webhook secret in Organization Settings → API Keys.
const crypto = require('crypto'); function verifySignature(body, signature, secret) { const expected = crypto .createHmac('sha256', secret) .update(body) .digest('hex'); return `sha256=${expected}` === signature; } app.post('/webhook', (req, res) => { const signature = req.headers['x-runlater-signature']; const body = JSON.stringify(req.body); // or raw body buffer if (!verifySignature(body, signature, process.env.RUNLATER_WEBHOOK_SECRET)) { return res.status(401).json({ error: 'Invalid signature' }); } // Safe to process - request is from Runlater const taskId = req.headers['x-runlater-task-id']; const executionId = req.headers['x-runlater-execution-id']; // Process the task... res.json({ ok: true }); });
Response Handling
Runlater interprets your response status code:
| Status | Result | Action |
|---|---|---|
2xx |
Success | Task marked complete |
4xx |
Client error | Task marked failed (no retry) |
429 |
Rate limited | Host blocked, all tasks to that host paused (see Smart Host Backoff) |
5xx |
Server error | Task retried with backoff; host blocked after 3 consecutive failures |
Timeout |
Timeout | Task retried with backoff |
Timeouts
Your endpoint has 30 seconds to respond. If no response is received within this time, the execution is marked as a timeout and scheduled for retry.
Automatic Retries
Retry behavior depends on the task type:
One-time Tasks (run_at)
One-time tasks automatically retry with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 2 minutes |
| 2nd retry | 4.5 minutes |
| 3rd retry | 8 minutes |
| 4th retry | 12.5 minutes |
| 5th retry | 18 minutes |
After 5 failed attempts, the task is marked as permanently failed and you'll receive an alert (if configured).
Recurring Tasks (cron)
Cron tasks also retry with exponential backoff, but retries are capped before the next scheduled run. If the backoff delay would land past the next cron execution, the retry is skipped and the next scheduled run takes over.
Smart Host Backoff
When a target API is rate-limiting or down, Runlater automatically pauses all deliveries to that host for your organization. This prevents hammering a struggling API and respects rate limits across all your tasks.
429 Rate Limiting
When your target returns a 429 Too Many Requests
response, Runlater blocks all deliveries to that host immediately. If a
Retry-After
header is present, we use that duration. Otherwise, we wait 60 seconds before retrying.
Any pending executions targeting the same host are automatically rescheduled to run after the backoff expires. Tasks targeting other hosts are not affected.
Down APIs (5xx / Connection Errors)
When a host returns 5xx errors or connection failures repeatedly, Runlater detects the pattern and backs off automatically:
| Consecutive Failures | Action |
|---|---|
| 1–2 | Normal retry behavior (per-task retries still apply) |
| 3 | Host blocked for 30 seconds |
| Continued failures | Escalating backoff: 60s → 120s → 300s (max 5 minutes) |
A single successful response resets the failure counter immediately.
api.example.com
are rate-limited, other organizations' tasks to the same host are not affected.
Idempotency
Your webhook handler should be idempotent - safe to call multiple times with the same data. This is important because:
- Network issues may cause duplicate deliveries
- Retries will re-send the same request
- A timeout might occur after your server processed the request
Use the X-Runlater-Execution-Id
header to deduplicate:
// Track processed executions const processed = new Set(); // Use Redis in production app.post('/webhook', async (req, res) => { const executionId = req.headers['x-runlater-execution-id']; if (processed.has(executionId)) { return res.json({ status: 'already_processed' }); } // Process the task... await doWork(); processed.add(executionId); res.json({ status: 'ok' }); });
Callbacks
Get notified when an execution completes by setting a
callback_url
on your task. After each execution finishes (success, failure, or timeout), Runlater will POST the result to your callback URL.
How Callbacks Work
-
Set
callback_urlon a task - Runlater executes your webhook as normal
- After completion, Runlater POSTs the result to your callback URL
- Callbacks retry up to 3 times with backoff (5s, 20s) on failure
Callback Payload
POST /your-callback HTTP/1.1 Content-Type: application/json X-Runlater-Signature: sha256=a1b2c3d4... { "event": "execution.completed", "task_id": "01JJXYZ...", "execution_id": "01JJXYZ...", "status": "success", "status_code": 200, "duration_ms": 145, "started_at": "2025-01-29T10:00:01Z", "finished_at": "2025-01-29T10:00:01Z", "attempt": 1 }
Callback Priority
Callbacks can be set at two levels. The execution-level callback (from a task push) takes priority over the task-level callback:
| Task callback_url | Push callback_url | Result |
|---|---|---|
| Set | Not set | Uses task callback_url |
| Set | Set | Uses push callback_url (override) |
| Not set | Set | Uses push callback_url |
| Not set | Not set | No callback |
X-Runlater-Signature
header to authenticate callbacks.
Notifications
Runlater can notify you via email and webhook when task execution status changes. Configure these in Organization Settings → Notifications.
Failure Notifications
Triggered on the first failure in a sequence (not on every consecutive failure). If a task fails 5 times in a row, you only get one notification.
Recovery Notifications
Triggered when a task succeeds after a failure. This lets you know your service is back to normal without manually checking the dashboard.
Both notification types are sent to your configured email and webhook URL. Webhook payloads are auto-formatted for Slack and Discord.
Per-Task & Per-Monitor Overrides
By default, notification settings are configured at the organization level. You can override these per task or monitor using
notify_on_failure
and notify_on_recovery:
null(default) — use organization settingtrue— always notify, even if org setting is offfalse— never notify, even if org setting is on
Set both notify_on_failure
and notify_on_recovery
to false
to silence all notifications for a specific resource.
Notification Webhook Payloads
// Failure notification { "event": "task.failed", "task": { "id": "...", "name": "...", "url": "..." }, "execution": { "status": "failed", "status_code": 500, ... } } // Recovery notification { "event": "task.recovered", "task": { "id": "...", "name": "...", "url": "..." }, "execution": { "status": "success", "status_code": 200, ... } }
Custom Headers
You can configure custom headers when creating a task:
curl -X POST https://runlater.eu/api/v1/schedules \ -H "Authorization: Bearer pk_live_xxx" \ -H "Content-Type: application/json" \ -d '{ "name": "My schedule", "url": "https://myapp.com/webhook", "cron": "0 * * * *", "headers": { "Authorization": "Bearer my-app-token", "X-Custom-Header": "custom-value" } }'