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.
POST /v1/messages/search celui que vous cherchez, et il attend qu'il arrive
GET /v1/inboxes/<id>/messages les 50 derniers, du plus récent au plus ancien
GET /v1/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 Réglages → Clés d’API et envoyez-la dans un en-tête :
Authorization: Bearer fk_votre_cle
Voir Clés d’API pour ce qu’une clé atteint et comment la faire tourner.
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 -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 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.
- 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, et c’est toujours court. Une attente de trente secondes, la valeur par défaut, traverse un déploiement sans rien voir.
Si vous lisez la liste d’une inbox
GET /v1/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(`/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 séparent un test fiable d’un test capricieux :
- Filtrez sur
to, 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é et stocké, pas encore analysé | Rappelez la route, ou utilisez wait qui ne rend que du parsed |
quota_exceeded | Refusé faute de plan. Rien n’a été stocké ni analysé | Achetez un pack, 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 objet error avec son code.
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": [{ "href": "https://exemple.test/confirm?token=…", "text": "Confirmer" }],
"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,
"url": "/v1/attachments/…"
}
],
"inlines": []
}
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.
Chercher par critères
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, 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.
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.
Le transitoire. Par défaut, la recherche ne rend que les messages parsed.
Passez "status": "any" pour voir aussi ceux qui viennent d’arriver.
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 /v1/attachments/<id> répond
404 ATTACHMENT_NOT_STORED. Le champ reason dit lequel des trois cas :
reason | Ce que ça veut dire | Ce que vous faites |
|---|---|---|
PERFORMANCE_MODE | L’inbox est réglée pour ne rien stocker | Changez le réglage de l’inbox si vous voulez les octets |
ATTACHMENT_TOO_LARGE | Le fichier dépasse la taille permise par votre plan | Changez de plan, ou testez avec un fichier plus petit |
STORAGE_QUOTA_EXCEEDED | L’enveloppe de stockage de l’organisation est pleine | Libérez de la place ou achetez un pack de stockage |
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. - 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.
- Le filtre de la page d’inbox ne voit que les 50 derniers messages. Il travaille
sur ce que
GET /v1/inboxes/{id}/messagesa rendu, donc une valeur présente dans le 51ᵉ message s’affiche comme absente. Passez parPOST /v1/messages/search, qui pagine et cherche dans le corps.
Vérifié le Cet article est faux ou incomplet ?