API Reference
Full reference for the Verne Clockwork HTTP API. All endpoints are served from https://api.vernesoft.com.
Authentication
Clockwork can be reached two ways. Both operate on the same jobs for your tenant — pick whichever fits the caller.
1. Session cookie — Console
Browser/console calls use your Kratos session cookie against the /dashboard/clockwork/* routes. The ory_kratos_session cookie is set automatically when you log in to the Verne Console.
Cookie: ory_kratos_session=<your_session_token>
Requests without a valid session return 401 Unauthorized. Admin accounts cannot access tenant dashboard routes and receive 403 Forbidden.
2. API key — Programmatic
Server-to-server calls use a Bearer API key against the /v1/clockwork/* routes. Clockwork keys follow the format:
vrn_clockwork_<secret>
Example header:
Authorization: Bearer vrn_clockwork_9f8a7...
Create and rotate keys on the Dashboard → Keys page (choose the Clockwork engine). The key is scoped to a single tenant — the tenant is derived from the key itself, so a caller can only ever read or modify its own jobs. A key from another engine (e.g. vrn_relay_*) is rejected with 403 Forbidden.
Requests with a missing or revoked key return 401 Unauthorized.
Route mapping
The two access modes are 1:1 — identical request bodies, responses, and status codes. Only the base path and the auth header differ. The rest of this reference documents the /dashboard/clockwork/* (session) paths; to call the API-key equivalent, swap the prefix:
| Session (cookie) | API key (Bearer) |
|---|---|
GET /dashboard/clockwork/jobs | GET /v1/clockwork/jobs |
POST /dashboard/clockwork/jobs | POST /v1/clockwork/jobs |
PATCH /dashboard/clockwork/jobs/{job_id} | PATCH /v1/clockwork/jobs/{job_id} |
DELETE /dashboard/clockwork/jobs/{job_id} | DELETE /v1/clockwork/jobs/{job_id} |
GET /dashboard/clockwork/jobs/{job_id}/executions | GET /v1/clockwork/jobs/{job_id}/executions |
GET /dashboard/clockwork/delayed | GET /v1/clockwork/delayed |
POST /dashboard/clockwork/delayed | POST /v1/clockwork/delayed |
DELETE /dashboard/clockwork/delayed/{job_id} | DELETE /v1/clockwork/delayed/{job_id} |
GET /dashboard/clockwork/delayed/{job_id}/executions | GET /v1/clockwork/delayed/{job_id}/executions |
Example — list jobs with an API key:
curl https://api.vernesoft.com/v1/clockwork/jobs \
-H "Authorization: Bearer vrn_clockwork_9f8a7..."
Example — create a cron job with an API key:
curl -X POST https://api.vernesoft.com/v1/clockwork/jobs \
-H "Authorization: Bearer vrn_clockwork_9f8a7..." \
-H "Content-Type: application/json" \
-d '{
"name": "Nightly report",
"schedule": "0 2 * * *",
"url": "https://yourapp.com/internal/reports/generate",
"method": "POST"
}'
Cron Jobs
Recurring jobs defined by a 5-field cron expression.
┌───── minute (0–59)
│ ┌───── hour (0–23)
│ │ ┌───── day of month (1–31)
│ │ │ ┌───── month (1–12)
│ │ │ │ ┌───── day of week (0–7, Sunday = 0 or 7)
* * * * *
Restrict day-of-month or day-of-week, not both.
A schedule that restricts both is rejected with a 400. POSIX crontabs combine
those two fields with OR — 0 0 13 * 5 means "the 13th of every month, and
every Friday" — while the scheduler underneath Clockwork combines them with AND,
which would make the same expression mean "Friday the 13th" and fire it once or
twice a year instead of about sixty-four times. Rather than pick one silently, we
refuse the expression and say so.
Put * in one of the two fields. Schedules where at most one is restricted —
the overwhelming majority — are unaffected and mean exactly what they mean
everywhere else.
Sunday is both 0 and 7, as in POSIX. A range ending in 7 runs through
Sunday, so 1-7 is every day and 6-7 is the weekend.
A schedule may be at most 255 characters. A longer one is rejected with a 400 rather than accepted and then failing to save.
List Cron Jobs
GET /dashboard/clockwork/jobs
Returns all cron jobs for the authenticated tenant, ordered by creation date (newest first).
Example
curl https://api.vernesoft.com/dashboard/clockwork/jobs \
-H "Cookie: ory_kratos_session=<session>"
Response (200 OK)
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"tenant_id": "tenant_001",
"name": "Health ping",
"schedule": "*/5 * * * *",
"url": "https://yourapp.com/health",
"method": "GET",
"headers": {},
"body": null,
"is_active": true,
"last_run_at": "2026-04-09T12:00:00Z",
"next_run_at": "2026-04-09T12:05:00Z",
"created_at": "2026-04-01T10:00:00Z",
"updated_at": "2026-04-09T12:00:00Z"
}
]
Create Cron Job
POST /dashboard/clockwork/jobs
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Human-readable name for the job. |
schedule | string | Yes | 5-field cron expression (e.g. 0 * * * *). |
url | string | Yes | Target URL to call on each execution. Must be http or https and point at a publicly reachable address. |
method | string | No | HTTP method. Default: POST. Allowed: GET, POST, PUT, PATCH, DELETE. |
headers | object | No | Key-value map of custom request headers. Names starting with X-Clockwork- are reserved and ignored. |
body | string | No | Raw request body (sent as-is). |
Example
curl -X POST https://api.vernesoft.com/dashboard/clockwork/jobs \
-H "Cookie: ory_kratos_session=<session>" \
-H "Content-Type: application/json" \
-d '{
"name": "Nightly report",
"schedule": "0 2 * * *",
"url": "https://yourapp.com/internal/reports/generate",
"method": "POST",
"headers": { "X-Internal-Token": "secret" },
"body": "{\"type\": \"daily\"}"
}'
Response (201 Created)
Returns the created CronJob object.
| Status Code | Meaning |
|---|---|
201 Created | Job created successfully. |
400 Bad Request | Invalid cron expression or unsupported HTTP method. |
401 Unauthorized | Missing or invalid session. |
Update Cron Job
PATCH /dashboard/clockwork/jobs/{job_id}
All fields are optional — only provided fields are updated.
Request Body
| Field | Type | Description |
|---|---|---|
name | string | New job name. |
schedule | string | New 5-field cron expression. |
url | string | New target URL. Same rules as on creation — see Target URLs. |
method | string | New HTTP method. |
headers | object | New headers map (replaces existing). |
body | string | New body (replaces existing). |
is_active | boolean | Set to false to pause the job without deleting it. |
Example
curl -X PATCH https://api.vernesoft.com/dashboard/clockwork/jobs/550e8400-e29b-41d4-a716-446655440000 \
-H "Cookie: ory_kratos_session=<session>" \
-H "Content-Type: application/json" \
-d '{ "is_active": false }'
Response (200 OK)
Returns the updated CronJob object.
| Status Code | Meaning |
|---|---|
200 OK | Job updated. |
400 Bad Request | Invalid cron expression or HTTP method. |
404 Not Found | Job not found (or belongs to another tenant). |
Delete Cron Job
DELETE /dashboard/clockwork/jobs/{job_id}
Permanently deletes the cron job and all its execution history.
curl -X DELETE https://api.vernesoft.com/dashboard/clockwork/jobs/550e8400-e29b-41d4-a716-446655440000 \
-H "Cookie: ory_kratos_session=<session>"
| Status Code | Meaning |
|---|---|
204 No Content | Job deleted. |
404 Not Found | Job not found. |
List Cron Job Executions
GET /dashboard/clockwork/jobs/{job_id}/executions
Returns the last 100 executions for a cron job, ordered newest first. A run that was retried contributes one row per attempt.
curl https://api.vernesoft.com/dashboard/clockwork/jobs/550e8400-e29b-41d4-a716-446655440000/executions \
-H "Cookie: ory_kratos_session=<session>"
Response (200 OK)
[
{
"id": "exec_002",
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "success",
"started_at": "2026-04-09T12:01:00Z",
"completed_at": "2026-04-09T12:01:00.290Z",
"duration_ms": 290,
"response_status": 200,
"response_body": "{\"ok\": true}",
"error_message": null,
"attempt": 2
},
{
"id": "exec_001",
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "failed",
"started_at": "2026-04-09T12:00:00Z",
"completed_at": "2026-04-09T12:00:00.312Z",
"duration_ms": 312,
"response_status": 503,
"response_body": "upstream unavailable",
"error_message": null,
"attempt": 1
}
]
The example above is one run: the first attempt got a 503 and the retry a
minute later succeeded.
| Field | Meaning |
|---|---|
status | success — 2xx. failed — non-2xx or network error. running — in progress. |
attempt | Which try produced this row, counting from 1. See Retries. |
response_status | HTTP status the target returned, or null if the request never completed. |
error_message | Why the request never completed; null when the target replied. |
Delayed Jobs
One-shot jobs that execute once at a specific future timestamp.
List Delayed Jobs
GET /dashboard/clockwork/delayed
Returns all delayed jobs for the authenticated tenant, ordered by run_at ascending.
curl https://api.vernesoft.com/dashboard/clockwork/delayed \
-H "Cookie: ory_kratos_session=<session>"
Response (200 OK)
[
{
"id": "660f9511-f3ac-52e5-b827-557766551111",
"tenant_id": "tenant_001",
"name": "Send welcome email",
"run_at": "2026-04-10T09:00:00Z",
"url": "https://yourapp.com/internal/send-welcome",
"method": "POST",
"headers": {},
"body": "{\"user_id\": \"usr_001\"}",
"status": "pending",
"created_at": "2026-04-09T10:00:00Z",
"updated_at": "2026-04-09T10:00:00Z"
}
]
status value | Meaning |
|---|---|
pending | Waiting for run_at. A job awaiting a retry returns to this state with a later run_at. |
running | Currently being executed. |
success | The target returned 2xx. |
failed | The target failed and no attempts remain. |
cancelled | Cancelled before execution. |
Create Delayed Job
POST /dashboard/clockwork/delayed
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Human-readable name for the job. |
run_at | string | Yes | ISO 8601 UTC timestamp. Must be in the future. |
url | string | Yes | Target URL to call at execution time. |
method | string | No | HTTP method. Default: POST. Allowed: GET, POST, PUT, PATCH, DELETE. |
headers | object | No | Key-value map of custom request headers. Names starting with X-Clockwork- are reserved and ignored. |
body | string | No | Raw request body (sent as-is). |
Example
curl -X POST https://api.vernesoft.com/dashboard/clockwork/delayed \
-H "Cookie: ory_kratos_session=<session>" \
-H "Content-Type: application/json" \
-d '{
"name": "Send invoice reminder",
"run_at": "2026-04-15T08:00:00Z",
"url": "https://yourapp.com/internal/invoices/remind",
"method": "POST",
"body": "{\"invoice_id\": \"inv_042\"}"
}'
Response (201 Created)
Returns the created DelayedJob object.
| Status Code | Meaning |
|---|---|
201 Created | Job scheduled. |
400 Bad Request | run_at is in the past, or unsupported HTTP method. |
401 Unauthorized | Missing or invalid session. |
Cancel Delayed Job
DELETE /dashboard/clockwork/delayed/{job_id}
Cancels a pending delayed job. Jobs that are already running or completed cannot be cancelled.
curl -X DELETE https://api.vernesoft.com/dashboard/clockwork/delayed/660f9511-f3ac-52e5-b827-557766551111 \
-H "Cookie: ory_kratos_session=<session>"
| Status Code | Meaning |
|---|---|
204 No Content | Job cancelled. |
404 Not Found | Job not found. |
409 Conflict | Job is not in pending status and cannot be cancelled. |
List Delayed Job Executions
GET /dashboard/clockwork/delayed/{job_id}/executions
Returns the execution records for a delayed job, ordered newest first — one per attempt, so a job that was retried has more than one.
curl https://api.vernesoft.com/dashboard/clockwork/delayed/660f9511-f3ac-52e5-b827-557766551111/executions \
-H "Cookie: ory_kratos_session=<session>"
Response (200 OK)
[
{
"id": "exec_002",
"job_id": "660f9511-f3ac-52e5-b827-557766551111",
"status": "success",
"started_at": "2026-04-10T09:00:01Z",
"completed_at": "2026-04-10T09:00:01.874Z",
"duration_ms": 874,
"response_status": 200,
"response_body": "{\"sent\": true}",
"error_message": null,
"attempt": 1
}
]
Target URLs
A job's url has to be somewhere Clockwork can reach from the public internet.
Requests that are not are refused with 400, and the error names the rule:
| Refused | Why |
|---|---|
ftp://…, file://… | only http and https are fetched |
https://user:pass@example.com/… | credentials in a URL are stored in plaintext and shown back in the job list — send them in a header instead |
http://10.0.0.5/, http://192.168.1.1/, http://127.0.0.1/ | private and loopback addresses |
http://169.254.169.254/… | link-local, which is where cloud metadata services live |
http://internal-service/… | a single-label hostname, which cannot be a public name |
The same rule is applied again when the job runs, and the hostname is resolved
first — so a name that resolves to a private address is refused at execution
time even if it looked fine when the job was created. Those attempts appear in
the execution history as failed, with the reason in error_message and no
response recorded.
Retries
A failed execution is retried automatically. Retry state is stored alongside the job rather than held in memory, so a pending retry survives a worker restart or deployment.
Each run gets up to 3 attempts: the first, then two retries — one minute and five minutes later. Clockwork's scheduler wakes once a minute, so a retry fires on the first tick at or after its due time.
What is retried
| Outcome | Retried | Why |
|---|---|---|
| Network failure — connection refused, timeout, DNS, TLS | Yes | The request never arrived, so nothing indicates it was wrong. |
5xx | Yes | A server-side error, commonly transient. |
429 Too Many Requests | Yes | An explicit "slow down". |
Any other 4xx | No | The request itself is rejected; an identical retry gets the same answer. |
2xx | — | Succeeded. |
Make your endpoint idempotent
A retry repeats the identical request. Because
5xxresponses are retried, an endpoint that fails after performing part of its work will perform that work again on the next attempt — Clockwork cannot distinguish a failure before a side effect from one after it.If your job is not naturally idempotent, return a
4xxfor permanent errors so Clockwork stops, and guard the side effect on your side using the headers below.
Every request Clockwork sends carries two headers:
| Header | Value |
|---|---|
X-Clockwork-Execution-Id | UUID of this execution. Matches the id in the execution history, so a request can be traced back to what Clockwork recorded. |
X-Clockwork-Attempt | Which try this is, counting from 1. Anything above 1 is a retry. |
A practical guard: when X-Clockwork-Attempt is greater than 1, check whether
the work was already done before doing it again. X-Clockwork-Execution-Id
changes per attempt — it identifies this try, not the run — so use it for
correlation and logging rather than as a deduplication key.
The X-Clockwork- prefix is reserved. Headers in a job definition that start
with it are ignored, so the target always receives exactly one value for each.
Cron jobs vs delayed jobs
- Cron jobs — a retry is an extra run inserted between regular runs. It never
shifts the schedule:
next_run_atis untouched, and once the attempts are spent the job simply waits for its next scheduled time. - Delayed jobs — the job returns to
pendingwith a laterrun_at, because there is no schedule to fall back on. It is markedfailedonly after the last attempt.
Seeing attempts
Every attempt is recorded as its own execution, numbered by attempt, so the
history shows what your endpoint returned each time rather than only the last
word. Both execution endpoints return them newest first.
Error Format
All errors follow a consistent structure:
{
"error": "Invalid cron schedule. Expected a 5-field POSIX expression with a future occurrence (e.g. \"0 * * * *\")"
}
A schedule is rejected unless it parses and has a future occurrence, so an
expression like 0 0 31 2 * — 31 February, which never comes — is refused at
creation rather than accepted and silently never run.
Include the request_id when contacting support for faster resolution.