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.

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/jobsGET /v1/clockwork/jobs
POST /dashboard/clockwork/jobsPOST /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}/executionsGET /v1/clockwork/jobs/{job_id}/executions
GET /dashboard/clockwork/delayedGET /v1/clockwork/delayed
POST /dashboard/clockwork/delayedPOST /v1/clockwork/delayed
DELETE /dashboard/clockwork/delayed/{job_id}DELETE /v1/clockwork/delayed/{job_id}
GET /dashboard/clockwork/delayed/{job_id}/executionsGET /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

FieldTypeRequiredDescription
namestringYesHuman-readable name for the job.
schedulestringYes5-field cron expression (e.g. 0 * * * *).
urlstringYesTarget URL to call on each execution. Must be http or https and point at a publicly reachable address.
methodstringNoHTTP method. Default: POST. Allowed: GET, POST, PUT, PATCH, DELETE.
headersobjectNoKey-value map of custom request headers. Names starting with X-Clockwork- are reserved and ignored.
bodystringNoRaw 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 CodeMeaning
201 CreatedJob created successfully.
400 Bad RequestInvalid cron expression or unsupported HTTP method.
401 UnauthorizedMissing or invalid session.

Update Cron Job

PATCH /dashboard/clockwork/jobs/{job_id}

All fields are optional — only provided fields are updated.

Request Body

FieldTypeDescription
namestringNew job name.
schedulestringNew 5-field cron expression.
urlstringNew target URL. Same rules as on creation — see Target URLs.
methodstringNew HTTP method.
headersobjectNew headers map (replaces existing).
bodystringNew body (replaces existing).
is_activebooleanSet 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 CodeMeaning
200 OKJob updated.
400 Bad RequestInvalid cron expression or HTTP method.
404 Not FoundJob 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 CodeMeaning
204 No ContentJob deleted.
404 Not FoundJob 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.

FieldMeaning
statussuccess — 2xx. failed — non-2xx or network error. running — in progress.
attemptWhich try produced this row, counting from 1. See Retries.
response_statusHTTP status the target returned, or null if the request never completed.
error_messageWhy 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 valueMeaning
pendingWaiting for run_at. A job awaiting a retry returns to this state with a later run_at.
runningCurrently being executed.
successThe target returned 2xx.
failedThe target failed and no attempts remain.
cancelledCancelled before execution.

Create Delayed Job

POST /dashboard/clockwork/delayed

Request Body

FieldTypeRequiredDescription
namestringYesHuman-readable name for the job.
run_atstringYesISO 8601 UTC timestamp. Must be in the future.
urlstringYesTarget URL to call at execution time.
methodstringNoHTTP method. Default: POST. Allowed: GET, POST, PUT, PATCH, DELETE.
headersobjectNoKey-value map of custom request headers. Names starting with X-Clockwork- are reserved and ignored.
bodystringNoRaw 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 CodeMeaning
201 CreatedJob scheduled.
400 Bad Requestrun_at is in the past, or unsupported HTTP method.
401 UnauthorizedMissing 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 CodeMeaning
204 No ContentJob cancelled.
404 Not FoundJob not found.
409 ConflictJob 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:

RefusedWhy
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

OutcomeRetriedWhy
Network failure — connection refused, timeout, DNS, TLSYesThe request never arrived, so nothing indicates it was wrong.
5xxYesA server-side error, commonly transient.
429 Too Many RequestsYesAn explicit "slow down".
Any other 4xxNoThe request itself is rejected; an identical retry gets the same answer.
2xxSucceeded.

Make your endpoint idempotent

A retry repeats the identical request. Because 5xx responses 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 4xx for permanent errors so Clockwork stops, and guard the side effect on your side using the headers below.

Every request Clockwork sends carries two headers:

HeaderValue
X-Clockwork-Execution-IdUUID of this execution. Matches the id in the execution history, so a request can be traced back to what Clockwork recorded.
X-Clockwork-AttemptWhich 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_at is untouched, and once the attempts are spent the job simply waits for its next scheduled time.
  • Delayed jobs — the job returns to pending with a later run_at, because there is no schedule to fall back on. It is marked failed only 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.