Ouvrir une boîte

API et automatisation

API email jetable : automatiser une boîte depuis un script

Trois endpoints, aucune clé et aucun compte : de quoi permettre à un script d’ouvrir une adresse, de lire ce qui y arrive et de ranger derrière lui. Voici toute la boucle — les deux budgets de requêtes qui décident de la vitesse à laquelle vous pouvez avancer, et l’unique échéance que vous ne pouvez pas déplacer.

  • Intermédiaire
  • 22 min de lecture
Un tapis roulant gris entraîné par un engrenage bleu, transportant deux enveloppes bleues vers un bac gris ouvert

Trois appels, et rien à mettre en place

Toute l’interface tient en trois endpoints sous https://grabmail.io/api/v1, plus une adresse pour les pièces jointes que les autres vous fournissent déjà construite. Il n’existe pas d’appel créer une boîte, et cette absence n’est pas un oubli : une adresse commence à exister quand un message y arrive, donc un tel appel n’aurait rien à faire.

AppelCe qu’il renvoieCe que vous fournissez
GET /mailboxTout ce qui attend à une adresse, du plus récent au plus ancien.address, et éventuellement limit et before
GET /message/{id}Un message entier : la partie texte brut, la partie HTML, et chaque pièce jointe avec une URL déjà construite.mailbox
DELETE /message/{id}Le supprime immédiatement, au lieu d’attendre la fin de la fenêtre de rétention.mailbox
GET /attachment/{id}Les octets d’un fichier, exactement tels qu’ils sont arrivés.mailbox

Chaque réponse est en JSON, y compris chaque erreur. Chaque horodatage est en UTC, au format RFC 3339. Les identifiants de message sont opaques : renvoyez-les tels quels, ne les décomposez jamais.

Le premier appel, et ce que répond une adresse vide

Choisissez un nom, faites-le suivre d’un des domaines publics, et lisez-le. Rien n’a besoin d’exister au préalable, et rien n’est créé par le simple fait de demander.

shell
$ curl -sG https://grabmail.io/api/v1/mailbox \
  --data-urlencode "address=k7fq2m@grabmail.io"
réponse
{
  "address": "k7fq2m@grabmail.io",
  "alias": "q4v8n2mt7xkd@example.net",
  "count": 1,
  "next": null,
  "messages": [
    {
      "id": "3QK7ZB5M9WVXR2HD4TNFJ0PC6A",
      "from": "no-reply@example.com",
      "from_name": "Example",
      "subject": "Your verification code",
      "preview": "Your code is 481920. It expires in 10 minutes.",
      "has_html": false,
      "date": "2026-08-29T09:14:02Z",
      "seen": false,
      "attachments": 0,
      "expires_at": "2026-09-03T09:14:02Z"
    }
  ]
}

Cinq champs, dont deux sont plus intéressants qu’ils n’y paraissent :

count
Le nombre de messages dans cette réponse — pas le nombre que contient la boîte. Dès que vous passez limit, ce sont deux nombres différents.
next
Le curseur vers la page suivante, ou null s’il n’y a rien après celle-ci. C’est l’id du dernier message qui vient de vous être donné, ce qui explique pourquoi la pagination ne coûte aucun appel supplémentaire à découvrir.
messages
La liste elle-même, du plus récent au plus ancien. Chaque entrée porte déjà subject, from, date, seen, un court preview du texte, si une partie HTML existe, et le nombre de pièces jointes.
alias
Une seconde adresse qui livre ici sans rien révéler de celle-ci. Donnez-la à un formulaire à la place de la véritable adresse ; quiconque finit par la saisir dans ce service y trouve une boîte vide.
address
L’adresse telle qu’elle a été comprise, mise en minuscules et nettoyée des espaces superflus. Comparez-la à ce que vous avez envoyé si vous construisez l’adresse à partir de plusieurs éléments.

Lire au-delà des cinquante premiers

Un appel renvoie par défaut au plus cinquante messages, et deux cents au maximum absolu. Un catch-all actif dépasse les deux en un après-midi, et c’est la suite que les lecteurs devinent le plus souvent mal — parce que ce n’est pas un numéro de page.

limit
Le nombre à renvoyer pour cet appel, de 1 à 200. Les valeurs hors limites sont ramenées dans la plage autorisée plutôt que refusées, donc limit=5000 vous donne silencieusement 200.
before
L’id du message le plus ancien que vous détenez déjà. Vous obtenez ceux qui viennent après. Renvoyez ce que la réponse précédente a placé dans next.
next
null signifie que vous avez atteint la fin de la boîte. C’est le seul signal fiable de fin de liste : une page courte n’en est pas un, car une page n’est courte que lorsque le serveur en a décidé ainsi.
ce qu’un appel a renvoyéce que before= ramènenextrécentancienPas un numéro de page — une position dans une liste.
limit plafonne une réponse, next indique où cette réponse s’est arrêtée, et before demande ce qui se trouve après.
parcourir toute une boîte, la page la plus ancienne en dernier
ADDR="k7fq2m@grabmail.io"
CURSOR=""

while :; do
  PAGE=$(curl -fsG https://grabmail.io/api/v1/mailbox \
           --data-urlencode "address=$ADDR" \
           --data-urlencode "limit=200" \
           ${CURSOR:+--data-urlencode "before=$CURSOR"})

  printf '%s' "$PAGE" | jq -c '.messages[]'

  CURSOR=$(printf '%s' "$PAGE" | jq -r '.next // empty')
  [ -n "$CURSOR" ] || break
  sleep 1
done

Bouclez tant que next n’est pas nul, et vous obtenez la boîte entière, aussi grande soit-elle devenue. Chaque appel est une lecture par plage sur un index plutôt que sur un décalage, donc la millième page coûte ce qu’a coûté la première.

Un curseur venu d’une autre boîte, ou qui a depuis expiré, n’est pas une erreur : vous obtenez une page vide et next: null. C’est la bonne réponse — répéter la page la plus récente donnerait au contraire à un script du courrier qu’il a déjà traité — mais cela signifie aussi qu’un curseur périmé ressemble exactement à la fin de la liste.

Ouvrir un message, et quand ce n’est pas nécessaire

L’id fourni par le listing, plus la boîte à laquelle il a été livré, vous donnent le message lui-même. Les deux sont requis : un id qui aurait fuité d’une boîte ne peut pas servir à en lire une autre, car chaque recherche est aussi limitée à l’adresse.

shell
$ curl -sG https://grabmail.io/api/v1/message/3QK7ZB5M9WVXR2HD4TNFJ0PC6A \
  --data-urlencode "mailbox=k7fq2m@grabmail.io"
réponse
{
  "id": "3QK7ZB5M9WVXR2HD4TNFJ0PC6A",
  "from": "no-reply@example.com",
  "to": "k7fq2m@grabmail.io",
  "subject": "Your verification code",
  "date": "2026-08-29T09:14:02Z",
  "expires_at": "2026-09-03T09:14:02Z",
  "text": "Your code is 481920. It expires in 10 minutes.",
  "html": null,
  "attachments": []
}
text
La partie texte brut. Analysez celle-ci quand elle est présente : elle est stable, ne porte aucun balisage, et un code à six chiffres qui s’y trouve est bien un code à six chiffres.
html
La partie HTML, ou null quand l’expéditeur n’en a pas envoyé. Les liens de confirmation n’existent souvent que là.
attachments
Une entrée par fichier, chacune avec l’URL pour la récupérer déjà construite. Une liste vide, pas null, quand il n’y en a aucun.
expires_at
Le moment où ce message sera supprimé, dans le même format RFC 3339 que date. Lisez-le plutôt que de le calculer — la fenêtre de rétention n’est pas un réglage dont vous pouvez être sûr depuis l’extérieur.

Très souvent, vous pouvez vous passer entièrement de cet appel. Le listing renvoie déjà le sujet, l’expéditeur, la date et un court aperçu du texte, ce qui suffit pour décider qu’un message n’est pas celui que vous attendez. Récupérer chaque message d’une boîte pour découvrir que vous n’en vouliez aucun est la façon la plus courante dont un script devient lent.

Récupérer un fichier

Chaque pièce jointe porte sa propre url, et le détail à connaître avant d’écrire la boucle est qu’il s’agit d’un chemin sur cette origine plutôt que d’une adresse absolue — avec le paramètre mailbox déjà inclus. Faites-le précéder de l’origine, récupérez-le, et il n’y a rien d’autre à fournir ni à autoriser.

shell
ADDR="k7fq2m@grabmail.io"
ID="3QK7ZB5M9WVXR2HD4TNFJ0PC6A"

curl -fsG https://grabmail.io/api/v1/message/$ID \
  --data-urlencode "mailbox=$ADDR" \
| jq -r '.attachments[] | "\(.url)\t\(.filename)"' \
| while IFS=$'\t' read -r path name; do
    curl -fs "https://grabmail.io$path" -o "$name"
  done

Elle répond toujours application/octet-stream avec Content-Disposition: attachment, quel que soit le type indiqué par l’expéditeur pour le fichier. C’est délibéré — renvoyer tel quel le text/html d’un inconnu permettrait à une pièce jointe de s’exécuter comme une page sur cette origine — donc un script qui se soucie du type le lit dans le JSON du message, où c’est une donnée et non une instruction.

Le message entier, fichiers compris, est plafonné à 5 MB. Ce que signifie ce plafond une fois que le base64 est passé par là sur un binaire est un sujet à part entière, traité dans un guide sur les pièces jointes.

Deux budgets de requêtes, pas un seul

Voici la partie à connaître, et qu’il est facile de manquer : lister une adresse et y lire sont mesurés séparément, parce que ce n’est pas le même risque. Quiconque connaît une adresse peut interroger son listing ; lire un message exige un id, et il n’y a rien à deviner.

Ce que vous appelezLe budgetCe que cela signifie en pratique
GET /mailboxUne requête par seconde, par adresseLe rythme d’interrogation prévu, jamais freiné à cette cadence. Aller plus vite est refusé, et n’aurait de toute façon rien changé.
GET /message/{id}, GET /attachment/{id}, DELETEBien plus généreux, par adresseVidez une page de messages en rafale, sans pause entre eux. C’est ce qui permet à une interface d’ouvrir un message la même seconde qu’une interrogation vient d’avoir lieu.
Le tout cumulé1200 requêtes par minute, par clientVingt adresses interrogées une fois par seconde — largement au-delà de toute automatisation réelle, et un frein pour un hôte unique qui parcourrait dix mille adresses.

Au-delà de l’un ou l’autre, vous obtenez 429 avec l’attente, en secondes, dans l’en-tête Retry-After. Respectez-la plutôt que de reculer d’un nombre inventé : c’est le serveur qui vous dit exactement quand il dira oui.

attendre exactement le temps demandé, pas plus
read_box() {
  local wait
  while :; do
    BODY=$(curl -s -D /tmp/gm.h -G https://grabmail.io/api/v1/mailbox \
             --data-urlencode "address=$1")
    grep -qi '^HTTP/[0-9.]* 429' /tmp/gm.h || { printf '%s' "$BODY"; return 0; }
    wait=$(awk 'tolower($1) == "retry-after:" { print $2 + 0 }' /tmp/gm.h)
    sleep "${wait:-1}"
  done
}

Comment attendre un message qui n’est pas encore arrivé — une échéance plutôt qu’un nombre de tentatives, et ce qu’il faut faire une fois celle-ci dépassée — fait l’objet du guide sur les tests de parcours de vérification. La boucle qui s’y trouve est exactement celle dont a besoin une tâche planifiée.

Supprimer, et le plancher qui s’applique à tout

Un message dont vous n’avez plus besoin peut disparaître immédiatement, plutôt que d’attendre la fin de sa fenêtre de rétention. L’appel est idempotent : supprimer deux fois le même id répond 200 les deux fois, donc une requête rejouée ne ressemble jamais à un échec.

shell
$ curl -s -X DELETE -G https://grabmail.io/api/v1/message/3QK7ZB5M9WVXR2HD4TNFJ0PC6A \
  --data-urlencode "mailbox=k7fq2m@grabmail.io"
Supprimez une fois que vous avez ce que vous cherchiez
Un script qui traite un message et le laisse en place le retraitera à l’exécution suivante, à moins de tenir lui-même la liste de ce qu’il a déjà vu. Supprimer revient à tenir cette comptabilité au moindre coût.
Ne comptez pas dessus pour la confidentialité
Entre l’arrivée et la suppression, quiconque connaît l’adresse a pu le lire. Supprimer referme la fenêtre ; cela ne l’efface pas rétroactivement.
Tout disparaît de toute façon à 5 jours
Lu ou non, supprimé ou non, un message disparaît 5 jours après son arrivée. C’est une limite stricte et non un réglage, et aucun paramètre ne permet de la prolonger.

Les slugs sur lesquels s’appuyer, et le champ à ne jamais lire

Chaque échec est un JSON avec les deux mêmes champs. error est un slug stable, lisible par une machine ; message est destiné aux humains et peut être reformulé à tout moment. S’appuyer sur le second est la façon dont un script casse un jour où rien n’a pourtant changé.

Statut et slugCe qui s’est passéCe qu’un script doit faire
400 invalid_addressL’adresse est absente, ou n’a pas la forme d’une adresse.Échouez immédiatement. Aucune tentative supplémentaire ne corrige une faute de frappe.
400 bad_cursorbefore n’est pas un id de message.Échouez immédiatement, et vérifiez que vous renvoyez bien next plutôt que quelque chose que vous avez construit vous-même.
404 unknown_domainCe domaine n’est pas hébergé ici.Échouez immédiatement. Sur votre propre domaine, c’est l’enregistrement MX qui est en cause — voir connecter un domaine.
404 not_foundAucun message de ce type dans cette boîte, ou sa fenêtre de rétention est dépassée.Considérez-le comme disparu. C’est aussi ce que vous obtenez pour un id valide lu avec la mauvaise boîte.
429 rate_limitedL’un des budgets ci-dessus.Attendez le nombre de secondes indiqué par Retry-After, puis continuez. Ne comptez jamais cela comme une exécution échouée.

Une tâche qui vide une adresse toutes les heures

En assemblant les pièces, une tâche planifiée tient en peu de lignes. Celle-ci prend chaque message qui attend à une adresse, l’écrit sur disque au format JSON, puis le supprime — de sorte que l’exécution suivante part d’une boîte vide et ne peut jamais traiter deux fois le même message.

drain.sh
#!/usr/bin/env bash
set -euo pipefail

ADDR="orders@example.com"
OUT="/var/lib/mailsink"
API="https://grabmail.io/api/v1"

mkdir -p "$OUT"

while :; do
  page=$(curl -fsG "$API/mailbox" \
           --data-urlencode "address=$ADDR" \
           --data-urlencode "limit=200")

  ids=$(printf '%s' "$page" | jq -r '.messages[].id')
  [ -n "$ids" ] || break

  for id in $ids; do
    curl -fsG "$API/message/$id" \
      --data-urlencode "mailbox=$ADDR" > "$OUT/$id.json"
    curl -fs -X DELETE -G "$API/message/$id" \
      --data-urlencode "mailbox=$ADDR" > /dev/null
  done

  sleep 1
done
crontab
17 * * * * /usr/local/bin/drain.sh

Quatre propriétés méritent d’être nommées, car ce sont elles qui distinguent une tâche que vous pouvez laisser tourner d’une tâche qu’il faut surveiller :

  1. Elle peut sans risque tourner deux fois. Deux copies lancées en même temps font le même travail dans un ordre différent et suppriment les mêmes messages ; la seconde trouve une boîte vide et s’arrête.
  2. Elle écrit avant de supprimer. Si le disque est plein ou que le processus est tué, le message est toujours dans la boîte à l’exécution suivante. L’ordre inverse perd du courrier précisément le jour où cela compte.
  3. Elle vide plutôt qu’elle ne lit. Comme chaque message disparaît dès qu’il est en sécurité sur le disque, le listing suivant renvoie les deux cents suivants — de sorte qu’une boîte ayant reçu quatre cents messages entre deux exécutions est vidée complètement, et non pas seulement jusqu’aux cinquante plus récents.
  4. Elle échoue bruyamment. C’est un code de sortie non nul qui pousse cron à vous envoyer la sortie. Une tâche qui avale ses propres erreurs est une tâche cassée depuis un mois.

Ce que cette API ne fera pas pour vous

Quatre choses qu’elle ne fait pas, chacune volontairement, et aucune ne viendra s’ajouter plus tard. Mieux vaut s’organiser autour d’elles dès maintenant que de les découvrir à travers un script qui, sans bruit, ne fonctionnait qu’à moitié :

Elle n’envoie jamais
Réception uniquement. Il n’existe aucun endpoint qui mette un message sur le fil, ce qui explique que rien ici ne puisse servir à envoyer depuis une adresse qui ne vous appartient pas.
Elle ne pousse jamais
Aucun webhook et aucun callback : vous demandez, elle répond. Un agent IA qui préférerait bloquer jusqu’à l’arrivée du courrier dispose plutôt de wait_for_message via MCP — voir le guide pour les agents.
Elle ne cherche jamais
Il n’existe aucun paramètre de requête pour un expéditeur ou un sujet. Le filtrage se fait de votre côté, sur le listing — l’une des raisons pour lesquelles le listing porte un aperçu.
Elle n’authentifie jamais, sur un domaine public
Quiconque connaît l’adresse peut lire la boîte. L’adresse est tout le secret, traitez-la donc comme tel : ne la dérivez jamais du nom d’un client, et ne dirigez jamais vers un domaine partagé quoi que ce soit que vous n’aimeriez pas voir lu à voix haute.

La réponse à ce dernier point est un domaine qui vous appartient. Pointez son MX vers smtp.grabmail.io et chaque adresse qui s’y trouve répond sur ces mêmes trois endpoints, sans seconde API à apprendre ni clé à faire tourner — et, sur demande, fermé de sorte que seule une clé Bearer puisse l’ouvrir. Connecter un domaine ne prend qu’un enregistrement DNS.

10 smtp.grabmail.io

Avant de la laisser tourner

Six choses à vérifier sur une tâche qui va tourner sans que vous la surveilliez :

  1. N’interrogez pas plus d’une fois par seconde par adresse, et respectez Retry-After quand on vous demande d’attendre.
  2. Suivez next jusqu’au bout, plutôt que de supposer qu’un seul appel couvre toute la boîte.
  3. Appuyez votre logique sur le code de statut et sur error, jamais sur message.
  4. Écrivez tout ce que vous devez conserver avant de le supprimer, et rappelez-vous que 5 jours est un plancher que vous ne pouvez pas déplacer.
  5. Ancrez tout ce que vous extrayez à votre propre modèle. Un simple motif à six chiffres correspondra tout aussi bien à une année, un prix, ou un numéro de commande arrivé avant.
  6. Considérez l’adresse comme publique sauf si elle se trouve sur un domaine que vous contrôlez, et réservez tout ce qui compte à un domaine qui l’est.

Rien de tout cela n’exige de compte. Si vous dépassez un jour ce que permettent les domaines publics, ce qui change, c’est le domaine dans l’adresse — les trois appels ci-dessus restent exactement les mêmes.

Questions

Faut-il une clé API ?

Non. Sur les domaines publics, il n’y a ni compte, ni jeton, ni rien à enregistrer, et un domaine que vous pointez ici répond sur les mêmes endpoints, sans clé non plus. La seule exception est un domaine que nous avons fermé sur demande, qui se lit alors avec un en-tête Authorization: Bearer.

À quelle vitesse puis-je interroger l’API ?

Une fois par seconde par adresse pour le listing, ce qui est le rythme prévu et n’est jamais freiné. Lire un message ou une pièce jointe est mesuré séparément et de façon bien plus généreuse, donc vous pouvez vider une page de messages en rafale. Le tout cumulé est plafonné à 1200 requêtes par minute et par client.

Comment savoir quand j’ai lu toute la boîte ?

Quand next revient à null. Ne le déduisez pas d’une page courte : c’est le serveur qui décide de ce qu’est une page, et une page plus courte que limit n’est pas en elle-même la fin.

Puis-je appeler ceci depuis un navigateur ?

Oui. Les réponses portent Access-Control-Allow-Origin: *, si bien qu’une page sur n’importe quelle origine peut appeler les endpoints directement, sans proxy de votre part au milieu. L’autorisation n’est ici jamais un cookie, donc ouvrir aussi largement ne coûte rien.

Que se passe-t-il si je demande un message qui a expiré ?

404 avec not_found, exactement comme pour un id qui n’a jamais existé. Tout est supprimé 5 jours après l’arrivée, lu ou non, et aucun paramètre ne permet de prolonger ce délai.

Puis-je recevoir un webhook à l’arrivée d’un message ?

Non — l’API REST fonctionne en question-réponse, sans callback. Si ce qu’il vous faut est du code qui bloque jusqu’à l’arrivée du message, le serveur MCP dispose de wait_for_message, qui fait exactement cela et qui est pensé pour les agents.

Est-il prudent d’utiliser une adresse publique en production ?

Seulement pour des choses qui ne vous dérangeraient pas de voir lues par un inconnu. Quiconque connaît l’adresse peut lire sa boîte, aussi bien via l’API que via le site. Pour tout le reste, pointez ici un domaine qui vous appartient — les appels ne changent pas.

Pourquoi un message que je n’ai jamais ouvert apparaît-il comme lu ?

Parce que quelque chose l’a ouvert. Lire un message via l’API positionne son indicateur seen, et cet indicateur est partagé avec tous ceux qui surveillent cette adresse. Un script et une personne qui surveillent la même boîte n’arrêteront pas de se surprendre l’un l’autre, donc filtrez sur les ids déjà traités plutôt que sur seen.

Dois-je supprimer les messages ?

Non — tout expire de lui-même au bout de 5 jours. Supprimer reste utile dans une tâche planifiée, car une boîte vidée est la trace la plus simple possible de ce que vous avez déjà traité.

Essayez-le pendant que c'est encore frais

Une adresse s'obtient en un clic, sans compte et sans carte. Tout ce que contient ce guide fonctionne avec elle, immédiatement.

Bon retour

Vos boîtes et vos domaines, au même endroit.