cronly

Webhooks

How we call your endpoints, handle failures, and keep things reliable.

How It Works

When a job triggers, Cronly makes an HTTP request to your configured URL. Your endpoint processes the request and returns a response.

  1. Job triggers (cron schedule or one-time)
  2. Cronly 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
User-Agent: Cronly/1.0
X-Cronly-Job-Id: job_abc123
X-Cronly-Execution-Id: exec_xyz789
X-Cronly-Signature: sha256=abcdef...
Header Description
X-Cronly-Job-Id The ID of the job being executed
X-Cronly-Execution-Id Unique ID for this execution attempt
X-Cronly-Signature HMAC signature for verifying authenticity

Verifying Signatures

To ensure requests come from Cronly, verify the signature using your webhook secret:

Node.js
const crypto = require('crypto');

function verifySignature(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return `sha256=${expected}` === signature;
}

// In your handler
app.post('/webhook', (req, res) => {
  const signature = req.headers['x-cronly-signature'];
  const isValid = verifySignature(
    JSON.stringify(req.body),
    signature,
    process.env.CRONLY_WEBHOOK_SECRET
  );

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Process the job...
});

Response Handling

Cronly interprets your response status code:

Status Result Action
2xx Success Job marked complete
4xx Client error Job marked failed (no retry)
5xx Server error Job retried with backoff
Timeout Timeout Job 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 job takes longer than 30 seconds, acknowledge the request immediately and process asynchronously. Return 200 to indicate the job was accepted.

Automatic Retries

Retry behavior depends on the job type:

One-time Jobs (run_at)

One-time jobs automatically retry with exponential backoff:

Attempt Delay
1st retry 1 minute
2nd retry 5 minutes
3rd retry 30 minutes
4th retry 2 hours
5th retry 8 hours

After 5 failed attempts, the job is marked as permanently failed and you'll receive an alert (if configured).

Recurring Jobs (cron)

Cron jobs do not retry. If a scheduled run fails, the next scheduled run will execute as normal. This prevents retry storms and duplicate executions.

Why no retries for cron? A minute-interval cron with 5 retries would create chaos - by the time retries finish, hundreds of scheduled runs would have stacked up. The next scheduled run is effectively your retry.

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-Cronly-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-cronly-execution-id'];

  if (processed.has(executionId)) {
    return res.json({ status: 'already_processed' });
  }

  // Process the job...
  await doWork();

  processed.add(executionId);
  res.json({ status: 'ok' });
});

Custom Headers

You can configure custom headers when creating a job:

curl -X POST https://cronly.eu/api/jobs \
  -H "Authorization: Bearer pk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My job",
    "url": "https://myapp.com/webhook",
    "cron": "0 * * * *",
    "headers": {
      "Authorization": "Bearer my-app-token"
    }
  }'