Aller au contenu
Facteur
ENCommencer

Lire un message depuis une suite de tests

Les trois routes de lecture, la forme d'un message, et le champ status qu'il faut lire avant tout le reste.

Trois routes suffisent à écrire un test.

GET  /v1/inboxes/<id>/messages    les 50 derniers, du plus récent au plus ancien
GET  /v1/messages/<id>            un message, en entier
POST /v1/messages/search          celui que vous cherchez, par critères

La recherche est ce qu’il faut préférer dès qu’un test attend un message précis : elle évite de lire une liste pour y chercher soi-même, et elle ne renvoie par défaut que les messages entièrement analysés.

Authentification : créez une clé depuis Réglages → Clés d’API et envoyez-la en Authorization: Bearer fk_…. Voir cles-api pour ce qu’une clé atteint et comment la faire tourner.

La forme d’un message

{
  "id": "…",
  "inbox": "ab12cd34",
  "receivedAt": "2026-08-07T09:14:02.481Z",
  "deliveryMs": 412,
  "status": "parsed",
  "from": { "address": "no-reply@exemple.test", "name": "Exemple" },
  "to": "signup+7c1e@ab12cd34.inbox.facteur.eu",
  "sizeBytes": 4821,
  "subject": "Votre code de confirmation",
  "text": { "body": "…" },
  "html": { "body": "…" },
  "otp": "418302",
  "links": ["https://exemple.test/confirm?token=…"],
  "codes": [],
  "headers": { "message-id": "…", "x-votre-en-tete": "…" },
  "extract": {}
}

deliveryMs est le délai entre l’envoi et la réception, mesuré chez nous. extract porte ce que vos propres pointeurs ont trouvé — voir pointeurs.

status se lit avant tout le reste

C’est le champ qui évite la classe d’échec la plus pénible : un test rouge sans raison visible.

statusCe que ça veut dire
receivedAccepté et stocké, pas encore analysé. Rappelez la route.
parsedTout ce qui dérive du corps est renseigné.
quota_exceededRefusé faute de plan. Rien n’a été stocké ni analysé.

L’analyse a lieu après l’acquittement SMTP. Pendant quelques millisecondes, un message existe donc sans sujet, sans code et sans liens. Sans ce champ, « il n’y a pas de code dans ce message » et « le code n’est pas encore extrait » se ressemblent trait pour trait.

Un message refusé omet les clés dérivées au lieu de les renvoyer vides : message.otp vaut undefined, pas un null plausible sur lequel on écrirait une assertion qui passe pour la mauvaise raison. Il porte à la place un objet error avec son code.

L’attente

Le serveur attend pour vous. POST /v1/messages/search accepte un wait, en millisecondes, et ne répond pas tant qu’aucun message ne correspond — c’est la section « Attendre le message plutôt que le sonder », plus bas. C’est la route à privilégier, et elle remplace tout ce qui suit dans cette section.

Ce paragraphe affirmait le contraire jusqu’en août 2026, longtemps après la livraison de l’attente.

Le motif de guet ci-dessous reste utile si vous lisez la liste d’une inbox plutôt que la recherche, car cette route-là n’a pas d’attente :

async function attendre(inboxId, versQui, delaiMs = 15000) {
  const fin = Date.now() + delaiMs;
  while (Date.now() < fin) {
    const messages = await api(`/v1/inboxes/${inboxId}/messages`);
    const trouve = messages.find((m) => m.to === versQui && m.status === 'parsed');
    if (trouve) return trouve;
    await new Promise((r) => setTimeout(r, 500));
  }
  throw new Error(`aucun message pour ${versQui} en ${delaiMs} ms`);
}

Deux détails qui font la différence entre un test fiable et un test capricieux :

  1. Filtrer sur to, pas sur le sujet ni sur « le dernier message ». C’est à cela que sert le sous-adressage — voir adresses-inbox.
  2. Exiger status === 'parsed'. Sinon le test lit parfois un message dont l’OTP n’est pas encore là, et échoue une fois sur cinquante.

Chercher un message

curl -X POST https://api.facteur.eu/v1/messages/search \
  -H "Authorization: Bearer fk_votre_cle" \
  -H "content-type: application/json" \
  -d '{"inbox": "j3k9x2mq", "sentTo": "commande-4821", "subject": "confirmation"}'

Les critèressentTo, sentFrom, subject, body, et since comme borne basse. Ce sont des sous-chaînes, insensibles à la casse ; % n’est pas un joker et ne cherche que le caractère %.

matchall par défaut : tous les critères doivent correspondre. any pour qu’un seul suffise. since n’entre jamais dans le any : c’est une borne, pas une alternative.

Où chercherinbox pour une inbox, workspace pour un espace entier, ou ni l’un ni l’autre pour tout ce que votre clé atteint.

La paginationlimit (50 par défaut, 200 au plus) et le nextCursor que la réponse renvoie. Reprenez-le tel quel ; il est nul sur la dernière page, ce qui évite de comparer un compte à une limite.

Attendre le message plutôt que le sonder

Ajoutez wait, en millisecondes, et la recherche ne répond plus tant que rien ne correspond :

curl -X POST https://api.facteur.eu/v1/messages/search \\
  -H "Authorization: Bearer fk_votre_cle" \\
  -H "content-type: application/json" \\
  -d '{"inbox": "j3k9x2mq", "sentTo": "commande-4821", "wait": 30000}'

C’est ce qui remplace la boucle sleep/retry que vous écririez autrement. Lancez la recherche avant de déclencher l’action qui envoie le mail : elle attendra.

  • Elle répond dès l’arrivée, pas à l’échéance.
  • Le message rendu est entièrement analysé — sujet, OTP, liens. Une attente qui répondrait dès qu’une ligne existe vous rendrait un message sans code.
  • Rien trouvé à l’échéance, c’est 200 avec une liste vide, pas une erreur : le message absent est une réponse, et c’est à votre test de décider si c’est un échec.
  • Cinq minutes au plafond. Au-delà, la valeur est ramenée au plafond plutôt que refusée. Sans wait, ou avec wait: 0, la recherche regarde une fois et répond immédiatement.
  • Vingt attentes simultanées par organisation. Au-delà, 429 TOO_MANY_WAITS — en général le signe d’une suite qui lance des recherches sans attendre leurs réponses.

Deux refus qui vous font gagner une heure

  • Un critère inconnu est refusé, pas ignoré. subjet au lieu de subject répond 400 CRITERIA_UNKNOWN en nommant le champ. Ignoré, il vous aurait rendu tous les messages de l’inbox, et votre test serait passé pour la mauvaise raison jusqu’au jour où deux messages arrivent.
  • Une inbox qui n’est pas la vôtre répond 404, pas une liste vide. Une liste vide se lit « le message n’est pas arrivé » et envoie déboguer un pipeline qui fonctionne.

Le champ status, encore

Par défaut, la recherche ne rend que les messages parsed. C’est voulu : un message existe quelques millisecondes avant d’être analysé, sans sujet ni OTP, et le rendre rendrait « pas encore arrivé » et « arrivé mais pas lu » indiscernables. Passez "status": "any" si vous voulez voir ce transitoire.

Limites connues

  • Cinquante messages sans pagination sur la liste. GET /v1/inboxes/<id>/messages renvoie les cinquante derniers et n’a pas de curseur. La recherche, elle, pagine — utilisez-la si vous avez besoin d’aller plus loin.
  • Le SDK n’existe pas encore. messages.await, montré sur le site, est un client TypeScript qui n’est pas publié. L’attente elle-même fonctionne — voir ci-dessus — mais il faut l’appeler en HTTP.
  • Pas de recherche plein texte. Les critères sont des sous-chaînes. Chercher un mot avec une casse ou un accent différent ne le trouvera pas.
  • Pas de pièces jointes. Le stockage les prévoit, l’API ne les sert pas.