Skip to content

Create a cron (POST /api/crons)

POST https://app.delay.run/api/crons creates a cron that Delay runs on your schedule. The response gives you the cron’s ID and its next run time. You need an API token from Settings > API token (see API overview and authentication).

The smallest working request, an hourly health check:

Terminal window
curl -X POST https://app.delay.run/api/crons \
-H "Authorization: Bearer $DELAY_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"name": "Health check", "url": "https://api.yourapp.com/health", "cron_expression": "0 * * * *"}'

This cron sends a GET request at the start of every hour, UTC, with a 30-second timeout and no alerts.

Request

Send these headers with every request:

HeaderValue
AuthorizationBearer followed by your API token
Content-Typeapplication/json
Acceptapplication/json. Without it, validation errors come back as a 302 redirect instead of a 422.

The cron is created in your current team, the one selected in the dashboard’s team switcher. See API overview and authentication.

Body fields

FieldTypeRequiredDefaultLimits and notes
namestringyesUp to 255 characters. Doesn’t have to be unique.
urlstringyesA valid URL, up to 2,048 characters. Must be publicly reachable.
cron_expressionstringyesFive fields or a shortcut like @daily, evaluated in UTC. Up to 255 characters. See Cron schedules.
methodstringnoGETGET, POST, PUT, PATCH or DELETE, uppercase
headersobjectnononeHeader names mapped to string values, sent with every run
payloadstringnononeSent as a JSON body (as query parameters for GET). A JSON object or array is sent as that JSON; any other text is wrapped as {"body": "..."}.
callback_urlstringnononeA valid URL, up to 2,048 characters. Delay POSTs a summary there after every run. See Callbacks.
timeout_secondsintegerno301 to 300
alert_thresholdintegernonone (alerts off)1, 2 or 3: alert after that many consecutive failed runs. See Set up alerts.
is_activebooleannotruefalse creates the cron paused

Fields that aren’t in this table are ignored, without an error. That includes success_assertions: the API can’t set assertions.

Sending a JSON body

payload is a string, not an object. To send a JSON body, encode it as a string first. In raw JSON that means escaping the inner quotes:

{
"payload": "{\"type\":\"full\"}"
}

Your endpoint then receives the body {"type":"full"}. In code, build the string with json_encode, JSON.stringify or json.dumps, as in the examples below. Sending an object instead gives a 422 with “The payload field must be a string.”

Examples

A nightly backup: a POST at 03:00 UTC with a header and a JSON body, a 2-minute timeout, and an alert after two failures in a row.

Terminal window
curl -X POST https://app.delay.run/api/crons \
-H "Authorization: Bearer $DELAY_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"name": "Nightly backup",
"url": "https://api.yourapp.com/backup",
"method": "POST",
"cron_expression": "0 3 * * *",
"headers": { "X-Api-Key": "your-endpoint-key" },
"payload": "{\"type\":\"full\"}",
"timeout_seconds": 120,
"alert_threshold": 2
}'

Each example reads the token from the DELAY_TOKEN environment variable. Set it first, for example export DELAY_TOKEN=your-token.

Response

A new cron returns 201 Created:

{
"id": "01a14f38-a170-70cb-bbb8-6b807a0ef65e",
"name": "Nightly backup",
"cron_expression": "0 3 * * *",
"next_run_at": "2026-10-19T03:00:00+00:00",
"is_active": true
}
FieldTypeDescription
idstringThe cron’s UUID
namestringThe name you sent
cron_expressionstringThe expression you sent
next_run_atstring or nullThe next run time, ISO 8601 in UTC. null when the cron was created paused (is_active: false).
is_activebooleanWhether the cron is running

The response escapes forward slashes, so */5 * * * * comes back as "*\/5 * * * *". Any JSON parser turns it back into */5 * * * *.

Errors

Checks happen in this order: the rate limit, then the body, then the token. A request with a bad body and a bad token gets 422, not 401.

401 Unauthorized

The token is missing or doesn’t match any user. The body is always JSON:

{ "error": "Missing authorization token" }
{ "error": "Invalid webhook token" }

“Invalid webhook token” means the API token is wrong or was regenerated. Copy the current one from Settings > API token.

422 Unprocessable Content

The body failed validation. message repeats the first error, and errors lists every problem by field:

{
"message": "A name for the cron job is required. (and 2 more errors)",
"errors": {
"name": ["A name for the cron job is required."],
"url": ["The target URL must be a valid URL."],
"cron_expression": ["The cron expression is not a valid cron expression."]
}
}

Messages you may see:

FieldMessage
name”A name for the cron job is required.”
url”A target URL is required.” or “The target URL must be a valid URL.”
method”The method must be one of: GET, POST, PUT, PATCH, DELETE.” (lowercase get is rejected too)
cron_expression”A cron expression is required.”, “The cron expression is not a valid cron expression.” or “The cron expression never produces a valid run time.”
payload”The payload field must be a string.”
timeout_seconds”Timeout must be at least 1 second.” or “Timeout cannot exceed 300 seconds (5 minutes).”
alert_threshold”The alert threshold must be 1, 2, or 3.”

You only get a 422 when the request has Accept: application/json. Without it, the same errors come back as a 302 redirect with an HTML body, which is easy to mistake for success.

429 Too Many Requests

The API allows 60 requests per minute per client IP address. The limit is shared by POST /api/crons and POST /api/jobs, so 60 jobs created in one minute also block cron creation from that address for the rest of the minute. It doesn’t depend on your token or team.

{ "message": "Too Many Attempts." }

The response includes Retry-After (seconds to wait), X-RateLimit-Limit and X-RateLimit-Remaining. Every response also carries the last two, so you can slow down before you hit the limit. When creating many crons in a loop, wait for Retry-After on a 429, or send at most one request per second.

What this endpoint can’t do

  • Set success assertions. Create the cron, then add them in the dashboard with Edit. See Success assertions.
  • Update, pause, delete, list or run a cron. There are no API endpoints for these. Use the dashboard: see Schedule a recurring request.
  • Prevent duplicates. Every successful call creates a new cron, even with the same name. If a request times out on your side, check the Cron jobs list before sending it again.
  • Use a timezone. Expressions are always UTC.
  • Check your usage. A team over its monthly limit still gets 201. The cron’s runs then fail with limit_exceeded. See How usage is counted.

Next steps