Limits & constraints
Every floor, cap, and validation rule SchedStack enforces — and the typed error you get
when you cross one. Each error follows the typed error envelope
({ "error": { "type", "code", "message", "param", "request_id" } }); the code is
shown below.
Timing
Section titled “Timing”| Rule | Limit | Error |
|---|---|---|
Minimum delay |
>= 1s |
422 sub_floor_delay |
delay format |
valid duration (30s, 5m, 24h, 1h30m) |
400 invalid_duration |
fire_at must be in the future |
>= now + 1s |
422 fire_at_in_past |
fire_at horizon |
<= now + 10 years |
422 fire_at_too_far |
fire_at format |
strict RFC 3339 instant | 400 (message) |
local_fire_at format |
wall-clock 2006-01-02T15:04:05 (with timezone) |
400 invalid_duration |
local_fire_at bounds |
same future / 10-year rules | 422 fire_at_in_past / fire_at_too_far |
| Exactly one timing source | one of delay · fire_at · local_fire_at · cron |
422 missing_timing / 400 multiple_timing |
cron |
5-field standard cron (no seconds field) | 422 invalid_cron |
timezone |
a valid IANA name (e.g. America/New_York); default UTC |
422 invalid_cron |
Payloads & headers
Section titled “Payloads & headers”| Rule | Limit | Behavior |
|---|---|---|
| Request body | <= 1 MB |
larger → 400 invalid_json |
Delivery body |
<= 256 KB |
larger → 422 payload_too_large |
| Custom delivery headers | keep them small | no enforced total-size cap today — don’t rely on one |
Two header rules make a delivery terminal (dead-lettered, never retried):
- No control characters. A header name or value containing CR, LF, or other control characters is rejected.
- No hop-by-hop / reserved names.
host,content-length,connection,keep-alive,proxy-*,te,trailer,transfer-encoding, andupgradeare not allowed.
SchedStack sets its own Sched-* and Idempotency-Key headers after yours, so you
cannot override them. See the delivery contract.
Retry policy
Section titled “Retry policy”All bounds are enforced; any violation returns 422 invalid_retry_policy.
| Field | Range | Default |
|---|---|---|
max_attempts |
1–50 | 8 |
base |
0–24h | 5s |
max |
0–168h (7 days) | 1h |
factor |
1–100 | 2 |
strategy |
exponential only |
exponential |
jitter |
boolean | true |
The next attempt is scheduled at base × factor^attempt, capped at max. See
Retries & dead-letter.
TTL & expiry
Section titled “TTL & expiry”ttl is any valid duration with no minimum or maximum. The delivery deadline is
first fire + ttl, and it caps retries: a delivery that can’t land before the deadline
won’t be retried past it — it finalizes as expired (a terminal, replayable state).
Idempotency (API requests)
Section titled “Idempotency (API requests)”The optional request Idempotency-Key header makes a mutation safe to retry.
| Rule | Behavior |
|---|---|
| Window | the first response is stored for 24 hours |
| Scope | (project, mode, key) — test and live never collide |
| “Same request” | a fingerprint of method + path + body |
| Same key, different body | 409 idempotency_key_reuse |
| Same key, still in flight | 409 idempotency_in_progress |
| What’s cached | only 2xx/3xx; a 4xx/5xx releases the key so you can retry |
| Replay | returns the stored response with Idempotent-Replayed: true |
| Key length | a free-form string (a UUID is ideal) — keep it short; no enforced maximum |
Applies to: POST /schedules, PATCH /schedules/{id}, /reschedule, POST /endpoints,
PATCH /endpoints/{id}, and /replay. See Idempotency.
Pagination
Section titled “Pagination”| Rule | Behavior |
|---|---|
Page size (limit) |
default 20, max 100, min 1 |
Out-of-range limit |
silently clamped to 20 — not an error |
| Cursor | opaque; pass next_cursor back via ?cursor= |
| Invalid cursor | 400 invalid_cursor |
Destinations
Section titled “Destinations”| Rule | Requirement | Error / outcome |
|---|---|---|
| Scheme | HTTPS required | 422 url_blocked |
| Reachability | must be publicly routable — private, loopback, link-local, carrier-grade-NAT, and cloud-metadata addresses (e.g. 169.254.169.254) are rejected |
delivery terminal |
| Redirects | not followed — a 3xx is a terminal misconfiguration, fix the URL |
delivery terminal |
| DNS rebinding | the resolved address is re-checked on every connection | delivery terminal |
| HTTP method | one of POST, PUT, PATCH, GET, DELETE (default POST) |
400 invalid_method |
Endpoint profiles
Section titled “Endpoint profiles”| Rule | Requirement | Error |
|---|---|---|
url |
required, HTTPS | 422 missing_url / url_blocked |
timeout |
> 0 and <= 1h |
422 invalid_timeout |
method with endpoint_id |
omit it — the method comes from the profile | 400 method_on_profile |
endpoint + endpoint_id |
mutually exclusive | 400 multiple_endpoint |
Unknown / archived endpoint_id |
must exist and be active | 422 invalid_endpoint / endpoint_archived |
| Archiving a profile in use | blocked while a schedule references it | 409 profile_in_use |
retry_budget |
rate >= 0, burst >= 0 (default 10 / 100) |
422 invalid_retry_budget |
rate_limit |
per_second >= 0 — accepted but not yet enforced |
422 invalid_rate_limit |
breaker_policy |
threshold 1–1000; base_open / max_open / probe_timeout all required, each > 0 and <= 24h |
422 invalid_breaker_policy |
See Endpoint profiles.
Authentication
Section titled “Authentication”| Rule | Behavior | Error |
|---|---|---|
| Header | Authorization: Bearer sk_<mode>_… |
— |
| Missing key | — | 401 missing_api_key |
| Invalid or revoked key | — | 401 invalid_api_key |
| Cross-scope access | a key only sees its own (project, mode) |
404 not_found |
Defaults worth knowing
Section titled “Defaults worth knowing”- Per-attempt delivery timeout: 30 seconds.
- Retry budget: 10 retries/second, burst 100, per
(project, endpoint). - Circuit breaker: opens after 5 consecutive failures; open window 30s → 1h; probes every 30s.
- Retry policy:
max_attemptsdefaults to 8 when you don’t supply a policy.
Error code index
Section titled “Error code index”Every typed code you can receive, with its HTTP status. Branch on type for control flow
and code for specifics (see Conventions).
code |
HTTP | When |
|---|---|---|
invalid_json |
400 | Malformed JSON, or request body over 1 MB |
invalid_duration |
400 | delay / local_fire_at isn’t a valid duration/format |
multiple_timing |
400 | More than one timing source given |
multiple_endpoint |
400 | Both endpoint and endpoint_id given |
method_on_profile |
400 | method sent alongside endpoint_id |
invalid_method |
400 | HTTP method not one of POST/PUT/PATCH/GET/DELETE |
invalid_cursor |
400 | Pagination cursor is not valid |
missing_api_key |
401 | No Authorization bearer key |
invalid_api_key |
401 | Key is unknown or revoked |
not_found |
404 | No such object in this (project, mode) |
idempotency_key_reuse |
409 | Same Idempotency-Key, different request body |
idempotency_in_progress |
409 | A request with the same Idempotency-Key is still in flight |
not_replayable |
409 | The delivery isn’t in a terminal state, so it can’t be replayed |
profile_in_use |
409 | Can’t archive an endpoint profile a schedule still references |
sub_floor_delay |
422 | delay below the 1 s floor |
fire_at_in_past |
422 | fire_at / local_fire_at is not in the future |
fire_at_too_far |
422 | fire_at / local_fire_at is more than 10 years out |
missing_timing |
422 | No timing source given |
invalid_cron |
422 | cron expression or timezone is invalid |
payload_too_large |
422 | Delivery body over 256 KB |
url_blocked |
422 | Destination isn’t HTTPS or isn’t publicly routable |
invalid_retry_policy |
422 | A retry_policy field is out of range |
missing_url |
422 | Endpoint profile has no url |
invalid_endpoint |
422 | endpoint_id doesn’t exist |
endpoint_archived |
422 | endpoint_id refers to an archived profile |
invalid_timeout |
422 | Profile timeout is <= 0 or > 1h |
invalid_retry_budget |
422 | Profile retry_budget values are invalid |
invalid_rate_limit |
422 | Profile rate_limit value is invalid |
invalid_breaker_policy |
422 | Profile breaker_policy is incomplete or out of range |