Skip to main content

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.

  1. Task triggers (cron schedule or one-time)
  2. Runlater sends HTTP request to your URL
  3. Your server processes the request
  4. Return 2xx status for success
  5. 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.

Node.js
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
Tip: Return 4xx for permanent failures (bad data, validation errors) and 5xx for temporary failures (database down, external service unavailable).

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.

Long-running tasks: If your task takes longer than 30 seconds, acknowledge the request immediately and process asynchronously. Return 200 to indicate the task was accepted.

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 retry capping An hourly cron that fails at :00 will retry at :02, :04, :08, etc. But a retry that would land past :59 is skipped — the next hourly run at 1:00 handles it. This prevents overlap between retries and scheduled runs.

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.

Per-organization isolation: Host blocking is scoped to your organization. If your tasks to 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:

Deduplication Example
// 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

  1. Set callback_url on a task
  2. Runlater executes your webhook as normal
  3. After completion, Runlater POSTs the result to your callback URL
  4. 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
Note: Callbacks are signed with the same webhook secret as regular webhook requests. Verify the 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 setting
  • true — always notify, even if org setting is off
  • false — 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"
    }
  }'