> ## 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.

# Cycle de vie de la livraison : de queued à delivered et les états intermédiaires

> Comment un message sortant passe de queued à sent puis à delivered (ou à un résultat d'échec), qui fait avancer chaque transition, comment les accusés de livraison (DLR) la déclenchent, et les particularités propres à chaque canal à connaître avant votre premier webhook.

# Cycle de vie de la livraison

Chaque message sortant que vous envoyez via Orbit porte un champ `status` qui avance à mesure que le message passe de votre appel d'API vers le combiné du destinataire — ou vers un résultat d'échec. Cette page explique cet automate d'états au niveau conceptuel : ce que signifient les états, ce qui déclenche chaque transition et où se situent les particularités propres aux opérateurs. Lisez-la avant de vous abonner à votre premier webhook ou de faire branchement de votre intégration sur les résultats des messages.

La sémantique par statut, la table complète des transitions et la correspondance avec les événements webhook se trouvent dans la [référence du cycle de vie des statuts de messages](/api-reference/messages-status-lifecycle) ; le schéma de réponse pour lire l'état actuel d'un message se trouve dans la [référence de l'API Messaging](/api-reference/endpoints/messaging). Cette page relie les deux à un niveau supérieur.

## Le chemin nominal

Un message qui réussit de bout en bout passe par :

`pending → queued → sending → sent → delivered → read`

Chaque transition est déclenchée par un acteur différent — aucune partie ne voit l'ensemble du parcours :

| Transition         | Signification                                                            | Ce qui la déclenche                                                                                                                                                                                          |
| ------------------ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `pending → queued` | La ligne existe et est placée dans la file d'envoi.                      | Le chemin d'envoi, à l'acceptation de `POST /messages/<channel>`. `pending` est un état transitoire pré-file que vous observerez rarement.                                                                   |
| `queued → sending` | Un worker a pris le message en charge et le dépêche vers le fournisseur. | Le pipeline d'envoi au moment du défilement.                                                                                                                                                                 |
| `sending → sent`   | Le fournisseur a acquitté la soumission sur le fil.                      | Le chemin d'envoi, au moment où le fournisseur accepte (un ACK de soumission SMPP, un identifiant de message du fournisseur, un identifiant de remise Meta — quel que soit l'accept au niveau fil du canal). |
| `sent → delivered` | L'opérateur a confirmé la livraison au destinataire.                     | Un accusé de livraison (DLR) qui revient de l'opérateur, traité par le pipeline DLR.                                                                                                                         |
| `delivered → read` | Le destinataire l'a ouvert.                                              | Un DLR d'accusé de lecture, sur les canaux qui en émettent (WhatsApp, RCS, e-mail). Implique que la ligne a d'abord été livrée.                                                                              |

Deux acteurs se tiennent à côté de ce chemin et peuvent en sortir une ligne :

* **Le planificateur sans-DLR.** Quand un opérateur ne renvoie jamais d'accusé de livraison, un planificateur bascule `sent` vers `submitted_no_receipt` après une fenêtre de grâce propre au canal — voir [Confirmé par l'opérateur ou neutre fil intermédiaire](#confirmé-par-lopérateur-ou-neutre-fil-intermédiaire).
* **Vous (l'opérateur).** L'annulation d'un message programmé encore non envoyé le fait passer à `cancelled`, un état terminal qu'aucun appel fournisseur et aucun DLR ne touche jamais. Les envois sandbox se résolvent en `test_sent`, un état terminal atteint avant toute dépêche au fournisseur. La suppression d'une ligne fait passer tout état terminal à `deleted`, après quoi plus rien ne peut la toucher.

Un message programmé pour le futur reste en `scheduled` jusqu'à son heure de déclenchement, puis rejoint la file. Il ne peut quitter `scheduled` que de trois façons : promu en `queued` à l'heure de déclenchement, `cancelled` par vous, ou `expired` si sa fenêtre de validité expire avant la dépêche.

## Confirmé par l'opérateur ou neutre fil intermédiaire

La distinction qui compte le plus pour les rapports et la réconciliation est qu'un état soit un **résultat confirmé par l'opérateur** ou un **neutre fil intermédiaire** :

* `delivered` et `read` sont confirmés par l'opérateur. Un vrai DLR est arrivé ; l'opérateur lui-même a affirmé le résultat.
* `submitted_no_receipt` est du neutre fil intermédiaire. Il signifie : « la soumission a été acceptée, et aucun accusé n'est revenu dans la fenêtre de grâce ». Il est étiqueté comme intermédiaire (`state_class: "intermediate"`, `is_terminal: false`) à chaque webhook, car un véritable DLR `delivered`, `read` ou d'échec peut encore arriver ensuite et l'écraser.

Traitez `submitted_no_receipt` comme *résultat inconnu*, non comme une livraison. Que « inconnu » penche vers le positif ou soit sincèrement ambigu dépend du canal — voir [Particularités par canal](#particularités-par-canal).

`expired` est le pendant de l'autre côté : un DLR est arrivé, mais si tard que la fenêtre d'accusé était déjà fermée. Le résultat est indéterminable et la ligne est fermée ; `expired` est relayé aux abonnés comme un événement `message.failed`.

## Particularités par canal

* **Canaux DM de Meta (Instagram, Messenger).** La Send API de Meta n'émet jamais d'accusés de livraison. L'envoi quitte Orbit en `sent`, est marqué `no_dlr_channel` dans les métadonnées du message et bascule en `submitted_no_receipt` au bout de 5 minutes. Pour un destinataire en opt-in, Meta garantit la livraison à l'acceptation — sur ces canaux, `submitted_no_receipt` se comporte donc comme un signal fonctionnel de livraison, et les métadonnées du message portent `no_dlr_channel: true` pour vous permettre de distinguer ce cas.
* **Canaux adossés à SMPP (SMS, MMS, voix, fax, RCS).** La fenêtre de grâce est de 30 minutes. Ici, `submitted_no_receipt` est véritablement ambigu : le combiné a peut-être reçu le message sans qu'aucun accusé ne rapporte, l'opérateur peut ne jamais envoyer d'accusés sur cette route, ou l'accusé a pu être perdu en transit. Suivez ce taux séparément de votre taux de livrés — une part croissante de `submitted_no_receipt` sur une destination pointe vers une route non coopérative ou un chemin d'accusé rompu, et mérite une enquête dans les deux cas.
* **E-mail.** Ajoute un résultat d'échec que les autres canaux n'ont pas : `bounced`, quand le serveur de messagerie récepteur rejette le message. Bounced se décompte de votre taux d'échecs terminaux comme `failed`, mais c'est un statut distinct pour vous permettre de séparer les rejets côté destinataire de ceux côté fournisseur.
* **Annulation côté opérateur.** `cancelled` n'est atteignable que par vous — via `POST /messages/:id/cancel` sur un message non envoyé. Aucun opérateur ne l'écrit jamais, et en conséquence il ne déclenche **aucun événement webhook** ; rien ne notifie un abonné d'une annulation. Interrogez `GET /messages/:id` si vous exposez l'annulation dans votre propre interface et devez l'observer.
* **Sandbox / mode test.** Les envois de test se résolvent en `test_sent` avant toute dépêche au fournisseur. Ils déclenchent `message.sent` avec `status: "test_sent"` et `metadata.test_mode: true`, donc un abonné doit faire branchement sur `metadata.test_mode` pour garder le trafic sandbox hors du traitement de production.

## Sur quoi brancher

Les intégrations doivent commuter uniquement sur des champs lisibles par machine, jamais sur les libellés d'affichage :

* **`status`** — l'état courant du message. C'est le point de branchement principal. Traitez l'ensemble complet : outre les états du chemin nominal et d'échec courants, n'oubliez pas `cancelled`, `test_sent`, `submitted_no_receipt` et `bounced`.
* **`metadata.classified_error_code`** — présent sur les échecs terminaux ; la catégorie d'échec normalisée et machine-friendly. Combinez-la aux `error_code` / `error_message` bruts sur les webhooks `message.failed` quand vous voulez la formulation de l'opérateur lui-même.
* **`metadata.no_dlr_channel`** — présent sur les lignes DM de Meta ; vous dit qu'un `submitted_no_receipt` est le cas Meta à livraison garantie, non le cas SMPP ambigu.
* **`state_class` / `is_terminal`** — sur les webhooks de cycle de vie. `is_terminal: true` (équivalent à `state_class: "terminal"`) signifie que le résultat est définitif ; `submitted_no_receipt` rapporte `is_terminal: false` précisément pour que vous ne clôturiez pas le dossier.

## Pièges courants

1. **Traiter `message.created` comme une acceptation.** `message.created` se déclenche au moment où la ligne est mise en file, avant tout appel fournisseur. Un rejet synchrone (un 4xx à l'envoi, ou un `rejected`/`failed` immédiat) laisse cet événement livré quand même. Associez-le à `message.sent` avant de conclure que le message est parti.
2. **Supposer que `delivered` est immuable.** Des opérateurs sur certaines routes émettent un accusé de livraison, puis une correction quelques minutes plus tard — certains opérateurs indiens et brésiliens font cela. Orbit l'honore : la ligne peut passer de `delivered → undelivered` ou `delivered → failed`. Si vous recopiez les statuts dans votre propre stockage de données, appliquez les mises à jour de manière idempotente par identifiant de message plutôt que d'ignorer les transitions pour un message que vous aviez déjà marqué livré.
3. **Attendre un webhook sur `cancelled`.** Il ne viendra jamais — l'annulation provient de votre propre appel d'API, non d'un callback d'opérateur, donc la plateforme n'émet aucun événement pour cela. La ligne reste simplement à `cancelled` jusqu'à ce que vous la supprimiez.
4. **Jeter `submitted_no_receipt` dans le sac des échecs.** Il arrive sur le type d'événement `message.failed` pour des raisons de transport (il n'y a pas d'événement dédié), mais la clé `status` de sa charge utile est `submitted_no_receipt` avec `is_terminal: false`. Branchez sur `data.status`, pas sur le type d'événement, et gardez-le hors de vos métriques d'échecs durs.
5. **S'attendre à un événement terminal par message.** Un message peut émettre `message.failed` avec `status: "submitted_no_receipt"` puis `message.delivered`, quand un accusé d'opérateur lent finit par arriver dans la fenêtre d'arrivées tardives. Dédoublonnez et réconciliez par `message_id`, en laissant l'événement le plus récent l'emporter.

Une fois l'automate d'états clair, les charges utiles des webhooks par statut sont dans la [référence des événements webhook](/reference/webhook-events), et la sémantique des statuts au niveau des points d'entrée est dans la [référence du cycle de vie des statuts de messages](/api-reference/messages-status-lifecycle).
