Schedule a job (POST /api/jobs)
POST https://app.delay.run/api/jobs schedules one HTTP request to run now or up to 24 hours later. Delay sends it with the timeout and retries of the named queue, and creates the queue if it doesn’t exist.
Minimal request
curl -X POST https://app.delay.run/api/jobs \ -H "Authorization: Bearer $DELAY_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{"queue": "emails", "url": "https://api.yourapp.com/send-welcome"}'This sends POST https://api.yourapp.com/send-welcome right away, with the body []. Your token is under Settings > API token (authentication).
Request fields
| Field | Type | Required | Default | Limits and notes |
|---|---|---|---|---|
queue | string | Yes | - | Max 255 characters. A queue in your current team. Created with default settings if it doesn’t exist. |
url | string | Yes | - | A valid URL, max 2,048 characters. Private and reserved addresses fail when the job runs, not here. |
method | string | No | POST | GET, POST, PUT, PATCH or DELETE, in uppercase. |
headers | object | No | None | Header names with string values, sent as given. |
payload | object or array | No | None | Sent as JSON, or as the query string for GET. A string or number is rejected. |
callback_url | string | No | None | A valid URL, max 2,048 characters. Receives a POST when the job succeeds or is dead-lettered (callbacks). |
delay | integer | No | 0 | Seconds before the first attempt, from 0 to 86,400 (24 hours). |
The timeout, retries, concurrency limit and assertions come from the queue. You can’t set them per job, and this endpoint can’t change them (queue settings).
How the payload is sent
| Method | What Delay sends |
|---|---|
GET | No body. Payload keys are added to the URL’s query string. A payload key replaces a URL parameter with the same name. |
POST, PUT, PATCH | A JSON body. With no payload (or an empty one) the body is []. |
DELETE | A JSON body when there’s a payload, otherwise no body. |
A request with a body carries Content-Type: application/json, unless your headers set a different one. GET, and DELETE without a payload, have no body and no Content-Type. If your team has a signing secret, Delay also adds X-Delay-Signature and X-Delay-Timestamp (verify requests).
Examples
Send a welcome email 15 minutes after signup, with a custom header and a callback:
curl -X POST https://app.delay.run/api/jobs \ -H "Authorization: Bearer $DELAY_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "queue": "emails", "url": "https://api.yourapp.com/send-welcome", "method": "POST", "headers": {"X-Source": "delay"}, "payload": {"user_id": 42, "template": "welcome"}, "callback_url": "https://api.yourapp.com/delay-callback?user=42", "delay": 900 }'With Guzzle (composer require guzzlehttp/guzzle):
<?php
require __DIR__ . '/vendor/autoload.php';
$client = new GuzzleHttp\Client();
// Guzzle throws an exception on 4xx and 5xx responses.$response = $client->post('https://app.delay.run/api/jobs', [ 'headers' => [ 'Authorization' => 'Bearer ' . getenv('DELAY_TOKEN'), 'Accept' => 'application/json', ], // 'json' encodes the body and sets Content-Type: application/json. 'json' => [ 'queue' => 'emails', 'url' => 'https://api.yourapp.com/send-welcome', 'method' => 'POST', 'headers' => ['X-Source' => 'delay'], 'payload' => ['user_id' => 42, 'template' => 'welcome'], 'callback_url' => 'https://api.yourapp.com/delay-callback?user=42', 'delay' => 900, ],]);
$job = json_decode((string) $response->getBody(), true);
echo "Job {$job['job_id']} runs at {$job['scheduled_at']}\n";In Laravel, the HTTP client does the same. This assumes you’ve added 'delay' => ['token' => env('DELAY_TOKEN')] to config/services.php:
use Illuminate\Support\Facades\Http;
$job = Http::withToken(config('services.delay.token')) ->acceptJson() ->post('https://app.delay.run/api/jobs', [ 'queue' => 'emails', 'url' => 'https://api.yourapp.com/send-welcome', 'payload' => ['user_id' => $user->id, 'template' => 'welcome'], 'callback_url' => "https://api.yourapp.com/delay-callback?user={$user->id}", 'delay' => 900, ]) ->throw() ->json();Node.js 18 or later (built-in fetch). Save it as an ES module, for example schedule-job.mjs:
const response = await fetch('https://app.delay.run/api/jobs', { method: 'POST', headers: { Authorization: `Bearer ${process.env.DELAY_TOKEN}`, 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify({ queue: 'emails', url: 'https://api.yourapp.com/send-welcome', method: 'POST', headers: { 'X-Source': 'delay' }, payload: { user_id: 42, template: 'welcome' }, callback_url: 'https://api.yourapp.com/delay-callback?user=42', delay: 900, }),});
const body = await response.json();
if (!response.ok) { throw new Error(`Delay returned ${response.status}: ${JSON.stringify(body)}`);}
console.log(`Job ${body.job_id} runs at ${body.scheduled_at}`);With requests (pip install requests):
import os
import requests
response = requests.post( "https://app.delay.run/api/jobs", headers={ "Authorization": f"Bearer {os.environ['DELAY_TOKEN']}", "Accept": "application/json", }, # json= encodes the body and sets Content-Type: application/json. json={ "queue": "emails", "url": "https://api.yourapp.com/send-welcome", "method": "POST", "headers": {"X-Source": "delay"}, "payload": {"user_id": 42, "template": "welcome"}, "callback_url": "https://api.yourapp.com/delay-callback?user=42", "delay": 900, }, timeout=10,)response.raise_for_status()
job = response.json()print(f"Job {job['job_id']} runs at {job['scheduled_at']}")Response
201 Created
{ "success": true, "queue_id": "019a0c4e-1f3b-7c55-8a1e-6d2f9b0c4a71", "job_id": "019a0c4e-7b21-7d3a-9f4e-2c1b8a6d5e40", "scheduled_at": "2026-10-18T14:15:00+00:00", "message": "Scheduled successfully"}| Field | Meaning |
|---|---|
success | Always true. |
queue_id | Id of the queue the job was added to. |
job_id | Id of attempt 1. Each retry gets a new id (why). |
scheduled_at | The earliest time the job runs: now plus delay, in ISO 8601 (UTC). |
message | Always Scheduled successfully. |
A 201 doesn’t mean your team has requests left this month. Over the limit, the job is still accepted, then fails when it runs, with no retries and no callback (usage).
Errors
401: missing or invalid token
Always JSON, whatever the Accept header:
{"error": "Missing authorization token"}{"error": "Invalid webhook token"}The first means there was no Authorization: Bearer header. The second means the token didn’t match, for example because it was regenerated. “Webhook token” here means your API token.
422: validation error
{ "message": "A queue name is required. (and 1 more error)", "errors": { "queue": ["A queue name is required."], "delay": ["The delay cannot exceed 86400 seconds (24 hours)."] }}message is the first error plus a count of the rest. errors lists the messages for each field.
- Validation runs before the token check, so a bad body gets a 422 even when the token is missing or wrong.
- You only get a 422 when you send
Accept: application/json. Without it, a validation error comes back as a302redirect with no details.Content-Type: application/jsonalone isn’t enough.
429: rate limit
The limit is 60 requests per minute per client IP address. POST /api/jobs and POST /api/crons share it, and it isn’t per token or per team, so everything you run behind one IP address shares one budget. See API overview and authentication.
A 429 response has the headers Retry-After (seconds to wait), X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. With Accept: application/json the body is:
{"message": "Too Many Attempts."}Successful responses carry X-RateLimit-Limit and X-RateLimit-Remaining too, so you can slow down before you hit the limit. To schedule many jobs, send them one at a time and, after a 429, wait Retry-After seconds before you continue.
What this endpoint can’t do
- Read, list, update or cancel jobs. No other job endpoints exist. To cancel a job, delete it while it’s pending (how).
- Run at an absolute time. Work out
delayyourself, up to 24 hours ahead. - Set a timeout, retries or assertions per job, or create and configure queues. Those are queue settings in the dashboard.
- Create several jobs in one request.
- Deduplicate. Every call creates a new job, so if you resend a call that timed out on your side, the job may be scheduled twice.
- Return the job’s result. Use
callback_url, or check Jobs in the dashboard.
Next steps
- API overview and authentication: token, headers, rate limit and error shapes.
- Jobs: statuses, delays and how retries work.
- Completion callbacks: what Delay posts to
callback_url. - Queues: the settings every job inherits.