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.
Base URL
Section titled “Base URL”All requests go to:
https://api.schedstack.com/v1There is no separate test host. Test and live are selected by your API key, not by the URL. See Modes.
Authentication
Section titled “Authentication”Send your API key as a bearer token on every request:
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_…" }}Versioning
Section titled “Versioning”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);/v1keeps working.
Pin to /v1 and ignore unknown JSON keys. That is the entire versioning story you need.
Request IDs
Section titled “Request IDs”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.
Pagination
Section titled “Pagination”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:
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')donecursor = Nonewhile True: params = {"limit": 50} if cursor: params["cursor"] = cursor page = httpx.get( "https://api.schedstack.com/v1/deliveries", params=params, headers={"Authorization": "Bearer sk_test_…"}, ).json() for obj in page["data"]: process(obj) if not page["has_more"]: break cursor = page["next_cursor"]let cursor = null;do { const url = new URL("https://api.schedstack.com/v1/deliveries"); url.searchParams.set("limit", "50"); if (cursor) url.searchParams.set("cursor", cursor);
const page = await fetch(url, { headers: { Authorization: "Bearer sk_test_…" }, }).then((r) => r.json());
for (const obj of page.data) process(obj); cursor = page.has_more ? page.next_cursor : null;} while (cursor);Errors
Section titled “Errors”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. |
Idempotency
Section titled “Idempotency”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.
Limits
Section titled “Limits”| 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.
See also
Section titled “See also”- API reference: interactive, per-endpoint request and response schemas.
- Idempotency: the two
Idempotency-Keymechanisms in full. - Retries and dead-letter: how delivery failures are retried and surfaced, never silently lost.