All docs

Scheduling

A schedule is one HTTP request you want sent at a future time. This page covers how to specify the time, the body, the method and headers, how to avoid duplicates, and how Early Firing works.

Choosing a fire time

Give exactly one of two fields:

  • fireAt — an absolute time, as an ISO-8601 string or a Date. Include a timezone offset (Z or +09:00); the time must be in the future.
  • in — a relative shorthand: "30s", "15m", "2h", "1d". Resolved relative to when the request is received.
await sendit.schedule({ url, fireAt: "2026-06-29T09:00:00Z", payload });
await sendit.schedule({ url, in: "2h", payload });

Over plain REST, only the absolute form exists — send fireAt as an ISO-8601 string. (The SDK converts in to a fireAt for you.)

A fireAt in the past is rejected with a validation error.

Payload

payload is optional. The SDK accepts either form:

  • An object — serialized to JSON, and Content-Type: application/json is added for you.
  • A string — sent verbatim, with no automatic Content-Type.
// Object → JSON body + application/json
await sendit.schedule({ url, in: "1h", payload: { userId: 42 } });

// String → sent as-is
await sendit.schedule({ url, in: "1h", payload: "raw text body" });

The payload is encrypted at rest (AES-256-GCM) the moment it's stored, and only decrypted in memory at the instant of firing. We never log it, and the API never returns it back to you — responses only tell you its size (payloadSizeBytes) and whether one exists (hasPayload). Payload size is capped per plan; see Plans & Limits.

Method and headers

method defaults to POST. Any of GET, POST, PUT, PATCH, DELETE is allowed.

headers is an optional map of custom request headers sent with the webhook. A few are reserved and rejected because we set them ourselves:

await sendit.schedule({
  url: "https://api.myapp.com/hook",
  method: "PUT",
  headers: { "X-My-Tenant": "acme" },
  in: "1d",
  payload: { ok: true },
});

Idempotency

To make scheduling safe to retry, pass an idempotencyKey. If you send the same key again (for the same account), we return the existing schedule instead of creating a second one — it does not count again against your monthly schedule quota.

await sendit.schedule({
  url,
  in: "1d",
  payload,
  idempotencyKey: `trial-end:${userId}`,
});

This also changes the SDK's retry behavior: a request carrying an idempotency key is safe to retry on network failures, so the SDK will. Without one, a schedule() call is not retried, to avoid accidentally firing twice. See Reliability.

For bulk creates, every item must carry an idempotency key for the batch to be retried, and duplicate keys within one batch are rejected.

Early Firing

By default a schedule begins sending at fireAt — we never dispatch before the instant you asked for. Because network round-trips take time, that means the request arrives slightly after the scheduled moment.

Early Firing is an opt-in correction for cases where you want arrival as close to fireAt as possible. When enabled, we measure the recent round-trip time (RTT) to the target host and begin sending a little early so the request lands closer to the intended instant.

Turn it on per schedule with earlyFire:

await sendit.schedule({ url, in: "1h", payload, earlyFire: true });
sendit.schedule(url=url, in_="1h", payload=payload, early_fire=True)
  • It's best-effort, not a hard guarantee. We aim for second-level precision; we do not promise an exact arrival instant.
  • The correction is derived from a smoothed average (EWMA) of recent RTTs to that host, and is capped at 2 seconds.
  • With too few samples for a host, no correction is applied.
  • The setting is stored on the schedule, so reschedule, replay, and clone keep it unless you override it.
  • You can set an account-wide default in settings; the per-schedule earlyFire flag overrides it.

Punctuality

Every schedule records how it actually fired, so precision is something you can measure rather than take on faith. After a schedule fires, get(id) and list() return:

  • fired_at (firedAt in the Node SDK) — the timestamp of the first delivery attempt, or null before it fires. Retries don't change it; it's the single ground-truth fire instant.
  • offset_ms (offsetMs) — fired_at − fireAt in milliseconds. Positive means it fired slightly late (normal dispatch jitter); negative means it fired early, which only happens on schedules with earlyFire enabled. null before it fires.
  • early_fire (earlyFire) — whether Early Firing was enabled for this schedule.

These are the raw measured values for your own schedules. The public, anonymized punctuality numbers shown on the site floor early-fire offsets at zero so an intentional early send never flatters the average — your per-schedule values are always the unrounded truth.

After scheduling

Once created, a schedule can be inspected and changed:

  • get(id) / list() — read status and metadata
  • reschedule(id, …) — move the fire time (only while still scheduled)
  • cancel(id) — cancel before it fires
  • replay(id) — re-send the same encrypted payload as a new schedule
  • clone(id, …) — send again with a new payload
  • getAttempts(id) — see each delivery attempt

See the SDK Reference for full signatures.