2xx and the status write can therefore redeliver.
Tasks that came due while the server was down are caught up on the next scan rather than skipped.
Request headers
Every delivery identifies itself. TheUser-Agent is schedy unless the task’s own headers set one.
X-Schedy-Task-Id carries the task’s id, so a receiver can correlate a request with GET /tasks/{id} - its attempt history and retries - without embedding the id in every payload.
It is set after the task’s custom headers, so a task cannot claim another task’s id.
Signed requests
SetSCHEDY_SIGNING_SECRET and Schedy signs every outgoing request so your receiver can verify it genuinely came from Schedy, and not from anyone who happened to learn the URL.
Two headers are attached:
The signature covers
timestamp.body rather than the body alone, so a captured request cannot be replayed indefinitely: reject anything whose timestamp is outside a small freshness window (a few minutes) and each request is usable only briefly.
To verify, recompute the HMAC over the timestamp, a literal ., and the raw request body (before any JSON parsing), then compare in constant time:
GET and HEAD deliveries carry no body, so they are signed over <timestamp>. (an empty body). The timestamp still authenticates the request and bounds replays.Blocked targets
So Schedy cannot be turned into an SSRF proxy into its host’s network, task URLs that resolve to private, loopback, link-local (including the169.254.169.254 cloud-metadata endpoint), or unspecified addresses are rejected at dial time. The check runs on the resolved IP, so a public DNS name that points at one of those ranges is blocked too.
Set SCHEDY_ALLOW_PRIVATE_TARGETS to allow them on a trusted self-hosted network.