IronWatchDocs
Dashboard
CronPilot · Quickstart
~3 min readsynced 2026-05-29·/v1 routes@live

Your first scheduled job

One curl creates a job that fires every 5 minutes against any HTTPS URL. Grab a key from Settings → API Keys, swap in your URL, and paste this into your terminal. The rest of this page explains each step in detail.

~/projects/acme · zsh
$curl -X POST "https://cronpilot.dev/v1/schedules" \ -H "Authorization: Bearer <CRONPILOT_API_KEY>" \ -H "Content-Type: application/json" \ -d { "name": "Health check", "cron": "*/5 * * * *", "url": "https://your-api.com/health" }'
201Created· 142ms · 312 bytes
{
  "id":           "d290f1ee-6c54-4b01-90e6-d701748f0851",
  "name":         "Health check",
  "cron":         "*/5 * * * *",
  "timezone":     "UTC",
  "url":          "https://your-api.com/health",
  "method":       "POST",
  "status":       "active",
  "next_fire_at": "2026-05-29T18:00:00.000Z",
  "created_at":   "2026-05-29T17:55:00.000Z"
}

What is CronPilot?

HTTP cron

CronPilot

HTTP cron, with receipts.

CronPilot turns a single POST into a reliable, retried, time-zoned schedule that fires HTTP requests at the cadence you specify and reports back what happened.

What it does
  • Schedule HTTP requests on a cron expression or one-shot delay
  • Automatic retries, deduplication keys, signed delivery requests
  • Durable message log: status, duration, response, every attempt
What it does not do
  • ↛ Not usRun code in our infrastructure
    We hit your HTTP endpoint. We don't execute user code.
    → try fly.io / lambda / your own workers
  • ↛ Not usCatch a missed schedule
    CronPilot tells you when a fire failed. PingWatch tells you when a fire never happened.
    → try PingWatch
  • ↛ Not usShow schedule status to your users
    Public-facing receipts live in FlipSign incidents and components.
    → try FlipSign

Authentication

Every API request carries a Bearer token. Create a key in Settings → API Keys — the full key (it starts with cp_sk_) is shown once at creation, so store it somewhere safe. Keys are hashed on our side; only the prefix stays visible in the dashboard for identification.

Header
Authorization: Bearer cp_sk_a1b2c3d4e5f6...

Create a schedule

A schedule is a recurring HTTP request driven by a standard 5-field cron expression. The required fields are namerequired, cronrequired, and urlrequired; everything else has a sensible default.

curl
curl -X POST "https://cronpilot.dev/v1/schedules" \
  -H "Authorization: Bearer <CRONPILOT_API_KEY>" \
  -H "Content-Type: application/json" \
  -d {
    "name": "Cleanup stale sessions",
    "cron": "0 */6 * * *",
    "url": "https://api.example.com/cleanup",
    "method": "POST",
    "timezone": "America/New_York"
  }'

One-shot messages (delays)

Need a single fire after a delay instead of a recurring schedule? Publish a message. It returns 202 Accepted — once accepted, the message is in the durable log and will be delivered even if our infrastructure restarts. Pass a deduplication_id to make the publish safe to retry.

curl
curl -X POST "https://cronpilot.dev/v1/messages" \
  -H "Authorization: Bearer <CRONPILOT_API_KEY>" \
  -H "Content-Type: application/json" \
  -d {
    "url": "https://your-api.com/send-reminder",
    "delay": 3600,
    "deduplication_id": "reminder-u_123-2026-05-29"
  }'
Response · 202
{
  "message_id":   "a3f2c1e0-...",
  "scheduled_at": "2026-05-29T18:55:00.000Z"
}

Verify the signature

When a signing secret is configured (per schedule or queue), every outbound request CronPilot fires at your endpoint carries an X-CronPilot-Signature header (v1=<hex HMAC-SHA256>) computed over {timestamp}.{body}, plus an X-CronPilot-Timestamp and an X-CronPilot-Message-Id for idempotency on your side. Verify with a constant-time compare and reject timestamps older than five minutes.

synced
import { createHmac } from "crypto";

function verify(req, secret) {
  const sig  req.headers["x-cronpilot-signature"]; // v1=<hex>
  const ts   req.headers["x-cronpilot-timestamp"]; // unix seconds

  // Reject if the timestamp is too old (replay protection)
  if (Math.abs(Date.now()  1000  Number(ts))  300) {
    throw new Error("Timestamp too old");
  }

  const payload   ts  "."  req.body; // raw body, not parsed
  const expected  "v1="  createHmac("sha256", secret)
    .update(payload)
    .digest("hex");

  if (sig  expected) throw new Error("Invalid signature");
}

Next steps

Ship it.
Open the dashboard, drop in a real URL, and watch your first run land in the log.