Skip to main content

Send-window optimization with send_at_type

Every send endpoint that accepts scheduled_at also accepts an optional send_at_type field. With the default ("fixed" or omitted), the platform honors your scheduled_at instant verbatim. With "optimize_with_send_window", the platform treats scheduled_at as the earliest acceptable send time and, if that instant falls outside your configured send window in the recipient’s local timezone, moves the fire time forward to the next window open. This page covers the two modes, where the send window comes from, a worked end-to-end example, how the shift is stored, what happens when the platform cannot read your window, and when to pick each mode. For the scheduling model itself — park, gate at fire time, fire — see Schedule one-off sends with scheduled_at.

fixed vs optimize_with_send_window

Omitting send_at_type is identical to passing "fixed" — every existing scheduled send keeps its current behavior. The optimization is strictly opt-in per message. When no send window is configured anywhere (neither on the message nor in organization settings), "optimize_with_send_window" is a no-op and behaves like "fixed".

Where the send window comes from

The platform resolves the window in two steps:
  1. Per-message override — a send_window object inside the message’s metadata. If present and enabled, it wins.
  2. Organization fallback — the send_window object in your organization settings, used when the message carries no enabled override.
The window shape is the same in both places:

Per-message window

Attach the window to the individual send:
Use this when one message needs a different envelope than the rest of your traffic — a promotional blast with tighter hours than your transactional sends, for example.

Organization-level window

Set the fallback once via PUT /api/v1/settings/general (the same surface the org quiet-hours gate uses; see Quiet hours: org-wide channel gates vs. the campaign fallback window):
With the org window in place, any send that passes send_at_type: "optimize_with_send_window" picks it up without repeating the window on every request. A per-message metadata.send_window overrides the org window for that message only. recipient_timezone is an IANA timezone name (America/New_York, Europe/Istanbul) that tells the platform which local clock to evaluate the window against. Pass the recipient’s timezone explicitly whenever you know it.

Worked example: 3 AM becomes 9 AM

POST to the SMS send endpoint with a fire time that lands at 3 AM in the recipient’s local timezone:
With the org window above (09:00–20:00 recipient-local, weekdays): 2026-04-16T07:00:00Z is 3:00 AM in New York on a Thursday, which is outside the window. The platform computes the next window open — 9:00 AM recipient-local, 2026-04-16T13:00:00Z — and stores that as the fire time. The response is still 202 with status: "scheduled". Had the requested instant already been inside the window — say 14:00Z (10 AM in New York) — it would be stored unchanged. The optimization only ever moves a send forward to the next open, never backward.

One-shot shift: the stored fire time is already adjusted

The shift happens once, at schedule time. The platform computes the adjusted instant and persists it as the row’s scheduled_at, exactly as if you had submitted that time yourself. There is no separate “requested” vs “adjusted” field and no per-fire window evaluation — the scheduler’s plain due-rows selector (scheduled_at <= now) picks the row up with no special handling. The practical consequences:
  • GET /api/v1/messages/scheduled and GET /api/v1/messages/{id} both show the adjusted fire time. If you scheduled 3 AM recipient-local and the window moved it to 9 AM, the API reads 9 AM back.
  • PATCH /api/v1/messages/{id} rewrites the (adjusted) scheduled_at like any other scheduled row; cancel and bulk-cancel behave identically.
  • If you need the original requested instant for audit, keep it in your own logs or in metadata — the platform stores the adjusted value as the single source of truth for when the row fires.

Failure mode: fail-closed when the window can’t be read

"optimize_with_send_window" is an opt-in promise that the message will not fire outside your window. If the platform cannot read your organization settings — a transient database fault, for example — it does not silently fall back to "fixed" and ship a 3 AM send. Instead the send request fails with a 503 and code SEND_WINDOW_LOOKUP_FAILED, asking you to retry. No scheduled row is persisted, so nothing fires out-of-window and nothing is lost — retry the request and it succeeds once settings are readable again. Note the asymmetry: a missing window (no per-message override, org window unset or enabled: false) is a no-op; an unreadable settings row is a 503. The first means “no optimization configured,” the second means “optimization requested but not verifiable.”

When to use each mode

Use optimize_with_send_window for compliance-sensitive marketing traffic. Non-transactional SMS is commonly restricted to daytime recipient-local hours (the TCPA treats 8 AM–9 PM recipient-local as the solicitation envelope in the US, and similar smile-hours rules exist elsewhere). Configure the org window once, keep passing your natural “send not before” time, and let the platform absorb the timezone arithmetic per recipient. This is a tenant-owned control: the window you configure is yours; the platform applies it but does not mandate it. Use fixed when the clock time is the product. Appointment reminders, two-factor codes, read-back-a-code flows, and any message whose value depends on landing at a precise instant. An appointment reminder optimized to a 9–20 window could silently slide to the next morning — for that class of message, verbatim timing is the correct behavior and the window offers nothing. The two modes compose per send: a tenant can run marketing blasts with optimization and transactional traffic with fixed timing on the same account, at the same time.