cron.win Documentation
Everything you need to monitor cron jobs, websites, and APIs from Cloudflare's global edge. Dead man's switch monitoring, HTTP uptime checks, multi-channel alerting, public status pages, and a full REST API.
Quick Start
Get monitoring in 60 seconds:
1. Sign in at cron.win with GitHub.
2. Create a monitor — pick a name and slug.
3. Add one line to your crontab:
# Ping cron.win after your backup succeeds
0 2 * * * /usr/bin/pg_dump mydb > /backups/db.sql && curl -fsS https://cron.win/p/db-backup
If the ping doesn't arrive within your configured grace period (default: 5 minutes), you get alerted on Slack, Discord, email, or webhook.
Tip: Use curl -fsS — the -f flag makes curl return a non-zero exit code on HTTP errors, -s silences progress bars, and -S still shows errors.
How It Works
cron.win uses two monitoring patterns:
Dead Man's Switch (Cron Monitors)
Instead of polling your server, your cron job pings us. Each monitor has a Cloudflare Durable Object that sets an alarm timer equal to your grace period. Every successful ping resets the timer. If the timer fires without a ping, cron.win creates an incident and sends alerts to all linked channels.
Active HTTP Checks (Uptime Monitoring)
cron.win sends HTTP requests from Cloudflare's edge (300+ cities) to your URLs at configurable intervals (30s–5min). Each check also uses a dedicated Durable Object for precise alarm-based scheduling, ensuring sub-second accuracy even at 30-second intervals.
Authentication
cron.win supports two authentication methods:
Session cookie — Set automatically after GitHub OAuth sign-in. Used by the browser dashboard. Sessions expire after 30 days.
API key — For programmatic access. Pass via X-API-Key header or Authorization: Bearer cw_.... Keys start with the cw_ prefix. See API Keys for details.
# Using API key header
curl -H "X-API-Key: cw_a1b2c3d4e5f6..." https://cron.win/api/monitors
# Using Bearer token
curl -H "Authorization: Bearer cw_a1b2c3d4e5f6..." https://cron.win/api/monitors
Ping API
Ping endpoints are unauthenticated — just curl them from your scripts. All HTTP methods work (GET, POST, HEAD). The slug is the unique identifier you set when creating a monitor.
| Endpoint | Method | Description |
|---|---|---|
/p/{slug} | GET POST | Success ping — resets the dead man's switch timer |
/p/{slug}/{exit_code} | GET POST | Ping with exit code (non-zero triggers failure alert) |
/p/{slug}/fail | GET POST | Explicit failure — creates incident, sends alerts immediately |
/p/{slug}/start | GET POST | Start a timer — duration calculated on next success ping |
Response
{
"ok": true,
"duration_ms": 4523 // present if /start was called first
}
Request Body
You can POST a body (up to 10KB) with logs or output — it's stored with the ping for debugging:
/usr/bin/pg_dump mydb 2>&1 | tail -20 | curl -fsS -X POST -d @- https://cron.win/p/db-backup
Exit Codes
Append the exit code to the ping URL. Code 0 = success, anything else triggers a failure alert and creates an incident:
# $? is the exit code of the previous command
0 * * * * /opt/scripts/etl.sh; curl -fsS https://cron.win/p/etl/$?
Common exit codes:
0— Success (no alert)1— General error2— Misuse of shell command126— Permission denied127— Command not found137— Killed by signal (OOM killer)143— Terminated (SIGTERM)
Duration Tracking
Track how long your jobs take by calling /start before and the success ping after:
# Start the timer
curl -fsS https://cron.win/p/backup/start
# ... your job runs ...
/usr/bin/pg_dump mydb > /backups/db.sql
# End — response includes duration_ms
curl -fsS https://cron.win/p/backup
Duration is stored per ping and shown in the dashboard ping history. The timer uses a Durable Object for accuracy.
Cron Expressions
When creating a monitor, set the schedule field to a standard 5-part cron expression. cron.win parses it to display:
- Human-readable label — e.g., "Daily at 02:00" or "Every 5 min"
- Next expected at — calculated next run time, shown in the dashboard
Supported formats:
*/5 * * * *→ "Every 5 min"0 2 * * *→ "Daily at 02:00"30 */6 * * *→ "Every 6h at :30"0 9 * * 1→ "Mon 09:00"0 0 1 * *→ "Day 1 at 00:00"- Ranges (
1-5), lists (1,15), and steps (*/10) are all supported
Monitor Management
Create monitors via the dashboard or the API. Each monitor has a unique slug, name, cron schedule (optional), and grace period.
Create a Monitor
curl -X POST https://cron.win/api/monitors \
-H "X-API-Key: cw_your_key" \
-H "Content-Type: application/json" \
-d '{
"name": "DB Backup",
"slug": "db-backup",
"schedule": "0 2 * * *",
"grace_seconds": 300
}'
Response
{
"id": "abc12345",
"slug": "db-backup",
"ping_url": "https://cron.win/p/db-backup",
"schedule": { "human": "Daily at 02:00", "next": "2025-01-16T02:00:00.000Z" }
}
Grace period: The time cron.win waits after the expected ping before alerting. Minimum depends on tier: Free (60s), Pro (30s), Team (10s). Default is 300s (5 minutes).
Slug format: Monitor slugs can only contain lowercase letters, numbers, and hyphens (a-z, 0-9, -). Dots are not allowed in monitor slugs (but are allowed in status page slugs). Other characters are silently stripped.
Pause & Resume
Doing maintenance? Pause monitoring to stop alerts without deleting the monitor:
# Pause
curl -X POST https://cron.win/api/monitors/{id}/pause -H "X-API-Key: cw_..."
# Resume
curl -X POST https://cron.win/api/monitors/{id}/resume -H "X-API-Key: cw_..."
Paused monitors show as "Operational" on status pages and ignore incoming pings.
Incidents
An incident is created when:
- A dead man's switch timer fires (no ping received within grace period)
- A ping reports a non-zero exit code
- An explicit
/failping is received - An HTTP uptime check fails (confirmed by multi-region verify)
When the next successful ping arrives (or the check recovers), the incident is automatically resolved with the total downtime calculated. Incidents feed into the uptime percentage and the 90-day timeline.
Uptime Timeline
Each monitor tracks uptime over a rolling 30-day window (or 90 days on status pages). The timeline shows per-day status:
- Green — No incidents that day
- Amber — Partial downtime (< 12 hours)
- Red — Major downtime (> 12 hours)
Access via GET /api/monitors/{id}/uptime or the dashboard monitor detail view.
Uptime Checks (HTTP)
Unlike cron monitors (which wait for you to ping), uptime checks actively request your URLs from Cloudflare's global edge.
- Supports any HTTP method (GET, POST, PUT, DELETE, PATCH, HEAD, etc.) — defaults to GET
- Configurable expected status code (default: 200)
- Custom request headers and body
- Configurable timeout (default: 10 seconds)
- Response time tracking per check
- Each check gets its own Durable Object for precise scheduling
Check API
Create a Check
curl -X POST https://cron.win/api/checks \
-H "X-API-Key: cw_your_key" \
-H "Content-Type: application/json" \
-d '{
"name": "API Health",
"url": "https://api.example.com/health",
"method": "GET",
"expected_status": 200,
"interval_seconds": 60,
"timeout_ms": 10000,
"headers": {"Authorization": "Bearer token123"}
}'
Check Intervals
| Tier | Minimum Interval | Scheduling |
|---|---|---|
| Free | 5 minutes | Worker cron trigger (*/5 * * * *) |
| Pro | 1 minute | Durable Object alarm (CheckAlarm) |
| Team | 30 seconds | Durable Object alarm (CheckAlarm) |
Pro and Team checks use a dedicated CheckAlarm Durable Object that self-reschedules after each run, providing sub-5-minute precision without relying on the worker's cron trigger.
Multi-Region Verification
When a check first fails, cron.win waits 2 seconds and retries before creating an incident. If the retry succeeds, the failure is treated as a false positive and no alert is sent.
This reduces noise from transient network issues, CDN blips, or single-region outages.
Response Time
Each check records the HTTP response time in milliseconds. The latest response time is shown on the dashboard and included in status page data. Historical response times are available via GET /api/checks/{id}/history.
Alert Channels
Channels are notification destinations you create and then link to monitors. When a monitor goes down (or recovers), all linked channels receive alerts. You can link the same channel to multiple monitors.
| Type | Config Field | Notes |
|---|---|---|
| Slack | webhook_url | Incoming Webhook URL from Slack app settings |
| Discord | webhook_url | Channel webhook URL from server settings |
email | Requires 6-digit verification code (see below) | |
| Webhook | url | JSON POST to any URL with structured event payload |
Slack Setup
1. Go to your Slack app settings → Incoming Webhooks
2. Create a new webhook and choose the channel
3. Copy the webhook URL
4. In cron.win, create a channel with type "Slack" and paste the URL
Slack alerts include rich formatting with monitor name, status, and a direct link to the monitor page.
Discord Setup
1. Server Settings → Integrations → Webhooks
2. Create a new webhook for your channel
3. Copy the webhook URL
4. In cron.win, create a channel with type "Discord" and paste the URL
Email Alerts
Email alerts are sent via Resend. Requires the RESEND_API_KEY worker secret to be configured. Emails come from [email protected] and include HTML formatting with monitor name and a direct link.
Requires: Email alerts are powered by Resend. The RESEND_API_KEY worker secret must be configured. Email channels are available on all tiers, but the channel count limit applies (Free: 1 channel, Pro: 10, Team: 25).
Webhook Payload
Custom webhooks receive a JSON POST with a structured event payload:
{
"event": "monitor.down",
"monitor_id": "abc12345",
"monitor_name": "DB Backup",
"message": "Monitor \"DB Backup\" is DOWN — no ping received within grace period (300s)",
"timestamp": "2025-01-15T02:05:00.000Z"
}
Event types: monitor.down, check.down, check.up, test
Retry Logic
Failed alert deliveries retry up to 3 times with a 5-second delay between each attempt (capped at 5s to stay within Workers CPU limits). Final delivery status (sent/failed) and retry count are logged in the alerts table and visible in the monitor detail view.
Test Channels
Verify your integration works before relying on it in production:
curl -X POST https://cron.win/api/channels/{id}/test \
-H "X-API-Key: cw_your_key"
Returns {"ok":true,"message":"Test sent!"} on success. The test message says "✅ Test alert from cron.win — your [type] integration is working!"
In the dashboard, click the 🧪 Test button next to any channel to trigger a test.
Email Verification
When adding an email channel, cron.win sends a 6-digit verification code to the email address. The code expires in 10 minutes. Enter the code to confirm ownership before the channel is created.
This prevents anyone from adding arbitrary email addresses as alert destinations.
Public Status Pages
Share uptime with your users at https://cron.win/status/{slug}. Status pages include:
- Overall system status banner ("All Systems Operational" / "Some Systems Degraded")
- 90-day uptime bars per monitor and uptime check (green/amber/red per day)
- Uptime percentage per service
- Response time for HTTP checks
- Customizable title
curl -X PUT https://cron.win/api/status-page \
-H "X-API-Key: cw_your_key" \
-H "Content-Type: application/json" \
-d '{"title":"Acme Corp Status","slug":"acme","public":true}'
Paused monitors appear as "Operational" on status pages. Dots in slugs are allowed (e.g., acme.io).
API Keys
Generate API keys for CI/CD pipelines, scripts, or integrations. Keys are SHA-256 hashed before storage — the raw key is only shown once on creation.
curl -X POST https://cron.win/api/keys \
-H "Cookie: session=..." \
-H "Content-Type: application/json" \
-d '{"name":"CI/CD Pipeline"}'
// Response (key shown ONCE):
{
"id": "abc",
"key": "cw_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"prefix": "cw_a1b2c3..."
}
Key limits per tier: Free (1), Pro (5), Team (20). The last_used_at timestamp is updated on each API call.
Badge Embeds
Add live status badges to your READMEs, docs, or dashboards:
# Markdown

# HTML
<img src="https://cron.win/badge/db-backup.svg" alt="status">
Badges show current status with color coding: up (green), down (red), failing (amber), paused/new (gray). Cached for 30 seconds via Cache-Control. Badge slugs follow the same format as monitor slugs: lowercase letters, numbers, and hyphens only (no dots).
Bulk Import
Import monitors from your crontab in one request:
curl -X POST https://cron.win/api/monitors/import \
-H "X-API-Key: cw_your_key" \
-H "Content-Type: application/json" \
-d '{
"crontab": "0 2 * * * /usr/bin/pg_dump mydb\n*/5 * * * * /opt/healthcheck.sh\n# This is a comment (skipped)",
"grace_seconds": 300
}'
Each line is parsed for the 5-part cron schedule and command name. Comment lines (#) are skipped. Monitor names are auto-generated from the command's filename. Monitors are created up to your tier limit.
In the dashboard, use the 📥 Import button and paste your crontab -l output.
Data Export
Download all your monitoring data as a single JSON file:
curl -H "X-API-Key: cw_your_key" https://cron.win/api/export > cron-win-export.json
The export includes:
- All monitors with configuration and uptime stats
- All uptime checks with latest results
- All channels (config excluded for security)
- Last 1,000 pings per monitor
- Last 200 incidents per monitor
- Last 500 check results per check
- Export timestamp and user metadata
In the dashboard, click the 💾 Export button to download immediately.
Dashboard
The dashboard auto-refreshes every 10 seconds and shows:
- Total monitors count, up/down/failing status summary
- Per-monitor cards with status indicator, schedule info, last ping time
- Uptime percentage badges (color-coded: green ≥ 99%, amber ≥ 95%, red < 95%)
- Monitor detail view with ping history, alert log, uptime timeline, incident list
- Uptime checks tab with URL, method, interval, response time
- Channel management modal with test and link/unlink controls
- Status page configuration
Monitors API
| Endpoint | Method | Description |
|---|---|---|
/api/monitors | POST | Create monitor |
/api/monitors | GET | List all monitors |
/api/monitors/:id | GET | Get monitor details + uptime % |
/api/monitors/:id | PUT | Update name, schedule, grace period |
/api/monitors/:id | DELETE | Delete monitor and all associated data |
/api/monitors/:id/pings | GET | Ping history (last 200) |
/api/monitors/:id/alerts | GET | Alert history (last 100) |
/api/monitors/:id/uptime | GET | 30-day uptime timeline with daily status |
/api/monitors/:id/incidents | GET | Incident history (last 50) |
/api/monitors/:id/pause | POST | Pause monitoring |
/api/monitors/:id/resume | POST | Resume monitoring |
/api/monitors/import | POST | Bulk import from crontab text |
Checks API
| Endpoint | Method | Description |
|---|---|---|
/api/checks | POST | Create HTTP check |
/api/checks | GET | List all checks |
/api/checks/:id | GET | Get check details |
/api/checks/:id | PUT | Update check settings |
/api/checks/:id | DELETE | Delete check and results |
/api/checks/:id/history | GET | Result history (last 200) |
Channels API
| Endpoint | Method | Description |
|---|---|---|
/api/channels | POST | Create channel (email requires verification) |
/api/channels | GET | List all channels |
/api/channels/:id | DELETE | Delete channel |
/api/channels/:id/test | POST | Send test message |
/api/monitors/:id/channels | POST | Link channel to monitor |
/api/monitors/:id/channels | GET | Get linked channels for monitor |
/api/monitors/:id/channels/:chid | DELETE | Unlink channel from monitor |
Keys API
| Endpoint | Method | Description |
|---|---|---|
/api/keys | POST | Generate new API key (raw key shown once) |
/api/keys | GET | List keys (prefix, name, last_used_at) |
/api/keys/:id | DELETE | Revoke key |
Other Endpoints
| Endpoint | Method | Description |
|---|---|---|
/api/me | GET | Current user info, tier, limits, monitor count |
/api/status-page | PUT | Configure public status page (slug, title, public flag) |
/api/status/:slug | GET | Public status page data (no auth required) |
/api/export | GET | Export all data as JSON download |
/badge/:slug.svg | GET | Status badge SVG (no auth required) |
/api/stripe/webhook | POST | Stripe webhook handler (HMAC-SHA256 verified) |
Pricing & Limits
Monitors + Checks share the same quota. If your tier allows 20, that's 20 total across both cron monitors and uptime checks combined.
Rate Limits
- Ping endpoints: Max 1 ping per second per slug. Excess pings return
{"ok":true,"status":"rate_limited"}with HTTP 200 (so curl doesn't error). - API endpoints: Standard Cloudflare Workers limits apply. No artificial throttling.
- Uptime checks: Run at configured interval per Durable Object alarm.
- Alerts: Sent immediately on state change, max 3 retries per channel per event.
- Stripe webhooks: HMAC-SHA256 signature verified with 600s clock tolerance (for Workers edge latency).
Tech Stack
| Component | Technology |
|---|---|
| Runtime | Cloudflare Workers (V8 isolates, 300+ locations, zero cold starts) |
| Database | Cloudflare D1 (SQLite at the edge) |
| Dead Man's Switch | Durable Objects — MonitorAlarm class |
| Uptime Checks | Durable Objects — CheckAlarm class |
| Auth | GitHub OAuth (read:user, user:email scopes) |
| Payments | Stripe Checkout + Webhooks |
| Resend API (optional) | |
| Frontend | Vanilla HTML/CSS/JS — single file SPA, no build step |
| Deployment | Wrangler CLI |
Examples
Basic Crontab
# Success ping after backup
0 2 * * * /usr/bin/pg_dump mydb > /backups/db.sql && curl -fsS https://cron.win/p/db-backup
# Report exit code
0 * * * * /opt/scripts/etl.sh; curl -fsS https://cron.win/p/etl/$?
# Duration tracking
0 2 * * * curl -fsS https://cron.win/p/backup/start && /usr/bin/pg_dump mydb && curl -fsS https://cron.win/p/backup
# Failure on error
0 3 * * * /opt/scripts/job.sh || curl -fsS https://cron.win/p/job/fail
Python with Error Handling
import subprocess, requests
try:
result = subprocess.run(
["python", "etl.py"],
capture_output=True,
timeout=3600
)
requests.get(f"https://cron.win/p/etl/{result.returncode}")
except Exception as e:
requests.post("https://cron.win/p/etl/fail", data=str(e)[:10000])
Docker Health Check
HEALTHCHECK --interval=60s --timeout=5s \
CMD curl -fsS https://cron.win/p/my-container || exit 1
GitHub Actions
- name: Notify cron.win
if: always()
run: |
if [ "${{ job.status }}" = "success" ]; then
curl -fsS https://cron.win/p/nightly-build
else
curl -fsS https://cron.win/p/nightly-build/fail
fi
Kubernetes CronJob
apiVersion: batch/v1
kind: CronJob
metadata:
name: db-backup
spec:
schedule: "0 2 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: postgres:16
command:
- /bin/sh
- -c
- |
pg_dump $DATABASE_URL > /backups/db.sql && \
curl -fsS https://cron.win/p/k8s-backup || \
curl -fsS https://cron.win/p/k8s-backup/fail
Node.js Heartbeat
const cron = require('node-cron');
cron.schedule('*/5 * * * *', async () => {
try {
await fetch('https://cron.win/p/app-heartbeat');
} catch (e) {
console.error('Ping failed', e);
}
});
Laravel Scheduled Task
// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
$schedule->command('reports:generate')
->daily()
->after(function () {
file_get_contents('https://cron.win/p/daily-reports');
});
}
Terraform / API Provisioning
# Create monitors via API in your CI/CD pipeline
curl -X POST https://cron.win/api/monitors \
-H "X-API-Key: $CRON_WIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "'"$SERVICE_NAME"' health",
"slug": "'"$SERVICE_NAME"'-health",
"schedule": "*/5 * * * *",
"grace_seconds": 600
}'
# Create uptime check
curl -X POST https://cron.win/api/checks \
-H "X-API-Key: $CRON_WIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "'"$SERVICE_NAME"' API",
"url": "'"$SERVICE_URL"'/health",
"interval_seconds": 60
}'