Cycle de vie de livraison
Chaque message sortant que vous envoyez via Orbit porte un champstatus qui progresse pendant que le message voyage de votre appel d’API vers l’appareil du destinataire — ou vers un résultat d’échec. Cette page explique la machine d’états conceptuelle : ce que signifient les états, ce qui pousse chaque transition et où se trouvent les bords dépendants de l’opérateur. Lisez-la avant de souscrire votre premier webhook ou de faire bifurquer votre intégration sur un résultat de message.
La sémantique des états, la table complète des transitions et la carte des événements webhook vivent dans la référence du cycle de vie de l’état des messages ; le schéma de réponse pour lire l’état courant d’un message est dans la référence de l’API de messagerie. Cette page relie les deux à un niveau plus élevé.
Le chemin heureux
Un message qui réussit de bout en bout traversе :pending → queued → sending → sent → delivered → read
Chaque transition est poussée par un composant différent — aucune partie ne voit tout le chemin :
Deux composants se tiennent à côté de ce chemin et peuvent en sortir un enregistrement :
- Le planificateur no-DLR. Quand un opérateur ne renvoie jamais d’accusé de livraison, le planificateur fait vieillir
sentensubmitted_no_receiptaprès une fenêtre de grâce propre au canal — voir Confirmé par l’opérateur vs. intermédiaire sur le fil. - Vous (l’expéditeur). L’annulation d’un message programmé non encore envoyé le fixe à
cancelled, un état terminal qu’aucun appel au fournisseur ni aucun DLR ne touche. Les envois de sandbox se résolvent entest_sent, un état terminal rapporté avant toute publication au fournisseur. La suppression d’un enregistrement déplace tout état terminal versdeleted; après, rien ne peut plus le toucher.
scheduled jusqu’à l’heure de lancement, puis rejoint la file. scheduled ne peut être quitté que par trois voies : promotion en queued à l’heure de lancement, cancelled de votre part, ou expired si sa fenêtre de validité expire avant la publication.
Confirmé par l’opérateur vs. intermédiaire sur le fil
La distinction la plus importante pour les rapports et la réconciliation est de savoir si un état est un résultat confirmé par l’opérateur ou une dispute intermédiaire sur le fil :deliveredetreadsont confirmés par l’opérateur. Un vrai DLR est arrivé ; l’opérateur lui-même a tranché le résultat.submitted_no_receiptest intermédiaire. Il signifie « accepté pour l’envoi, et aucun accusé n’est revenu dans la fenêtre de grâce ». Il est décrit comme intermédiaire (state_class: "intermediate",is_terminal: false) sur chaque webhook, parce qu’un vrai DLRdelivered,readou d’échec peut arriver plus tard et le remplacer.
submitted_no_receipt comme un résultat inconnu, non comme une livraison. Que l’« inconnu » penche vers le positif ou vers le simple intermédiaire dépend du canal — voir Variations par canal.
expired est la vue par l’autre côté : un DLR est arrivé, mais si tard que la fenêtre de l’accusé était déjà fermée. Le résultat ne peut être connu et l’enregistrement est clos ; expired est distribué aux abonnés comme un événement message.failed.
Variations par canal
- Canaux DM de Meta (Instagram, Messenger). La surface d’envoi de Meta n’émet jamais d’accusés de livraison. L’envoi quitte Orbit en
sent, marqueno_dlr_channeldans les métadonnées du message, et décroît ensubmitted_no_receiptaprès 5 minutes. Un destinataire accepté de manière optionnelle implique que Meta garantit la livraison à l’acceptation — donc sur ces canaux,submitted_no_receiptse comporte comme un signal fonctionnel de livraison, et les métadonnées du message portentno_dlr_channel: truepour que vous puissiez distinguer l’état. - Canaux desservis par SMPP (SMS, MMS, voix, fax, RCS). La fenêtre de grâce est de 30 minutes. Ici,
submitted_no_receiptest un véritable intermédiaire : l’appareil a peut-être reçu le message sans accusé rapporté, l’opérateur n’émet peut-être jamais d’accusés sur cette route, ou l’accusé s’est peut-être perdu en transit. Suivez cette proportion comme un signal distinct du taux de livraisons — une proportion installée desubmitted_no_receiptsur une destination signale une route mal desservie ou une route d’accusés cassée, et mérite une enquête dans les deux cas. - E-mail. Ajoute un résultat d’échec que les canaux DM purs n’ont pas :
bounced, quand le serveur de messagerie réception refuse le message. Le rebond compte dans votre taux d’échec final commefailed, mais c’est un état séparé qui vous permet de distinguer les refus côté destinataire de ceux côté fournisseur. - Annulation de l’expéditeur.
cancelledn’est rapportée que par vous — viaPOST /messages/:id/cancelsur un message non encore envoyé. Aucun opérateur ne l’écrit jamais, et par conséquent cela ne déclenche aucun événement webhook ; rien n’informe l’abonné de l’annulation. AppelezGET /messages/:idsi vous exposez l’annulation dans votre interface et devez l’observer. - Sandbox / mode test. Les envois de test se résolvent en
test_sentavant toute publication au fournisseur. Ils déclenchentmessage.sentavecstatus: "test_sent"etmetadata.test_mode: true, donc l’abonné doit bifurquer surmetadata.test_modepour garder le trafic sandbox hors du traitement de production.
Sur quoi bifurquer
Les intégrations ne doivent bifurquer que sur des champs lisibles par machine, jamais sur des descriptions affichées :status— l’état courant du message. C’est le point de bifurcation primaire. Traitez l’ensemble complet : outre les états du chemin heureux et le résultat d’échec, n’oubliez pascancelled,test_sent,submitted_no_receiptetbounced.metadata.classified_error_code— présent sur les résultats d’échec terminaux ; la catégorie d’échec définie par machine et activable par machine. Combinez-la avec leerror_code/error_messagebrut du webhookmessage.failedquand vous voulez les mots de l’opérateur.metadata.no_dlr_channel— présent sur les enregistrements DM de Meta ; vous dit quesubmitted_no_receiptest l’état de livraison garanti par Meta, pas l’intermédiaire traduit de SMPP.state_class/is_terminal— sur le webhook du cycle de vie.is_terminal: true(associé àstate_class: "terminal") signifie que le résultat est final ;submitted_no_receiptrapporteis_terminal: falseprécisément pour que vous ne fermiez pas le livre dessus.
Erreurs fréquentes
- Traiter
message.createdcomme l’acceptation.message.createdse déclenche au moment où l’enregistrement est mis en file, avant tout appel au fournisseur. Un refus synchrone (4xx à l’envoi, ou unrejected/failedimmédiat) laisse cet événement livré tout de même. Combinez-le avecmessage.sentavant de conclure que le message est parti. - Supposer que
deliveredest immuable. Les opérateurs sur certaines routes émettent un accusé de livraison puis une correction quelques minutes plus tard — certains opérateurs indiens et brésiliens le font. Orbit le respecte : un enregistrement peut passer dedelivered → undeliveredou dedelivered → failed. Si vous reflétez les états dans votre propre magasin de données, appliquez les mises à jour de façon idempotente par id de message au lieu de jeter les transitions pour un message déjà servi. - Attendre un webhook sur
cancelled. Il n’arrivera jamais — l’annulation est liée à votre appel d’API, pas à un appel de l’opérateur, donc la plateforme n’émet pas d’événement pour elle. L’enregistrement reste simplement encancelledjusqu’à ce que vous le supprimiez. - Jeter
submitted_no_receiptdans le flux des échecs. Il arrive comme type d’événementmessage.failedpour des raisons de classification (pas de route à part), mais bifurquez surdata.status, pas sur le type d’événement, et tenez-le à l’écart des tons d’échec durs. - Attendre un seul événement final par message. Un message peut émettre
message.failedavecstatus: "submitted_no_receipt"puismessage.delivered, quand l’accusé lent de l’opérateur arrive enfin dans la fenêtre de retard. Mettez à jour et dédupliquez parmessage_id, et laissez l’événement le plus tardif l’emporter.