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 |
— | 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. |
rate_limit_error |
429 |
Too many requests — back off and retry. |
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 headers you attach to a delivery have no separate cap — they count toward the 1 MB
request body 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.