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.
- Job triggers (cron schedule or one-time)
- Cronly 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 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:
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 |
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 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.
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:
// 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" } }'