Reliability & Retries
SendItWhenever is built to deliver, even when your endpoint has a bad moment.
Delivery is at-least-once
We guarantee a scheduled webhook is delivered at least once. In rare cases — a network blip right as we record success, for example — your endpoint may receive the same delivery twice. Make your handler idempotent: key off the schedule id or your own idempotencyKey and ignore a repeat.
The signed payload includes everything you need to deduplicate (see Verifying Webhooks).
Retries and backoff
If a delivery fails — connection error, timeout, or a non-2xx response — we retry:
- Up to 5 attempts total, by default.
- Exponential backoff, starting around 30 seconds and growing each attempt (≈30s → 1m → 2m → 4m → …).
Each attempt is recorded so you can see exactly what happened.
Customising retries
You can tune retry behaviour per account from the dashboard (or via PATCH /v1/me/relay-settings): the attempt count, the backoff delay, and the strategy. Values are clamped to your plan's limits — the maximum attempt count is 5 on Free and 10 on Indie (and every paid plan). Leaving a setting unset inherits the defaults above.
The dead-letter queue (DLQ)
When all retries are exhausted — or the target is permanently rejected (for example, it resolves to a blocked address; see Security) — the schedule moves to status dead and is parked in a dead-letter queue.
deadschedules are not retried automatically. They're kept for inspection.- To find dead deliveries, filter your schedule list by
status=dead, or scanGET /v1/logs?outcome=failed(see below). There is no per-account alert — surface failures by polling these. - To re-attempt a dead delivery after you've fixed the cause, use
replay(id)(same payload) orclone(id, …)(new payload). Each creates a fresh schedule.
The dead-letter record stores only metadata (target, attempt count, failure reason) — never the decrypted payload.
Inspecting delivery attempts
Every attempt is logged. Fetch them with getAttempts(id):
const attempts = await sendit.getAttempts("sch_123");
for (const a of attempts) {
console.log(a.attemptNo, a.statusCode, a.latencyMs, a.errorText);
}
Each attempt includes:
| Field | Meaning |
|---|---|
attemptNo |
1-based attempt number. |
statusCode |
HTTP status your endpoint returned, or null if the request never completed. |
latencyMs |
Round-trip time for the attempt, or null if it failed before a response. |
errorText |
A short snippet of your endpoint's response body, or the error — useful for debugging. Never your own payload. |
attemptedAt |
When the attempt ran. |
Attempt logs are retained per plan (7 days on Free, 30 days on Indie — see Plans & Limits).
For a single feed across all your schedules — newest first, one row per attempt, filterable by outcome — use GET /v1/logs (see the API Reference). It's the quickest way to spot recent failures without walking each schedule.
Timeouts and redirects
When we call your endpoint:
- Requests time out after a bounded window (tens of seconds) — a hung endpoint counts as a failed attempt and is retried.
- Redirects are not followed. A
3xxresponse is treated as a non-success. Point the schedule at the final URL directly. (This is a security measure — see Security.)