Lire un message depuis une suite de tests
Une seule requête suffit : elle attend le message, et vous le rend entièrement analysé.
Trois routes suffisent à écrire un test.
GET /messages celui que vous cherchez, et il attend qu'il arrive
GET /inboxes/<id>/messages les messages d'une inbox, du plus récent au plus ancien
GET /messages/<id> un message, en entier
Utilisez la première. C'est celle qui attend le message pour vous, et celle qui vous le rend entièrement analysé.
Avant de commencer
Créez une clé d'API depuis Paramètres → Clés d'API et envoyez-la dans un en-tête :
Authorization: Bearer fk_votre_cle
Il faut la permission ws:read. Voir Clés d'API pour ce qu'une clé atteint et
comment la remplacer.
Attendre le message plutôt que le sonder
Ajoutez wait, en millisecondes, et la recherche ne répond pas tant que rien ne
correspond :
curl -G https://api.facteur.eu/v1/messages \
-H "Authorization: Bearer fk_votre_cle" \
--data-urlencode "inbox=j3k9x2mq" \
--data-urlencode "sentTo=commande-4821" \
--data-urlencode "wait=30000"
C'est ce qui remplace la boucle sleep ou retry que vous écririez autrement.
Lancez la recherche avant de déclencher l'action qui envoie l'email : elle
attendra.
Six choses à savoir :
- Elle répond dès l'arrivée du message, pas à l'échéance : la recherche est rejouée toutes les 250 ms.
- Le message rendu est entièrement analysé : sujet, code à usage unique, liens.
- Rien trouvé à l'échéance donne un
200avec une liste vide, pas une erreur. C'est à votre test de décider si un message absent est un échec. - Cinq minutes au plafond. Demandez plus, 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. 503 API_DRAININGveut dire « relancez », pas « rien n'est arrivé ». Quand nous remplaçons une instance, celle qui tenait votre attente vous la rend au lieu de couper la connexion. Relancez la même recherche : une autre instance la reprend. L'en-têteRetry-Afterdit combien attendre.
Si vous lisez la liste d'une inbox
GET /inboxes/<id>/messages n'a pas d'attente. Il vous faut alors une boucle de
guet :
async function attendre(inboxId, versQui, delaiMs = 15000) {
const fin = Date.now() + delaiMs;
while (Date.now() < fin) {
const { messages } = await api(`/inboxes/${inboxId}/messages`);
const trouve = messages.find((m) => m.toAddress === 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 séparent un test fiable d'un test capricieux :
- Filtrez sur
toAddress, pas sur le sujet ni sur « le dernier message ». C'est à cela que sert le sous-adressage, voir Adresses d'inbox. - Exigez
status === 'parsed'. Sinon le test lit parfois un message dont le code n'est pas encore extrait, et échoue une fois sur cinquante.
Lire le champ status avant tous les autres
status |
Ce que ça veut dire | Ce que vous faites |
|---|---|---|
parsed |
Tout ce qui dérive du corps est renseigné | Lisez le message |
received |
Accepté, pas encore analysé | Rappelez la route, ou utilisez wait qui ne rend que du parsed |
quota_ |
Refusé : une limite de votre plan ou de l'inbox est atteinte. Rien n'a été analysé | Voir Quotas et rétention |
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 champs extraits au lieu de les renvoyer vides :
message.otp vaut undefined et non un null sur lequel vous écririez une assertion
qui passe pour la mauvaise raison. Il porte à la place un errorCode qui dit laquelle des limites l'a refusé.
La forme d'un message
{
"id": "…",
"inbox": "ab12cd34",
"receivedAt": "2026-08-07T09:14:02.481Z",
"deliveryMs": 412,
"status": "parsed",
"fromAddress": "no-reply@exemple.test",
"fromName": "Exemple",
"toAddress": "signup+7c1e@ab12cd34.inbox.facteur.eu",
"subject": "Votre code de confirmation",
"textBody": "…",
"htmlBody": "…",
"otp": "418302",
"links": ["https://exemple.test/confirm?token=…"],
"codes": [],
"headers": { "message-id": "…", "x-votre-en-tete": "…" },
"extract": {},
"attachments": [
{
"id": "…",
"fileName": "facture-4821.pdf",
"contentType": "application/pdf",
"sizeBytes": 86412,
"checksum": "sha256:9f2c…",
"stored": true
}
],
"inlines": []
}
deliveryMs est le temps que le serveur de réception a mis à accepter le message.
headers, textBody et htmlBody ne sont que dans la lecture d'un seul message, pas
dans les listes. extract porte ce que les extracteurs de l'inbox ont trouvé, voir
Pointeurs.
Chercher par critères
curl -G https://api.facteur.eu/v1/messages \
-H "Authorization: Bearer fk_votre_cle" \
--data-urlencode "inbox=j3k9x2mq" \
--data-urlencode "sentTo=commande-4821" \
--data-urlencode "subject=confirmation"
Chaque critère est un paramètre de l'adresse (-G fait mettre les paramètres dans
l'adresse par curl, et --data-urlencode écrit les caractères spéciaux à votre place).
Les mots que vous cherchez font donc partie de l'adresse de la requête, et peuvent être
écrits dans les journaux d'un proxy ou d'une passerelle entre votre machine et Facteur.
Facteur, lui, ne les écrit ni dans ses journaux d'accès, ni dans ses journaux
d'application, ni dans ses traces, et il n'écrit pas non plus l'adresse de la page de
l'application d'où une recherche est lancée (le navigateur n'indique que le nom du site).
Les journaux d'un composant qui n'est pas à Facteur sont hors de sa portée. Cherchez une
adresse, un sujet ou un code, jamais un secret.
Les critères. sentTo, sentFrom, subject, body, otp, attachmentName.
Ce sont des sous-chaînes, insensibles à la casse. % n'est pas un joker et ne cherche
que le caractère %. otp cherche le code extrait et lui seul : body=882314
trouverait aussi un message qui ne fait que citer ce nombre.
Les bornes. since, hasAttachment et hasOtp rétrécissent la fenêtre au lieu
de s'ajouter aux alternatives.
match. all par défaut : tous les critères doivent correspondre. Passez any
pour qu'un seul suffise. Une borne n'entre jamais dans le any, sinon « depuis hier
ou de Camille » vous rendrait des messages d'avant-hier.
Les longueurs maximales. En caractères : inbox 36, workspace 40, sentTo et
sentFrom 254, subject, body et attachmentName 255, otp 16. Une valeur plus longue
est refusée avec 400 CRITERIA_INVALID plutôt que coupée : coupée, elle trouverait
d'autres messages. Un cursor fait 64 caractères au plus, et un plus
long répond 400 INVALID_CURSOR. Un critère vrai ou faux
(hasOtp, hasAttachment) s'écrit true ou false, et limit et wait sont des
nombres.
Où chercher. inbox pour une inbox, workspace (l'adresse de l'espace) 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, à repasser dans cursor. Reprenez-le tel quel ; il est nul sur la dernière page, ce qui évite
de comparer un compte à une limite.
Le transitoire. Par défaut, la recherche ne rend que les messages parsed.
Passez status=any pour voir aussi ceux qui viennent d'arriver et ceux qui ont
été refusés.
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.
Pièces jointes
attachments liste les vraies pièces jointes. inlines liste les images
référencées par le corps HTML (cid:), le plus souvent un logo de signature. Les deux
sont séparées pour que attachments.length ne change pas selon que l'expéditeur a
ajouté une bannière à sa signature.
Vérifier le contenu sans télécharger le fichier. Comparez le checksum, qui
porte l'empreinte SHA-256 du fichier reçu préfixée par sha256:, à celle de votre
fichier local :
sha256sum facture-4821.pdf
# 9f2c… facture-4821.pdf
Télécharger les octets :
curl -H "Authorization: Bearer fk_votre_cle" \
https://api.facteur.eu/v1/attachments/<id> -o facture.pdf
Les mêmes règles d'accès que pour le reste : une clé qui n'atteint pas l'espace de
travail contenant le message reçoit 404 ATTACHMENT_NOT_FOUND, qu'une pièce jointe
existe réellement sous cet identifiant ou non.
stored peut valoir false. Le fichier est alors décrit (nom, taille, empreinte)
mais ses octets ne sont pas conservés, et GET /attachments/<id> répond
404 ATTACHMENT_NOT_STORED. Le champ reason dit lequel des quatre cas :
reason |
Ce que ça veut dire | Ce que vous faites |
|---|---|---|
PERFORMANCE_ |
L'inbox est réglée pour ne rien stocker | Changez le réglage de l'inbox si vous voulez les octets |
TRIAL_ |
L'inbox est une inbox d'essai, qui ne garde pas les pièces | Utilisez une inbox de votre organisation |
ATTACHMENT_ |
Le fichier dépasse la taille permise par votre plan | Changez de plan, ou testez avec un fichier plus petit |
STORAGE_ |
Le stockage de l'organisation est plein | Libérez de la place ou achetez un pack de stockage |
Limites connues
- 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.