Skip to main content

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 ; 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. 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 : 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.
  • 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. 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, 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.