> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Message validity period: how long the carrier keeps retrying

> What the validity_period send parameter does, its s/m/h/d format contract, how each outbound channel maps it onto the carrier, and how an exhausted window closes the message as expired.

# 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](/concepts/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](/concepts/messaging-services-model) 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.

| Suffix | Unit | Example | Parsed value |
| - | - | - | - |
| *(none)* or `s` | seconds | `"900"` | 900 seconds |
| `m` | minutes | `"30m"` | 1,800 seconds |
| `h` | hours | `"24h"` | 86,400 seconds |
| `d` | days | `"2d"` | 172,800 seconds |

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:

| Channel | Mechanism | Resolution |
| - | - | - |
| SMS via Jasmin | Sent as the `validity-period` parameter in whole seconds | Exact seconds |
| SMS via SMPP | Encoded in the SMPP `validity_period` field as relative time, in whole hours | Whole hours; sub-hour values round up to 1 h |
| SMS via Telnyx | Computed into an absolute ISO 8601 `valid_until` timestamp (send time plus the parsed seconds) | Exact seconds |
| MMS via Telnyx | Same `valid_until` computation as Telnyx SMS | Exact seconds |

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.

| Traffic | Suggested `validity_period` | Reasoning |
| - | - | - |
| OTP / 2FA | `"5m"` | A code the recipient has not used in five minutes is stale; a new code, not a late one, is the remedy. On SMPP routes the value rounds up to 1 h, so pair short OTP windows with your own application-side expiry. |
| Transactional notifications | `"24h"` | Order and appointment messages are still actionable the next morning; overnight handset outages recover inside the window. |
| Marketing / campaigns | `"2d"` | A promotion read two days late still converts; a late OTP does not. |

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.

## Related

* [Delivery lifecycle: queued to delivered, and the states in between](/concepts/delivery-lifecycle)
* [Message send request lifecycle: one send from POST to provider](/concepts/message-send-request-lifecycle)
* [Messaging API reference](/api-reference/endpoints/messaging)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.