Skip to content

API conventions

These rules apply to every endpoint under /v1. Read this once and the per-endpoint reference becomes mechanical. For the full interactive request and response schemas, see the API reference.

All requests go to:

https://api.schedstack.com/v1

There is no separate test host. Test and live are selected by your API key, not by the URL. See Modes.

Send your API key as a bearer token on every request:

Terminal window
curl https://api.schedstack.com/v1/schedules \
-H "Authorization: Bearer sk_test_…"

Keys are prefixed by mode, sk_test_… or sk_live_…, and each key is scoped to a single project and mode. Every read and write is automatically confined to that (project, mode) pair; you cannot reach another project’s data, and a test key never sees live data or vice versa.

A missing, malformed, or revoked key returns 401 with type: authentication_error. A missing key uses code: missing_api_key (message Provide an API key via Authorization: Bearer <key>.); an invalid or revoked key uses code: invalid_api_key:

{
"error": {
"type": "authentication_error",
"code": "invalid_api_key",
"message": "The API key is invalid or has been revoked.",
"request_id": "req_…"
}
}

The major version lives in the path: /v1. That prefix is the contract. We ship backward-compatible additions continuously without a version bump, so write your client to tolerate new fields:

  • New response fields, new enum values, and new endpoints can appear at any time.
  • Existing fields keep their name, type, and meaning.
  • A breaking change, if one ever happens, ships under a new path prefix (/v2); /v1 keeps working.

Pin to /v1 and ignore unknown JSON keys. That is the entire versioning story you need.

Every response, success or error, carries a Sched-Request-Id header:

Sched-Request-Id: req_…

On an error, the same id is also echoed inside the body as error.request_id. Log it. It is the fastest way for you or for us to trace a single call end to end.

List endpoints (GET /v1/schedules, /v1/deliveries, /v1/endpoints, and the nested .../deliveries and .../attempts collections) return a cursor-paginated envelope:

{
"object": "list",
"data": [ { "...": "..." } ],
"has_more": true,
"next_cursor": "eyJ…"
}
Field Meaning
object Always "list" for a collection response.
data The page of objects, newest first.
has_more true if more pages remain after this one.
next_cursor Opaque cursor for the next page. null on the last page.

Control the page with two query parameters:

Parameter Default Notes
limit 20 Number of objects per page.
cursor none The next_cursor from the previous response.

Treat next_cursor as opaque. Do not parse or construct it. Walk pages until has_more is false:

Terminal window
cursor=""
while : ; do
resp=$(curl -s "https://api.schedstack.com/v1/deliveries?limit=50&cursor=$cursor" \
-H "Authorization: Bearer sk_test_…")
echo "$resp" | jq '.data[]'
[ "$(echo "$resp" | jq -r '.has_more')" = "true" ] || break
cursor=$(echo "$resp" | jq -r '.next_cursor')
done

Every error, at any status code, uses the same typed envelope:

{
"error": {
"type": "invalid_request_error",
"code": "url_blocked",
"message": "The destination resolves to a blocked address.",
"param": "endpoint",
"request_id": "req_…"
}
}
Field Always present Meaning
type yes Broad category. Branch on this. See the enum below.
code yes Stable machine-readable specifier (e.g. url_blocked, invalid_cron).
message yes Human-readable explanation. Display it; don’t match on it.
param when applicable The request field that caused the error.
request_id yes Same value as the Sched-Request-Id header.

type is one of:

type Typical status Meaning
invalid_request_error 400 / 422 Malformed or invalid input. The request as sent will never succeed unchanged.
authentication_error 401 Missing, malformed, or revoked API key.
idempotency_error 409 An Idempotency-Key conflict (see below).
not_found_error 404 No such object in this (project, mode).
api_error 5xx Something failed on our side. Safe to retry.

Send an Idempotency-Key header on any mutating call (such as POST /v1/schedules) to make a network retry safe: a replayed request returns the original result instead of creating a second schedule. Keys are stored for 24 hours, scoped to (project, mode), and matched against the method, path, and body.

This is one of two distinct mechanisms named Idempotency-Key in SchedStack (the other is the dedup key on deliveries to your endpoint). The full rules (fingerprinting, conflict codes, and how to dedup on the receiving side) are in Idempotency.

Limit Value Applies to
Request body ≤ 1 MB The JSON you POST/PATCH to the API.
Delivery body ≤ 256 KB (262144 bytes) The body SchedStack sends to your endpoint.
Minimum delay ~1 second The soonest a schedule can fire; a smaller delay is rejected.

The custom headers you attach to a delivery are capped at 16 KB total; a larger set is rejected with 413 payload_too_large. (This is separate from, and in addition to, the 1 MB request-body cap above.)

Exceed a limit and you get an invalid_request_error naming the offending param. The minimum delay reflects the dispatch model: SchedStack publishes a 100–300 ms p99 dispatch-initiation SLO, not sub-second precision. See Modes for what “dispatch-initiation” measures.