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.
status | Ce que ça veut dire |
|---|---|
received | Accepté et stocké, pas encore analysé. Rappelez la route. |
parsed | Tout ce qui dérive du corps est renseigné. |
quota_exceeded | Refusé 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 :
- Filtrer sur
to, pas sur le sujet ni sur « le dernier message ». C’est à cela que sert le sous-adressage — voir adresses-inbox. - 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ères — sentTo, 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 %.
match — all 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ù chercher — inbox pour une inbox, workspace pour un espace entier, ou ni
l’un ni l’autre pour tout ce que votre clé atteint.
La pagination — limit (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
200avec 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 avecwait: 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é.
subjetau lieu desubjectrépond400 CRITERIA_UNKNOWNen 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>/messagesrenvoie 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.