All docs

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.

  • dead schedules are not retried automatically. They're kept for inspection.
  • To find dead deliveries, filter your schedule list by status=dead, or scan GET /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) or clone(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 3xx response is treated as a non-success. Point the schedule at the final URL directly. (This is a security measure — see Security.)