Skip to main content

Message validity period

Every outbound message can carry a validity_period: a per-send time-to-live that tells the carrier how long to keep attempting delivery before it gives the message up. While the window is open, a handset that is switched off or out of coverage stays queued at the carrier and gets retried; once the window lapses, the carrier stops, and Orbit closes the row as expired. This page defines the parameter’s format, how each channel applies it, and how the end of the window shows up in the delivery lifecycle. validity_period is a tenant-owned send control. You set it per message on the send request, or once as the default validity_period_seconds on a messaging service that fills in when the request leaves the field blank.

What validity_period does

A message whose recipient cannot be reached immediately does not fail at once. The carrier holds it and retries: the handset powers on, re-registers, and the attempt resumes. validity_period bounds that store-and-forward retry loop. It answers one question: how long is this message worth delivering? While the window is open, the message sits at the carrier after Orbit has already recorded sent. sent is a submission acknowledgement, not a delivery claim, so a row can hold at sent for minutes or hours of carrier retrying before it resolves to delivered or expired. When the window lapses with no delivery, the carrier drops the attempt and reports it; Orbit maps that outcome to the terminal status expired. Nothing retries the send at that point: a lapsed window closes the row, and delivering the content again means sending a new message. Omitting the parameter leaves the carrier’s own default window in force. Orbit does not impose a platform-wide TTL of its own on the send path; the only platform-imposed bound is the one you set yourself, either per send or as a messaging-service default (validity_period_seconds, 0–14400 seconds).

The format contract

validity_period is a string: a positive integer with an optional unit suffix. Suffixes are case-insensitive ("30M" parses as 30 minutes). Anything else, a negative value, 0, or a bare non-numeric string is malformed: Orbit ignores the parameter for that send, logs a warning naming the offending value, and the message goes out under the carrier default instead of failing the request.

Why the suffix matters: the historic 60x overrun

The suffix is not decorative. An earlier implementation read the value with a plain integer parse that stopped at the first non-digit character, so "30m" yielded the number 30, and the channel then interpreted that number as hours. A sender that asked for 30 minutes of retrying silently bought 30 hours: a roughly 60x overrun that kept undeliverable messages lingering on carrier queues long past the sender’s intent. The current parser resolves the suffix first and converts the whole value to seconds, so "30m" is 1,800 seconds on every channel that reads it. Two consequences follow. First, always send the suffix that matches your intent; a bare number is seconds. Second, the one channel that can only express whole hours (SMPP, below) rounds sub-hour values up to one hour so a short TTL is honoured as a truthful minimum, never resurrected as the old 60x overrun.

Per-channel mapping

The channels that honour validity_period all read the same parsed value, but each maps it onto its carrier’s own expiry mechanism: Notes per channel:
  • Jasmin (SMS). The parsed seconds are forwarded as-is; a "30m" send carries a 1,800-second validity on the wire.
  • SMPP (SMS). The SMPP relative-time field Orbit writes expresses validity in whole hours, so a value under one hour rounds up to 1h, and "90m" rounds up to 2h. If you need minute-scale expiry on an SMPP route, know that the carrier will hold the message for at least one hour.
  • Telnyx (SMS and MMS). The parsed seconds are added to the send time to produce the absolute valid_until timestamp the carrier expects; the two Telnyx channels compute it identically.
Channels not in the table, including the Meta DM channels (WhatsApp, Instagram, Messenger) and email, do not consume validity_period: their retry and expiry behaviour is governed by the provider, so the parameter is ignored on those sends. The service-level default behaves the same way: validity_period_seconds on a messaging service caps at 14,400 seconds (4 hours), while the per-send suffixed string has no platform cap beyond what you set.

Inbound traffic

validity_period applies to outbound (MT) traffic only. Inbound messages carry no validity window of this kind; nothing on the inbound path reads or honours the parameter.

Lifecycle interaction: expired

When a carrier gives up at the end of the window, Orbit closes the row as expired, a terminal state fanned out to your subscribers as a message.failed event with status: "expired". The receipt side names the same outcome ttl_expired: the validity period lapsed before the destination accepted the message. Three statuses can each look like “the message did not get through in the end”, but they answer different questions:
  • expired is carrier-confirmed expiry. A receipt arrived stating that the validity window closed before delivery; the outcome is known and final. This is the state an exhausted validity_period produces.
  • submitted_no_receipt is outcome unknown, not expiry. The submission was accepted on the wire but no receipt of any kind came back inside the grace window (30 minutes on SMPP-backed channels). No carrier asserted anything about the window, and a late genuine receipt can still overwrite it; expired can never be overwritten.
  • cancelled is operator-initiated. You cancelled a still-unsent message through the API; no carrier was ever asked to deliver it, and no webhook fires for it.
One more boundary: a scheduled send whose validity window lapses before its fire time leaves scheduled as expired without ever being dispatched. On a dispatched message, the window runs from submission to the carrier, so a message sent at 12:00 with validity_period: "1h" can only resolve expired from about 13:00 onward.

Choosing a TTL

Match the window to how long the content is actionable, not to how long delivery usually takes. Most messages that will deliver do so within seconds or minutes; the validity period is the tail of the distribution, the switched-off-handset case. Pitfalls to plan around:
  1. Sub-hour values round up on SMPP. "30m" becomes 1 h on the SMPP channel. If a strict sub-hour window is a hard requirement, enforce it in your application as well as on the send.
  2. Malformed values fall back silently. "30mins" or "-1h" is ignored with a warning; the send proceeds on the carrier default. Validate the s/m/h/d grammar before you rely on it.
  3. The window starts at dispatch, but scheduled sends wait first. A message scheduled an hour out with validity_period: "30m" has its carrier-side window start at fire time, yet if the window lapses before dispatch the row expires in scheduled. Schedule and TTL together, not independently.
  4. A lapsed window is not a retry queue. expired is terminal; there is no automatic re-send. If the content is still relevant, send a fresh message.
  5. Longer is not safer. Every hour a dead message sits on a carrier queue is an hour your recipient’s handset state and your content age together. A window longer than the content’s usefulness only delays the expired outcome you would have wanted earlier.