Core concepts
SchedStack’s delivery hierarchy is three core objects. Learn them once and the
rest of the API reads itself. (A fourth object, the optional reusable
endpoint profile, is a saved destination a
schedule can reference by endpoint_id instead of an inline endpoint — not
part of the delivery flow below.)
-
A schedule is your instruction: this destination, at this time (or on this recurrence). You create one with
POST /v1/schedules. -
A schedule produces deliveries — one per occurrence. A one-shot schedule produces one delivery; a recurring schedule produces a new delivery for each fire of its
cron. -
Each delivery makes one or more attempts — individual outbound HTTP requests to your endpoint. The first attempt that gets a
2xxends the delivery; failures are retried under the delivery’sretry_policy.
schedule ──┬─► delivery (occurrence) ──┬─► attempt 1 (HTTP POST → your endpoint) │ ├─► attempt 2 (retry) │ └─► attempt 3 (succeeds → delivery done) └─► delivery (next occurrence, recurring only) …The guarantee: no silent loss
Section titled “The guarantee: no silent loss”Once SchedStack accepts a delivery (your POST returned 2xx), that delivery is
durable. It will not vanish, and it will not stall unobserved. It ends in exactly one
terminal state, and that outcome is recorded and readable on the delivery:
succeeded— an attempt got a2xxfrom your endpoint.dead_letter— retries were exhausted without a2xx. The delivery is parked, not dropped; you can inspect it and replay it.expired— the delivery’sttlelapsed before it could land.canceled— you canceled the schedule (or the delivery) before it completed.
There is no fifth outcome and no “lost” outcome. If you accepted it, you can always find out what happened to it.
At-least-once, made safe
Section titled “At-least-once, made safe”SchedStack is at-least-once, not exactly-once. A delivery can legitimately hit
your endpoint more than once — a network blip after your 2xx, a lease handoff, a
replay. That is the honest tradeoff of durable
delivery, and we hand you the two tools to make it safe:
- Signature — when your project (or the endpoint profile) has at least one active
signing secret, every outbound request carries a
Sched-SignatureHMAC so your receiver can prove the request came from SchedStack before acting on it. Without an active secret the header is omitted and the request is sent unsigned, so configure a secret before you rely on verification. This is the signature-verification guide — read it before you ship a receiver. - Idempotency — every attempt for a given delivery carries the same stable
outbound
Idempotency-Key— the schedule’sidempotency_keyif you set one, otherwise the delivery’s ID. Dedup on it and a redelivered occurrence is a no-op on your side.
So the contract is split honestly: SchedStack guarantees at-least-once with no silent loss; you guarantee effectively-once by verifying the signature and deduping on the key. Neither side can do the other’s half.
Schedules
Section titled “Schedules”A schedule is created live or in test mode (the API key prefix — sk_live_… vs
sk_test_… — decides; the two never mix). It is either a one-shot (delay,
fire_at, or local_fire_at + timezone) or recurring (cron, with DST-correct
local-time recurrence — see recurring schedules).
curl -X POST https://api.schedstack.com/v1/schedules \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{"endpoint":"https://acme.dev/hook","delay":"24h"}'curl -X POST https://api.schedstack.com/v1/schedules \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "endpoint": "https://acme.dev/hook", "local_fire_at": "2026-07-01T09:00:00", "timezone": "America/New_York" }'curl -X POST https://api.schedstack.com/v1/schedules \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "endpoint": "https://acme.dev/hook", "cron": "0 9 * * 1-5", "timezone": "America/New_York" }'Schedule states
Section titled “Schedule states”| State | Meaning |
|---|---|
active |
The schedule is live. One-shots have a pending delivery; recurring schedules keep producing them. |
paused |
You paused it. Pending deliveries are held — not fired, not lost — until you resume. |
canceled |
You canceled it. Outstanding deliveries are stopped; this is terminal for the schedule. |
Transitions are driven only by you, via
pause / resume / cancel — active → paused → active, or
active|paused → canceled.
Deliveries
Section titled “Deliveries”A delivery is one occurrence of a schedule firing. It moves through a small state machine. Non-terminal states live on the active delivery; the four terminal states are the recorded outcome.
| State | Terminal? | Meaning |
|---|---|---|
scheduled |
— | Created and waiting for its fire time. |
claimed |
— | A dispatcher node has it and is sending the HTTP request right now. This single state spans the whole send (there is no separate “in-flight” or “sending” state). |
retry_scheduled |
— | The last attempt failed retryably; the next attempt is queued for a later time. |
paused |
— | The parent schedule was paused while this delivery was outstanding. |
succeeded |
✅ | An attempt got a 2xx. |
dead_letter |
✅ | Retries exhausted without success. Inspect and replay. |
expired |
✅ | The ttl elapsed before the delivery could land. |
canceled |
✅ | Canceled before it completed. |
Attempts
Section titled “Attempts”An attempt is a single HTTP request to your endpoint, numbered from 1. Every attempt
carries the delivery’s reserved headers so your receiver can authenticate and dedup:
| Header | Value |
|---|---|
Sched-Delivery-Id |
The delivery’s stable ID. |
Sched-Attempt |
The attempt number (1, 2, …). |
Sched-Timestamp |
Unix seconds when the request was signed. |
Sched-Signature |
HMAC over the payload, format t=…,v1=… (supports key rotation overlap). Present only when an active signing secret exists; omitted otherwise (request sent unsigned). |
Idempotency-Key |
The schedule’s idempotency_key if you set one, otherwise the delivery ID — stable across every attempt of this occurrence either way. Dedup on it. |
These headers are reserved: you cannot override Sched-* or Idempotency-Key from your
schedule’s own headers. Each attempt is classified by its result:
| Outcome | What it means | What SchedStack does next |
|---|---|---|
success |
A 2xx response. |
Delivery → succeeded. Done. |
retryable |
A 5xx, 429, timeout, or connection error. |
Schedule the next attempt per retry_policy; delivery → retry_scheduled. When retries run out → dead_letter. |
terminal |
A non-retryable result (most 4xx, plus 3xx redirects and blocked/SSRF-rejected destinations). |
Stop. Delivery → dead_letter. No point retrying a request your endpoint rejected — or one we refused to send. |
How many retryable attempts run, and the backoff between them, comes from the
delivery’s retry_policy — see retries and backoff.
Status legend
Section titled “Status legend”A compact reference you can keep open while you build:
| Object | Live (non-terminal) | Terminal |
|---|---|---|
| Schedule | active, paused |
canceled |
| Delivery | scheduled, claimed, retry_scheduled, paused |
succeeded, dead_letter, expired, canceled |
| Attempt (outcome) | — | success, retryable, terminal |
Where to go next
Section titled “Where to go next”- Quickstart — create a schedule, receive it, verify it, watch it land.
- Verifying signatures — the receiver-side trust centerpiece, with Node / Python / Go samples.
- Idempotency — request keys vs. delivery dedup, kept straight.
- Retries and backoff — the
retry_policyand how attempts are classified. - Dead-letter and replay — inspect parked deliveries and resend them.
- Recurring schedules —
cron, timezones, and DST-correct firing. - Lifecycle — pause, resume, cancel, reschedule.
- API reference — the full interactive REST reference.