API e automação

API de e-mail temporário: automatize com um script

Três endpoints, sem chave e sem conta: o suficiente para um script abrir um endereço, ler o que chega nele e limpar a própria bagunça depois. Aqui está o loop inteiro — as duas cotas de requisições que decidem o quão rápido você pode ir, e o único prazo que você não pode mudar.

  • Intermediário
  • 22 min de leitura
Uma esteira cinza acionada por uma engrenagem azul, carregando dois envelopes azuis em direção a uma bandeja cinza aberta

Três chamadas, e nada para configurar

A interface inteira são três endpoints sob https://grabmail.io/api/v1, mais um endereço para anexos que os outros já entregam pronto. Não existe uma chamada de criar caixa de entrada, e essa ausência não é uma omissão: um endereço passa a existir quando um e-mail chega nele, então não sobra nada para uma chamada dessas fazer.

ChamadaO que ela respondeO que você passa
GET /mailboxTudo o que está esperando em um endereço, do mais novo para o mais antigo.address, e opcionalmente limit e before
GET /message/{id}Uma mensagem completa: a parte em texto simples, a parte em HTML, e cada anexo com uma URL já pronta.mailbox
DELETE /message/{id}Remove agora, em vez de esperar a janela de retenção acabar.mailbox
GET /attachment/{id}Os bytes de um arquivo, exatamente como chegaram.mailbox

Toda resposta é JSON, inclusive todo erro. Todo horário está em UTC, no formato RFC 3339. Os ids das mensagens são opacos: devolva-os como vieram, nunca tente decifrá-los.

A primeira chamada, e o que um endereço vazio responde

Escolha um nome, coloque um dos domínios públicos atrás dele, e leia. Nada precisa existir antes, e nada é criado só por perguntar.

shell
$ curl -sG https://grabmail.io/api/v1/mailbox \
  --data-urlencode "address=k7fq2m@grabmail.io"
resposta
{
  "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"
    }
  ]
}

Cinco campos, e dois deles são mais interessantes do que parecem:

count
Quantas mensagens estão nesta resposta — não quantas a caixa de entrada guarda. No momento em que você passa limit, esses são dois números diferentes.
next
O cursor para a página depois desta, ou null quando não há nada depois dela. É o id da última mensagem que você acabou de receber, e é por isso que paginar não custa nenhuma chamada extra para descobrir.
messages
Cada entrada já traz subject, from, date, seen, um preview curto do texto, se há uma parte em HTML, e quantos anexos existem.
alias
Um segundo endereço que entrega aqui e não revela nada sobre este. Passe-o a um formulário em vez do endereço real; quem quer que acabe com ele e o digite neste serviço encontra uma caixa de entrada vazia.
address
O endereço tal como foi entendido, em minúsculas e sem espaços nas pontas. Compare com o que você enviou se estiver montando o endereço a partir de partes separadas.

Lendo além das primeiras cinquenta

Por padrão, uma chamada responde com no máximo cinquenta mensagens, e duzentas no limite máximo. Um catch-all movimentado ultrapassa os dois em uma tarde, e a parte que costuma ser mal interpretada é o que vem a seguir — porque não é um número de página.

limit
Quantas retornar nesta chamada, de 1 a 200. Valores fora do intervalo são ajustados em vez de recusados, então limit=5000 silenciosamente te dá 200.
before
O id da mensagem mais antiga que você já tem. Você recebe as que vêm depois dela. Devolva o que quer que a resposta anterior tenha colocado em next.
next
null significa que você chegou ao fim da caixa de entrada. É o único sinal confiável de fim de lista: uma página curta não é um desses sinais, porque uma página só é curta quando o servidor decide que é.
o que uma chamada trouxeo que before= traz de voltanextnovoantigoNão é número de página — é uma posição na lista.
limit limita uma resposta, next indica onde essa resposta parou, e before pede o que vem depois dali.
percorra a caixa de entrada inteira, com a página mais antiga por último
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

Repita enquanto next não for nulo e você terá a caixa de entrada inteira, não importa o quanto ela cresceu. Cada chamada é uma leitura de intervalo sobre um índice, não um offset, então a milésima página custa o mesmo que a primeira.

Um cursor de outra caixa de entrada, ou um que já expirou, não é um erro: você recebe uma página vazia e next: null. Essa é a resposta certa — repetir a página mais nova, em vez disso, entregaria a um script uma mensagem que ele já tinha processado — mas isso também significa que um cursor obsoleto parece exatamente com o fim da lista.

Abrindo uma mensagem, e quando você não precisa

O id da listagem mais a caixa de entrada para a qual a mensagem foi entregue te dão a mensagem em si. Os dois são obrigatórios: um id que vazou de uma caixa de entrada não pode ser usado para ler outra, porque toda busca também é restrita ao endereço.

shell
$ curl -sG https://grabmail.io/api/v1/message/3QK7ZB5M9WVXR2HD4TNFJ0PC6A \
  --data-urlencode "mailbox=k7fq2m@grabmail.io"
resposta
{
  "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
A parte em texto simples. Faça o parsing dela quando ela existir: é estável, não carrega marcação nenhuma, e um código de seis dígitos ali dentro é um código de seis dígitos.
html
A parte em HTML, ou null quando o remetente não enviou uma. Links de confirmação frequentemente existem só aqui.
attachments
Uma entrada por arquivo, cada uma com a URL para buscá-lo já pronta. Uma lista vazia, não null, quando não há nenhum.
expires_at
Quando esta mensagem será apagada, no mesmo formato RFC 3339 que date. Leia esse valor em vez de calculá-lo — a janela de retenção não é uma configuração da qual você possa ter certeza de fora.

Muitas vezes você pode pular esta chamada por completo. A listagem já retorna o assunto, o remetente, a data e um preview curto do texto, o que é suficiente para decidir que uma mensagem não é a que você está esperando. Buscar cada mensagem de uma caixa de entrada só para descobrir que você não queria nenhuma delas é o jeito mais comum de um script ficar lento.

Baixando um arquivo

Todo anexo carrega sua própria url, e o detalhe que vale saber antes de escrever o loop é que ela é um caminho nesta origem, e não um endereço absoluto — já com o parâmetro mailbox incluído. Coloque a origem na frente, busque, e não há mais nada para passar nem para autorizar.

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

Ela sempre responde application/octet-stream com Content-Disposition: attachment, seja qual for o rótulo que o remetente deu ao arquivo. Isso é proposital — repetir de volta o text/html de um desconhecido deixaria um anexo rodar como página nesta origem — então um script que se importa com o tipo o lê a partir do JSON da mensagem, onde ele é dado, não instrução.

A mensagem inteira, arquivos incluídos, tem um teto de 5 MB. O que esse limite significa depois que o base64 faz a dele com um binário é assunto para outro guia — um guia sobre anexos cobre isso.

Duas cotas de requisições, não uma

Esta é a parte que vale a pena conhecer e que é fácil deixar passar: listar um endereço e ler a partir dele são medidos separadamente, porque não são o mesmo risco. Qualquer um que souber um endereço pode fazer polling da sua listagem; ler uma mensagem exige um id, e não há nada para adivinhar.

O que você está chamandoA cotaO que isso significa na prática
GET /mailboxUma requisição por segundo, por endereçoÉ o ritmo de polling pretendido, e nunca é reduzido nesse ritmo. Mais rápido do que isso é recusado, e não ajudaria de qualquer forma.
GET /message/{id}, GET /attachment/{id}, DELETEBem mais generosa, por endereçoEsvazie uma página de mensagens em uma rajada, sem pausar entre elas. É por isso que uma interface consegue abrir uma mensagem no mesmo segundo em que um polling rodou.
Tudo somado1200 requisições por minuto, por clienteVinte endereços com polling uma vez por segundo — bem acima de qualquer automação real, e um freio para um único host varrendo dez mil endereços.

Ao ultrapassar qualquer uma delas você recebe 429 com a espera, em segundos, no cabeçalho Retry-After. Respeite esse valor em vez de recuar por um número inventado por você: é o servidor te dizendo exatamente quando ele vai dizer sim.

durma pelo tempo que te pediram, e nem um segundo a mais
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
}

Como esperar por uma mensagem que ainda não chegou — um prazo em vez de uma contagem de tentativas, e o que fazer quando ele passa — é o assunto de o guia sobre como testar fluxos de verificação. O loop de lá é o mesmo loop que um job agendado precisa.

Apagando, e o piso sob tudo isso

Uma mensagem com a qual você já terminou pode ir embora na hora, em vez de esperar o fim da sua janela de retenção. A chamada é idempotente: apagar o mesmo id duas vezes responde 200 nas duas vezes, então uma requisição repetida nunca parece uma falha.

shell
$ curl -s -X DELETE -G https://grabmail.io/api/v1/message/3QK7ZB5M9WVXR2HD4TNFJ0PC6A \
  --data-urlencode "mailbox=k7fq2m@grabmail.io"
Apague quando já tiver o que veio buscar
Um script que processa uma mensagem e a deixa lá vai processá-la de novo na próxima execução, a não ser que mantenha sua própria lista do que já viu. Apagar é o controle mais barato.
Não conte com isso para privacidade
Entre a chegada e a exclusão, qualquer um que soubesse o endereço poderia tê-la lido. Apagar fecha a janela; não desfaz o que já aconteceu nela.
Tudo desaparece aos 5 dias de qualquer forma
Lida ou não, apagada ou não, uma mensagem desaparece 5 dias depois de chegar. É um limite rígido, não uma configuração, e nenhum parâmetro o estende.

Os slugs para decidir o próximo passo, e o campo que você nunca deve ler

Toda falha é JSON com os mesmos dois campos. error é um slug estável e legível por máquina; message é para humanos e pode ser reformulado a qualquer momento. Decidir sua lógica com base no segundo é como um script quebra num dia em que nada mudou.

Status e slugO que aconteceuO que um script deveria fazer
400 invalid_addressO endereço está faltando, ou não tem o formato de um.Falhe na hora. Nenhuma quantidade de tentativas conserta um erro de digitação.
400 bad_cursorbefore não é um id de mensagem.Falhe na hora, e confira se você está devolvendo next em vez de algo que você mesmo construiu.
404 unknown_domainEsse domínio não é hospedado aqui.Falhe na hora. No seu próprio domínio, isso é o registro MX — veja conectando um domínio.
404 not_foundNão existe essa mensagem naquela caixa de entrada, ou ela já passou da janela de retenção.Trate como algo que já foi embora. É também o que você recebe ao ler um id válido contra a caixa de entrada errada.
429 rate_limitedUma das cotas acima.Durma por Retry-After segundos e continue. Nunca conte isso como uma execução falha.

Um job que esvazia um endereço a cada hora

Junte as peças e um job agendado fica curto. Este aqui pega cada mensagem esperando em um endereço, grava em disco como JSON, e apaga — então a próxima execução começa de uma caixa de entrada vazia e nunca pode processar a mesma mensagem duas vezes.

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

Quatro propriedades valem a pena nomear, porque são elas que separam um job que você pode deixar rodando sozinho de um que você precisa ficar vigiando:

  1. É seguro rodar duas vezes. Duas cópias iniciadas ao mesmo tempo fazem o mesmo trabalho em uma ordem diferente e apagam as mesmas mensagens; a segunda encontra uma caixa de entrada vazia e para.
  2. Ele grava antes de apagar. Se o disco estiver cheio ou o processo for morto, a mensagem ainda está na caixa de entrada na próxima execução. A ordem contrária perde e-mail justamente no dia em que isso importa.
  3. Ele esvazia, em vez de só ler. Como cada mensagem vai embora assim que está gravada com segurança em disco, a próxima listagem retorna as duzentas seguintes — então uma caixa de entrada que recebeu quatrocentas mensagens entre uma execução e outra é esvaziada por completo, não só até as cinquenta mais novas.
  4. Ele falha de forma barulhenta. Um exit diferente de zero é o que faz o cron te enviar a saída. Um job que engole os próprios erros é um job que está quebrado há um mês.

O que esta API não vai fazer por você

Quatro coisas que ela não faz, cada uma de propósito, e nenhuma delas chegando depois. Melhor projetar em volta disso agora do que descobrir por meio de um script que estava funcionando pela metade em silêncio:

Ela nunca envia
Só recebe. Não existe endpoint que coloque uma mensagem na rede, e é por isso que nada aqui pode ser usado para enviar a partir de um endereço que você não possui.
Ela nunca empurra
Sem webhooks e sem callbacks: você pergunta, ela responde. Um agente de IA que preferisse bloquear até o e-mail chegar tem wait_for_message via MCP para isso — veja o guia para agentes.
Ela nunca busca
Não existe parâmetro de consulta para um remetente ou um assunto. A filtragem acontece do seu lado, sobre a listagem — uma das razões pelas quais a listagem traz um preview.
Ela nunca autentica, em um domínio público
Qualquer um que souber o endereço lê a caixa de entrada. O endereço é o segredo inteiro, então trate-o como um: nunca o derive do nome de um cliente, e nunca aponte para um domínio compartilhado nada que você se importasse de ver lido em voz alta.

A resposta para o último ponto é um domínio seu. Aponte o MX dele para smtp.grabmail.io e todo endereço nele responde nesses mesmos três endpoints, sem uma segunda API para aprender e sem chave para rotacionar — e, sob pedido, fechado, de forma que só uma chave bearer o abre. Conectar um domínio leva um único registro DNS.

10 smtp.grabmail.io

Antes de deixar isso rodando

Seis coisas que vale a pena conferir em um job que vai rodar sem que você fique de olho:

  1. Não faça polling mais rápido do que uma vez por segundo por endereço, e respeite Retry-After quando disserem para você esperar.
  2. Siga next até o fim, em vez de presumir que uma chamada é a caixa de entrada inteira.
  3. Decida com base no código de status e em error, nunca em message.
  4. Grave tudo que você precisa manter antes de apagar, e lembre-se de que 5 dias é um piso que você não pode mover.
  5. Ancore o que você extrai ao seu próprio modelo. Um padrão nu de seis dígitos vai combinar alegremente com um ano, um preço, ou um número de pedido que chegou antes.
  6. Presuma que o endereço é público, a não ser que esteja em um domínio que você controla, e coloque tudo o que importa em um que esteja.

Nada disso precisa de conta. Se você crescer além dos domínios públicos, o que muda é o domínio no endereço — as três chamadas acima continuam exatamente como estão.

Perguntas

Preciso de uma chave de API?

Não. Nos domínios públicos não há conta, não há token e nada para cadastrar, e um domínio que você aponta para cá responde nos mesmos endpoints, também sem chave. A única exceção é um domínio que fechamos a pedido, que é lido com um cabeçalho Authorization: Bearer.

Com que velocidade posso fazer polling?

Uma vez por segundo por endereço para a listagem, que é o ritmo pretendido e nunca é reduzido. Ler uma mensagem ou um anexo é medido separadamente, e de forma bem mais generosa, então você pode esvaziar uma página de mensagens em uma rajada. Tudo somado tem um teto de 1200 requisições por minuto por cliente.

Como sei quando li a caixa de entrada inteira?

Quando next volta null. Não deduza isso a partir de uma página curta: o servidor decide o que é uma página, e uma página menor do que limit não é, por si só, o fim.

Posso chamar isso a partir de um navegador?

Sim. As respostas trazem Access-Control-Allow-Origin: *, então uma página em qualquer origem pode chamar os endpoints diretamente, sem nenhum proxy seu no meio do caminho. A autorização aqui nunca é um cookie, então abrir até esse ponto não custa nada.

O que acontece se eu pedir uma mensagem que já expirou?

404 com not_found, exatamente como para um id que nunca existiu. Tudo é apagado 5 dias depois de chegar, lido ou não, e nenhum parâmetro estende esse prazo.

Posso receber um webhook quando um e-mail chegar?

Não — a API REST é de pergunta e resposta, sem callbacks. Se o que você quer é um código que bloqueia até a mensagem chegar, o servidor MCP tem wait_for_message, que faz exatamente isso e é pensado para agentes.

É seguro usar um endereço público em produção?

Só para coisas que não te incomodariam se um desconhecido lesse. Qualquer um que souber o endereço pode ler sua caixa de entrada, pela API assim como pelo site. Para qualquer outra coisa, aponte para cá um domínio que você possui — as chamadas não mudam.

Por que uma mensagem que eu nunca abri aparece como lida?

Porque alguma coisa a abriu. Ler uma mensagem pela API define sua flag seen, e essa flag é compartilhada com todo mundo que está olhando aquele endereço. Um script e uma pessoa observando a mesma caixa de entrada vão continuar se surpreendendo um ao outro, então filtre pelos ids que você já tratou, não por seen.

Eu preciso apagar as mensagens?

Não — tudo expira sozinho depois de 5 dias. Ainda assim vale a pena apagar em um job agendado, porque uma caixa de entrada vazia é o registro mais simples possível do que você já processou.

Experimente enquanto ainda está fresco

Um endereço leva um clique, sem conta e sem cartão. Tudo neste guia funciona nele imediatamente.

Bem-vindo de volta

As suas caixas e os seus domínios, num só lugar.