Skip to content

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.)

  1. A schedule is your instruction: this destination, at this time (or on this recurrence). You create one with POST /v1/schedules.

  2. 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.

  3. Each delivery makes one or more attempts — individual outbound HTTP requests to your endpoint. The first attempt that gets a 2xx ends the delivery; failures are retried under the delivery’s retry_policy.

schedule ──┬─► delivery (occurrence) ──┬─► attempt 1 (HTTP POST → your endpoint)
│ ├─► attempt 2 (retry)
│ └─► attempt 3 (succeeds → delivery done)
└─► delivery (next occurrence, recurring only) …

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 a 2xx from your endpoint.
  • dead_letter — retries were exhausted without a 2xx. The delivery is parked, not dropped; you can inspect it and replay it.
  • expired — the delivery’s ttl elapsed 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.

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-Signature HMAC 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’s idempotency_key if 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.

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).

Terminal window
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"}'
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 / cancelactive → paused → active, or active|paused → canceled.

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.

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.

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