Cycle de vie de la livraison
Chaque message sortant que vous envoyez via Orbit porte un champstatus 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
sentverssubmitted_no_receiptaprè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 entest_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.
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 :deliveredetreadsont confirmés par l’opérateur. Un vrai DLR est arrivé ; l’opérateur lui-même a affirmé le résultat.submitted_no_receiptest 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 DLRdelivered,readou d’échec peut encore arriver ensuite et l’écraser.
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_channeldans les métadonnées du message et bascule ensubmitted_no_receiptau bout de 5 minutes. Pour un destinataire en opt-in, Meta garantit la livraison à l’acceptation — sur ces canaux,submitted_no_receiptse comporte donc comme un signal fonctionnel de livraison, et les métadonnées du message portentno_dlr_channel: truepour 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_receiptest 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 desubmitted_no_receiptsur 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 commefailed, 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.
cancelledn’est atteignable que par vous — viaPOST /messages/:id/cancelsur 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. InterrogezGET /messages/:idsi vous exposez l’annulation dans votre propre interface et devez l’observer. - Sandbox / mode test. Les envois de test se résolvent en
test_sentavant toute dépêche au fournisseur. Ils déclenchentmessage.sentavecstatus: "test_sent"etmetadata.test_mode: true, donc un abonné doit faire branchement surmetadata.test_modepour 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 pascancelled,test_sent,submitted_no_receiptetbounced.metadata.classified_error_code— présent sur les échecs terminaux ; la catégorie d’échec normalisée et machine-friendly. Combinez-la auxerror_code/error_messagebruts sur les webhooksmessage.failedquand 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’unsubmitted_no_receiptest 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_receiptrapporteis_terminal: falseprécisément pour que vous ne clôturiez pas le dossier.
Pièges courants
- Traiter
message.createdcomme une acceptation.message.createdse déclenche au moment où la ligne est mise en file, avant tout appel fournisseur. Un rejet synchrone (un 4xx à l’envoi, ou unrejected/failedimmédiat) laisse cet événement livré quand même. Associez-le àmessage.sentavant de conclure que le message est parti. - Supposer que
deliveredest 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 dedelivered → undeliveredoudelivered → 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é. - 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 àcancelledjusqu’à ce que vous la supprimiez. - Jeter
submitted_no_receiptdans le sac des échecs. Il arrive sur le type d’événementmessage.failedpour des raisons de transport (il n’y a pas d’événement dédié), mais la cléstatusde sa charge utile estsubmitted_no_receiptavecis_terminal: false. Branchez surdata.status, pas sur le type d’événement, et gardez-le hors de vos métriques d’échecs durs. - S’attendre à un événement terminal par message. Un message peut émettre
message.failedavecstatus: "submitted_no_receipt"puismessage.delivered, quand un accusé d’opérateur lent finit par arriver dans la fenêtre d’arrivées tardives. Dédoublonnez et réconciliez parmessage_id, en laissant l’événement le plus récent l’emporter.