Aller au contenu
Facteur
ENCommencer

Tapez au moins deux caractères.

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 200 avec 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 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.
  • 503 API_DRAINING veut 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ête Retry-After dit 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 :

  1. Filtrez sur to, pas sur le sujet ni sur « le dernier message ». C’est à cela que sert le sous-adressage, voir Adresses d’inbox.
  2. 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

statusCe que ça veut direCe que vous faites
parsedTout ce qui dérive du corps est renseignéLisez le message
receivedAccepté et stocké, pas encore analyséRappelez la route, ou utilisez wait qui ne rend que du parsed
quota_exceededRefusé 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é. 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.

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 :

reasonCe que ça veut direCe que vous faites
PERFORMANCE_MODEL’inbox est réglée pour ne rien stockerChangez le réglage de l’inbox si vous voulez les octets
ATTACHMENT_TOO_LARGELe fichier dépasse la taille permise par votre planChangez de plan, ou testez avec un fichier plus petit
STORAGE_QUOTA_EXCEEDEDL’enveloppe de stockage de l’organisation est pleineLibérez de la place ou achetez un pack de stockage

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.
  • 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}/messages a rendu, donc une valeur présente dans le 51ᵉ message s’affiche comme absente. Passez par POST /v1/messages/search, qui pagine et cherche dans le corps.

Vérifié le Cet article est faux ou incomplet ?