Getting started

CronSafe monitors your cron jobs by receiving HTTP pings. If a ping doesn't arrive on time, we alert you. Get running in under a minute:

1

Create an account

Sign up at getcronsafe.com — free, no credit card.

2

Create a monitor

Give it a name and set the expected interval. You'll get a unique slug.

3

Add one line to your cron job

curl -fsS https://api.getcronsafe.com/ping/YOUR_MONITOR_SLUG

That's it. If this request stops arriving, you get alerted.

Base URL

https://api.getcronsafe.com

Authentication

All API endpoints except /ping/* and /api/badge/* require a JWT token. Include it as a Bearer token in the Authorization header.

POST/api/auth/register

Create a new account. Returns a JWT token valid for 30 days.

Request

curl -X POST https://api.getcronsafe.com/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "you@example.com",
    "password": "your-secure-password"
  }'

Response — 201 Created

{
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "you@example.com",
    "plan": "free",
    "created_at": "2026-03-26T12:00:00.000Z"
  },
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
FieldTypeRules
emailstringValid email address, unique
passwordstringMin 8 characters, max 128

Errors: 400 invalid input, 409 email already registered.

POST/api/auth/login

Log in with existing credentials. Returns a JWT token valid for 30 days.

Request

curl -X POST https://api.getcronsafe.com/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "password": "your-secure-password"}'

Response — 200 OK

{
  "user": { "id": "uuid", "email": "you@example.com", "plan": "free", "created_at": "..." },
  "token": "eyJhbGciOiJIUzI1NiIs..."
}

Errors: 400 missing fields, 401 wrong credentials.

Using your token

curl https://api.getcronsafe.com/api/monitors \
  -H "Authorization: Bearer YOUR_TOKEN"

Monitors

Monitors represent the cron jobs you want to track. Each monitor gets a unique 12-character slug used for pinging.

POST/api/monitors

Create a new monitor.

Requires Authorization: Bearer TOKEN

FieldTypeRequiredDescription
namestringYesHuman-readable name (max 255 chars)
expected_interval_minutesintegerYesHow often the job runs (positive int)
grace_period_minutesintegerNoBuffer before alerting (default: 5)
alert_channelsarrayNoArray of channel objects (default: [])

Request

curl -X POST https://api.getcronsafe.com/api/monitors \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Nightly database backup",
    "expected_interval_minutes": 1440,
    "grace_period_minutes": 10,
    "alert_channels": [
      { "type": "email", "address": "ops@example.com" },
      { "type": "slack", "webhook_url": "https://hooks.slack.com/services/T00/B00/xxx" }
    ]
  }'

Response — 201 Created

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Nightly database backup",
  "slug": "aBcDeFgHiJkL",
  "expected_interval_minutes": 1440,
  "grace_period_minutes": 10,
  "status": "pending",
  "last_ping_at": null,
  "alert_channels": [...],
  "created_at": "2026-03-26T12:00:00.000Z"
}

Errors: 400 invalid input, 403 monitor limit reached.

GET/api/monitors

List all monitors for the authenticated user. Sorted by creation date, newest first.

Requires Authorization: Bearer TOKEN

curl https://api.getcronsafe.com/api/monitors \
  -H "Authorization: Bearer YOUR_TOKEN"

Response — 200 OK

[
  { "id": "uuid", "name": "Nightly backup", "slug": "aBcDeFgHiJkL", "status": "up", ... },
  { "id": "uuid", "name": "Hourly sync", "slug": "xYzAbCdEfGhI", "status": "pending", ... }
]
PUT/api/monitors/:id

Update a monitor. Only send the fields you want to change.

Requires Authorization: Bearer TOKEN

curl -X PUT https://api.getcronsafe.com/api/monitors/MONITOR_ID \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Updated name", "grace_period_minutes": 15}'

Errors: 400 invalid input, 403 not your monitor, 404 not found.

DELETE/api/monitors/:id

Delete a monitor and all its ping history. This cannot be undone.

Requires Authorization: Bearer TOKEN

curl -X DELETE https://api.getcronsafe.com/api/monitors/MONITOR_ID \
  -H "Authorization: Bearer YOUR_TOKEN"

Returns 204 No Content on success. Errors: 403, 404.

GET/api/monitors/:id/pings

Get ping history for a monitor. Returns the most recent pings, newest first.

Requires Authorization: Bearer TOKEN

ParamTypeDefaultDescription
limitinteger50Number of pings to return (max 200)
offsetinteger0Pagination offset
curl "https://api.getcronsafe.com/api/monitors/MONITOR_ID/pings?limit=20" \
  -H "Authorization: Bearer YOUR_TOKEN"

Response — 200 OK

[
  { "id": "uuid", "monitor_id": "uuid", "received_at": "2026-03-26T03:00:12Z", "ip_address": "203.0.113.1" },
  { "id": "uuid", "monitor_id": "uuid", "received_at": "2026-03-26T02:00:08Z", "ip_address": "203.0.113.1" }
]

Pings

The ping endpoint is what your cron jobs call. It requires no authentication, has no request body, and is guaranteed to respond in under 50 milliseconds. Database writes happen asynchronously after the response is sent.

GET/ping/:slug

Record a heartbeat ping for a monitor.

Response — 200 OK

{ "ok": true }

Examples in every language

curl

curl -fsS https://api.getcronsafe.com/ping/aBcDeFgHiJkL

Python

import requests
requests.get("https://api.getcronsafe.com/ping/aBcDeFgHiJkL", timeout=5)

Node.js

await fetch("https://api.getcronsafe.com/ping/aBcDeFgHiJkL");

PHP

file_get_contents("https://api.getcronsafe.com/ping/aBcDeFgHiJkL");

Ruby

require "net/http"
Net::HTTP.get(URI("https://api.getcronsafe.com/ping/aBcDeFgHiJkL"))

Go

resp, err := http.Get("https://api.getcronsafe.com/ping/aBcDeFgHiJkL")
if err == nil { resp.Body.Close() }

Badges

Embed a live status badge in your GitHub README, wiki, or status page. The badge updates in real time — no caching.

GET/api/badge/:slug

Returns an SVG status badge image. No authentication required.

Monitor statusBadge
upGreen — "cron | passing"
downRed — "cron | failing"
pending / unknownGray — "cron | unknown"

Embed in Markdown

![CronSafe](https://api.getcronsafe.com/api/badge/aBcDeFgHiJkL)

Embed in HTML

<img src="https://api.getcronsafe.com/api/badge/aBcDeFgHiJkL" alt="CronSafe status">

Alert channels

Configure alert channels per monitor. When a monitor goes down or recovers, CronSafe sends notifications to all configured channels simultaneously.

Email

Send alerts to an email address. Works on all plans.

{ "type": "email", "address": "ops@example.com" }

Slack

Send alerts to a Slack channel via an incoming webhook.

Setup: Go to api.slack.com/apps → Create New App → Incoming Webhooks → Activate → Add New Webhook to Workspace → choose a channel → copy the webhook URL.

{ "type": "slack", "webhook_url": "https://hooks.slack.com/services/T00000/B00000/xxxx" }

Discord

Send alerts to a Discord channel via a webhook.

Setup: Server Settings → Integrations → Webhooks → New Webhook → copy URL.

{ "type": "discord", "webhook_url": "https://discord.com/api/webhooks/000/xxxx" }

Telegram

Send alerts to a Telegram chat via the Bot API.

Setup: Message @BotFather on Telegram → /newbot → copy the bot token. Then add the bot to your group and send a message. Get your chat_id from api.telegram.org/bot<TOKEN>/getUpdates.

{ "type": "telegram", "bot_token": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", "chat_id": "-1001234567890" }

Generic webhook

POST a JSON payload to any URL. Use this to integrate with PagerDuty, OpsGenie, custom systems, or anything with an HTTP API.

{ "type": "webhook", "url": "https://your-server.com/alerts" }

Payload format

{
  "monitor_id": "uuid",
  "monitor_name": "Nightly backup",
  "status": "down",
  "message": "[CronSafe] Monitor "Nightly backup" is DOWN. ...",
  "timestamp": "2026-03-26T03:15:00.000Z"
}

Integration guides

Add a ping to the end of your scheduled task. If the task fails or hangs, the ping never fires, and CronSafe alerts you.

Bash crontab

# Run backup every day at 3 AM, ping CronSafe on success
0 3 * * * /usr/bin/bash /scripts/backup.sh \
  && curl -fsS --retry 3 https://api.getcronsafe.com/ping/aBcDeFgHiJkL

Python script

import requests

def main():
    # Your task logic here
    print("Backup completed successfully")

if __name__ == "__main__":
    main()
    requests.get("https://api.getcronsafe.com/ping/aBcDeFgHiJkL", timeout=5)

Node.js

async function main() {
  // Your task logic here
  console.log("Task completed");
}

main()
  .then(() => fetch("https://api.getcronsafe.com/ping/aBcDeFgHiJkL"))
  .catch((err) => { console.error(err); process.exit(1); });

Docker entrypoint

#!/bin/sh
set -e

# Run the actual job
python /app/process_queue.py

# Ping on success
curl -fsS https://api.getcronsafe.com/ping/aBcDeFgHiJkL

GitHub Actions

name: Nightly build
on:
  schedule:
    - cron: '0 3 * * *'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - name: Ping CronSafe
        if: success()
        run: curl -fsS https://api.getcronsafe.com/ping/aBcDeFgHiJkL

Kubernetes CronJob

apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-backup
spec:
  schedule: "0 3 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: backup
              image: your-app:latest
              command: ["/bin/sh", "-c"]
              args:
                - |
                  python /app/backup.py && \
                  wget -qO- https://api.getcronsafe.com/ping/aBcDeFgHiJkL
          restartPolicy: OnFailure

Errors

All errors return a JSON object with a single error field containing a human-readable message.

{ "error": "Monitor limit reached. Upgrade your plan." }

HTTP status codes

CodeMeaningWhen
400Bad RequestInvalid input — missing fields, wrong types, bad email format
401UnauthorizedMissing or invalid JWT token
403ForbiddenAccessing another user's resource, or plan limit reached
404Not FoundMonitor or resource does not exist
429Too Many RequestsRate limit exceeded — auth: 10/min, API: 100/min, ping: 1000/min
500Internal Server ErrorUnexpected server error — please report if persistent

Rate limits

EndpointLimitWindow
/api/auth/*10 requests1 minute per IP
/api/*100 requests1 minute per IP
/ping/*1,000 requests1 minute per IP

When rate limited, the response includes Retry-After and X-RateLimit-* headers.