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.

EndpointMethodDescription
/p/{slug}GET POSTSuccess ping — resets the dead man's switch timer
/p/{slug}/{exit_code}GET POSTPing with exit code (non-zero triggers failure alert)
/p/{slug}/failGET POSTExplicit failure — creates incident, sends alerts immediately
/p/{slug}/startGET POSTStart 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 error
  • 2 — Misuse of shell command
  • 126 — Permission denied
  • 127 — Command not found
  • 137 — 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 /fail ping 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

TierMinimum IntervalScheduling
Free5 minutesWorker cron trigger (*/5 * * * *)
Pro1 minuteDurable Object alarm (CheckAlarm)
Team30 secondsDurable 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.

TypeConfig FieldNotes
Slackwebhook_urlIncoming Webhook URL from Slack app settings
Discordwebhook_urlChannel webhook URL from server settings
EmailemailRequires 6-digit verification code (see below)
WebhookurlJSON 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
![db-backup](https://cron.win/badge/db-backup.svg)

# 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

EndpointMethodDescription
/api/monitorsPOSTCreate monitor
/api/monitorsGETList all monitors
/api/monitors/:idGETGet monitor details + uptime %
/api/monitors/:idPUTUpdate name, schedule, grace period
/api/monitors/:idDELETEDelete monitor and all associated data
/api/monitors/:id/pingsGETPing history (last 200)
/api/monitors/:id/alertsGETAlert history (last 100)
/api/monitors/:id/uptimeGET30-day uptime timeline with daily status
/api/monitors/:id/incidentsGETIncident history (last 50)
/api/monitors/:id/pausePOSTPause monitoring
/api/monitors/:id/resumePOSTResume monitoring
/api/monitors/importPOSTBulk import from crontab text

Checks API

EndpointMethodDescription
/api/checksPOSTCreate HTTP check
/api/checksGETList all checks
/api/checks/:idGETGet check details
/api/checks/:idPUTUpdate check settings
/api/checks/:idDELETEDelete check and results
/api/checks/:id/historyGETResult history (last 200)

Channels API

EndpointMethodDescription
/api/channelsPOSTCreate channel (email requires verification)
/api/channelsGETList all channels
/api/channels/:idDELETEDelete channel
/api/channels/:id/testPOSTSend test message
/api/monitors/:id/channelsPOSTLink channel to monitor
/api/monitors/:id/channelsGETGet linked channels for monitor
/api/monitors/:id/channels/:chidDELETEUnlink channel from monitor

Keys API

EndpointMethodDescription
/api/keysPOSTGenerate new API key (raw key shown once)
/api/keysGETList keys (prefix, name, last_used_at)
/api/keys/:idDELETERevoke key

Other Endpoints

EndpointMethodDescription
/api/meGETCurrent user info, tier, limits, monitor count
/api/status-pagePUTConfigure public status page (slug, title, public flag)
/api/status/:slugGETPublic status page data (no auth required)
/api/exportGETExport all data as JSON download
/badge/:slug.svgGETStatus badge SVG (no auth required)
/api/stripe/webhookPOSTStripe webhook handler (HMAC-SHA256 verified)

Pricing & Limits

Free
Pro · $5/mo
Team · $15/mo
Monitors + Checks
3
20
100
Ping retention
3 days
90 days
1 year
Min grace period
60s
30s
10s
Check interval
5 min
1 min
30s
Alert channels
1
10
25
API keys
1
5
20
Email alerts
Bulk import
Status page
Badge embeds
Data export

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

ComponentTechnology
RuntimeCloudflare Workers (V8 isolates, 300+ locations, zero cold starts)
DatabaseCloudflare D1 (SQLite at the edge)
Dead Man's SwitchDurable Objects — MonitorAlarm class
Uptime ChecksDurable Objects — CheckAlarm class
AuthGitHub OAuth (read:user, user:email scopes)
PaymentsStripe Checkout + Webhooks
EmailResend API (optional)
FrontendVanilla HTML/CSS/JS — single file SPA, no build step
DeploymentWrangler 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
  }'