IronWatchDocs
Dashboard
synced 2026-05-29·/v1 routes@live
CronPilot · API reference

API reference

Everything you need to schedule HTTP requests, fire one-shot delayed messages, and publish to durable queues. New to CronPilot? Start with the quickstart.

Base URL & authentication

The base URL is https://cronpilot.dev. Every request carries a Bearer token (cp_sk_…) created in Settings → API Keys. A missing, placeholder, invalid, or revoked key returns 401 with a descriptive message.

Schedules

Recurring HTTP requests driven by cron expressions. Each firing creates a message in the durable log. Full CRUD lives under /v1/schedules (GET, POST, GET /:id, PUT /:id, DELETE /:id); the create call is documented below.

POST/v1/schedules
Bearertier daily quota

Create a recurring schedule. Returns the full schedule object including the first computed next_fire_at.

Request body
name
string·REQUIRED
Human-readable label for this schedule.
cron
string·REQUIRED
Standard 5-field cron expression, evaluated in the schedule's timezone.
"*/5 * * * *""0 */6 * * *""0 3 * * *"
url
string·REQUIRED
HTTPS endpoint to call. Plain HTTP and private/internal addresses are rejected (422).
method
string·OPTIONAL
HTTP verb used for each fire.
default·"POST"
"GET""POST""PUT""PATCH""DELETE"
headers
object·OPTIONAL
Custom headers included on every request.
body
string·OPTIONAL
Request body sent on each fire. Max 100KB.
timezone
string·OPTIONAL
IANA timezone the cron expression is interpreted in. DST is handled per IANA rules.
default·"UTC"
retries
integer·OPTIONAL
Max retry attempts on failure. Default is fire-and-forget; capped by your tier's max.
default·0
Responses
201Created
Schedule created and active. Body is the full schedule object, including the computed next_fire_at. If the target domain is unverified, a warning field is included.
400Bad Request
Validation failed — missing required field, invalid cron expression, non-HTTPS URL, unsupported method, or an interval below your tier's minimum.
error.codename is requiredcron is requiredInvalid cron expression: …Only HTTPS URLs are allowedminimum interval …
401Unauthorized
Missing, placeholder, invalid, or revoked API key.
422Unprocessable
SSRF guard blocked the target — the URL resolves to a private, loopback, or metadata address.
error.codeSSRF blocked
Request
POST /v1/schedules
curl -X POST "https://cronpilot.dev/v1/schedules" \
  -H "Authorization: Bearer <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"
     }'
Response201Created
application/json
{
"id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
"name": "Cleanup stale sessions",
"cron": "0 */6 * * *",
"timezone": "America/New_York",
"url": "https://api.example.com/cleanup",
"method": "POST",
"status": "active",
"next_fire_at": "2026-05-29T18:00:00.000Z",
"created_at": "2026-05-29T15:32:00.000Z"
}

Messages (delays)

One-shot HTTP requests delivered after a delay or at a specific time. Returns 202 Accepted — once accepted, the message is durable and will be delivered even if our infrastructure restarts.

POST/v1/messages
BearerIdempotenttier daily + monthly quota

Publish a single delayed (or immediate) HTTP request to the durable log.

Request body
url
string·REQUIRED
HTTPS endpoint to call.
method
string·OPTIONAL
HTTP verb.
default·"POST"
"GET""POST""PUT""PATCH""DELETE"
headers
object·OPTIONAL
Custom headers.
body
string·OPTIONAL
Request body. Max 100KB.
delay
integer·OPTIONAL
Seconds from now to deliver. Cannot be combined with scheduled_at.
scheduled_at
string·OPTIONAL
ISO 8601 timestamp for delivery. Cannot be combined with delay.
deduplication_id
string·OPTIONAL
Unique key to prevent duplicate publishes. A reuse returns 409.
retries
integer·OPTIONAL
Max retries on failure. Capped by tier.
default·0
callback_queue_id
string·OPTIONAL
Queue to publish the delivery result to. Must be a queue you own.
Responses
202Accepted
Message accepted into the durable log. Body is { message_id, scheduled_at }. If neither delay nor scheduled_at was given, it is delivered immediately.
400Bad Request
Invalid URL, non-HTTPS, both delay and scheduled_at supplied, or an oversized body.
error.codeurl is requiredProvide either delay or scheduled_at, not bothBody exceeds 102400 byte limit
409Conflict
deduplication_id was used before.
error.codeDuplicate message …
422Unprocessable
SSRF guard blocked the target.
429Too Many Requests
Daily or monthly message quota reached, or the unverified-domain daily limit (10/day).
error.codeDaily/Monthly message limit reached
Request
POST /v1/messages
curl -X POST "https://cronpilot.dev/v1/messages" \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d {
       "url": "https://api.example.com/send-reminder",
       "delay": 3600,
       "deduplication_id": "reminder-u_123-2026-05-29"
     }'
Response202Accepted
application/json
{
"message_id": "a3f2c1e0-9b8d-4e21-8c0f-2d1e3f4a5b6c",
"scheduled_at": "2026-05-29T16:32:00.000Z"
}

Queues

Named queues with configurable concurrency. At concurrency: 1, messages are delivered strictly in order. Queues default to 3 retries — the semantic is "publish means deliver." Manage them under /v1/queues and publish to /v1/queues/{name}/messages.

POST/v1/queues
Bearertier daily quota

Create a named queue with concurrency, ordering, failure-mode, and optional rate limits.

Request body
name
string·REQUIRED
Queue name. Used in the publish URL.
concurrency
integer·OPTIONAL
Max messages dispatched simultaneously (1–100). At 1, delivery is strictly in order.
default·1
retries
integer·OPTIONAL
Default retries for messages in this queue. Capped by tier.
default·3
ordering
string·OPTIONAL
Delivery ordering.
default·"fifo"
"fifo""lifo"
failure_mode
string·OPTIONAL
How a dead message affects the queue.
default·"continue"
"continue""block""pause"
rate_limit
integer·OPTIONAL
Max messages per rate_period_seconds. Must be set together with rate_period_seconds.
rate_period_seconds
integer·OPTIONAL
Window for rate_limit, in seconds.
signing_secret_id
string·OPTIONAL
Signing secret applied to every message in this queue.
default_target_url
string·OPTIONAL
Fallback HTTPS URL for messages published without an explicit url.
default_target_group_id
string·OPTIONAL
Target group to fan a publish out across. Must be a group you own.
Responses
201Created
Queue created. Body includes concurrency, ordering, failure_mode, rate limits, and the blocked flags (always false for a fresh queue).
400Bad Request
Missing name, concurrency out of 1–100, invalid ordering/failure_mode, or rate_limit set without rate_period_seconds (or vice-versa).
error.codename is requiredconcurrency must be between 1 and 100rate_limit and rate_period_seconds must be set togetherordering must be one of: fifo, lifo
404Not Found
default_target_group_id refers to a group you don't own.
error.codedefault_target_group_id not found
422Unprocessable
default_target_url failed the SSRF guard.
Request
POST /v1/queues
curl -X POST "https://cronpilot.dev/v1/queues" \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d {
       "name": "email-sends",
       "concurrency": 5
     }'
Response201Created
application/json
{
"id": "b4c5d6e7-1234-4abc-9def-0123456789ab",
"name": "email-sends",
"concurrency": 5,
"max_retries": 3,
"ordering": "fifo",
"failure_mode": "continue",
"status": "active",
"rate_limit": null,
"rate_period_seconds": null,
"blocked": false,
"blocked_message_count": 0,
"created_at": "2026-05-29T15:32:00.000Z"
}
POST/v1/queues/{name}/messages
Bearertier daily quota

Publish a message onto a named queue. It inherits the queue's signing secret, retry config, and concurrency limit.

Path parameters
name
string·REQUIRED
The queue name from the publish URL.
Request body
url
string·OPTIONAL
HTTPS endpoint to call. Optional when the queue has a default_target_url or default_target_group_id.
method
string·OPTIONAL
HTTP verb.
default·"POST"
"GET""POST""PUT""PATCH""DELETE"
headers
object·OPTIONAL
Custom headers.
body
string·OPTIONAL
Request body. Max 100KB.
deduplication_id
string·OPTIONAL
Unique key to prevent duplicate publishes.
callback_queue_id
string·OPTIONAL
Queue to publish the delivery result to.
Responses
202Accepted
Message accepted onto the queue. Body is { message_id, queue, scheduled_at }; a fan-out across a target group returns { fan_out, message_ids, count, … } instead.
404Not Found
No queue with that name belongs to you.
429Too Many Requests
Daily message limit reached.
Request
POST /v1/queues/{name}/messages
curl -X POST "https://cronpilot.dev/v1/queues/:name/messages" \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d {
       "url": "https://api.example.com/send-email",
       "body": "{ \"to\": \"user@example.com\", \"template\": \"welcome\" }"
     }'
Response202Accepted
application/json
{
"message_id": "c5d6e7f8-2345-4bcd-8ef0-123456789abc",
"queue": "email-sends",
"scheduled_at": "2026-05-29T15:32:01.000Z"
}

Target groups

A target group is a named set of HTTPS endpoints. Pass its id as default_target_group_id on a queue or message and a single publish fans out to every member — each delivery is tracked and retried independently. Members are validated through the same HTTPS-only, SSRF-guarded checks as any other target.

POST/v1/target-groups
Bearertier daily quota

Create a target group. The name is unique within your account.

Request body
name
string·REQUIRED
Label for the group. Unique within your account; max 200 characters.
Responses
201Created
Group created. Body is { id, name, created_at, updated_at }.
400Bad Request
name missing, blank, or over 200 characters.
error.codename is required (max 200 chars)
401Unauthorized
Missing or invalid API key.
409Conflict
A target group with this name already exists in your account.
error.codeA target group with this name already exists
Request
POST /v1/target-groups
curl -X POST "https://cronpilot.dev/v1/target-groups" \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d {
       "name": "regional-webhooks"
     }'
Response201Created
application/json
{
"id": "f1e2d3c4-4567-4def-a012-3456789abcde",
"name": "regional-webhooks",
"created_at": "2026-05-29T15:40:00.000Z",
"updated_at": "2026-05-29T15:40:00.000Z"
}
POST/v1/target-groups/{id}/members
Bearertier daily quota

Add an HTTPS endpoint to a target group. Sensitive headers are redacted in responses.

Path parameters
id
string·REQUIRED
The target group id.
Request body
target_url
string·REQUIRED
HTTPS endpoint to fan out to. Plain HTTP and private/internal addresses are rejected.
method
string·OPTIONAL
HTTP verb used when delivering to this member.
default·"POST"
"GET""POST""PUT""PATCH""DELETE"
headers
object·OPTIONAL
Custom headers for this member. Sensitive values (e.g. Authorization) are redacted in API responses.
Responses
201Created
Member added. Body is { id, target_url, method, headers, created_at } with sensitive headers redacted.
400Bad Request
target_url missing, unparseable, non-HTTPS, or an unsupported method.
error.codetarget_url is requiredInvalid target_urlOnly HTTPS target_urls are allowedmethod must be one of: GET, POST, PUT, PATCH, DELETE
401Unauthorized
Missing or invalid API key.
404Not Found
No target group with that id belongs to you.
422Unprocessable
The SSRF guard blocked target_url — it resolves to a private, loopback, or metadata address.
Request
POST /v1/target-groups/{id}/members
curl -X POST "https://cronpilot.dev/v1/target-groups/:id/members" \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d {
       "target_url": "https://eu.example.com/hook",
       "method": "POST"
     }'
Response201Created
application/json
{
"id": "a1b2c3d4-5678-4ef0-b123-456789abcdef",
"target_url": "https://eu.example.com/hook",
"method": "POST",
"headers": {
"authorization": "***redacted***"
},
"created_at": "2026-05-29T15:41:00.000Z"
}

Domain verification

To prevent abuse, target domains must be verified before you can send unlimited messages to them. Unverified domains are limited to 10 messages per day per domain; verified domains use your full tier quota. Verifying a registrable domain covers all of its subdomains.

POST/v1/domains
Bearertier daily quota

Begin verifying ownership of a domain. Returns both a .well-known file challenge and a DNS TXT record — satisfy either and call /v1/domains/verify.

Request body
domain
string·REQUIRED
The domain to verify (e.g. api.example.com). A URL is accepted and normalised to its hostname.
Responses
201Created
Verification initiated. Body returns both a well_known file challenge and a dns_txt record. (An already-verified domain returns 200 with status: verified.)
400Bad Request
domain missing or not a parseable hostname.
error.codedomain is requiredInvalid domain
401Unauthorized
Missing or invalid API key.
Request
POST /v1/domains
curl -X POST "https://cronpilot.dev/v1/domains" \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d {
       "domain": "api.example.com"
     }'
Response201Created
application/json
{
"id": "e7f8a9b0-3456-4cde-9f01-23456789abcd",
"domain": "api.example.com",
"status": "pending",
"verification": {
"well_known": {2 fields},
"dns_txt": {3 fields}
}
}

HMAC signing

When a signing secret is configured (per schedule or queue), every outbound request includes three verification headers. Compute the expected signature over {timestamp}.{rawBody}, compare in constant time, and reject timestamps older than five minutes.

Webhook · signed
HMAC-SHA2565-min replay window
eventschedule fire → your endpoint
headerX-CronPilot-Signature: v1=<hex>
verify"v1=" + hmac_sha256(secret, `${t}.${rawBody}`)
Payload
{
  "method":  "POST",
  "url":     "https://api.example.com/cleanup",
  "headers": { "X-CronPilot-Message-Id": "msg_8RXc1Pm9k" },
  "body":    "{ \"run\": \"cleanup\" }"
}
Verify on your serverconstant-time compare · also sent: X-CronPilot-Timestamp, X-CronPilot-Message-Id
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
  if (Math.abs(Date.now()  1000  Number(ts))  300) return false;

  const expected  "v1="  createHmac("sha256", secret)
    .update(ts  "."  req.body)  // raw, unparsed body
    .digest("hex");
  return sig  expected;
}

Errors

Every error returns JSON with an error field; quota and domain-limit errors add structured fields (window, limit, used).

StatusMeaning
400Invalid body, missing required field, bad cron expression, or non-HTTPS URL
401Missing, placeholder, invalid, or revoked API key
404Schedule, queue, target group, or resource not found
409Duplicate deduplication_id
422SSRF blocked — target resolves to a private/internal address
429Daily/monthly quota reached, or the 10/day unverified-domain limit

Rate limits & tiers

TierMessages/dayMin intervalMax retries
Free ($0)50015 min1
Starter ($12/mo)3,4001 min3
Pro ($39/mo)34,0001 min5
Business ($149/mo)340,0001 min10

tip See the live numbers and upgrade on the pricing page.