3つの呼び出しだけで、準備は不要
インターフェース全体は、https://grabmail.io/api/v1以下にある3つのエンドポイントと、他の呼び出しが用意済みのURLとして渡してくれる添付ファイル用のアドレス1つだけで完結します。メールボックスを作成するという呼び出しはありません。これは抜け漏れではなく、アドレスはメールが届いた瞬間に存在し始めるため、そのような呼び出しにはそもそもやることがないのです。
| 呼び出し | 返す内容 | 渡す値 |
|---|---|---|
GET /mailbox | そのアドレスに届いているものすべてを、新しい順に返します。 | address、任意でlimitとbefore |
GET /message/{id} | 1通のメッセージを丸ごと返します。プレーンテキスト部分、HTML部分、そしてURLが組み立て済みのすべての添付ファイルです。 | mailbox |
DELETE /message/{id} | 保持期間が終わるのを待たずに、今すぐ削除します。 | mailbox |
GET /attachment/{id} | 1つのファイルのバイト列を、届いたとおりに返します。 | mailbox |
すべての応答はJSONで返り、エラーも例外ではありません。すべての日時はRFC 3339形式のUTCです。メッセージIDは不透明な値として扱ってください。そのまま返すだけにして、分解しないでください。
最初の呼び出しと、空のアドレスが返すもの
名前を選び、公開ドメインのいずれかを後ろに付けて、読み取ってみましょう。事前に何かが存在している必要はなく、問い合わせても何かが作成されるわけでもありません。
$ curl -sG https://grabmail.io/api/v1/mailbox \
--data-urlencode "address=k7fq2m@grabmail.io"{
"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"
}
]
}5つのフィールドがあり、そのうち2つは見た目以上に興味深いものです。
count- この応答に含まれるメッセージの数であり、メールボックスが保持している総数ではありません。
limitを渡した瞬間から、この2つは別の数字になります。 next- 次のページのためのカーソルです。その先に何もない場合は
nullになります。これはたった今渡されたメッセージの中で最後のもののIDなので、ページングのために追加の呼び出しをして調べる必要はありません。 messages- 一覧そのものであり、新しい順に並んでいます。各エントリにはすでに
subject、from、date、seen、本文の短いpreviewに加えて、HTML部分の有無と添付ファイルの数が含まれています。 alias- ここに配信されるものの、元のアドレスについては何も明かさない、もう一つのアドレスです。本物のアドレスの代わりにフォームへ渡してください。それを手にした誰かがこのサービスに入力しても、空のメールボックスが見つかるだけです。
address- サーバーが解釈したとおりのアドレスで、小文字化と前後の空白除去が行われています。アドレスを部品から組み立てている場合は、送信した内容と照合してください。
最初の50件を超えて読む
1回の呼び出しはデフォルトで最大50件、上限でも200件までしか返しません。忙しいキャッチオールアドレスなら、その両方を午後のうちに超えてしまいます。そして読者が誤解しがちなのはその先の部分です — これはページ番号ではないからです。
limit- この呼び出しで返す件数で、1から200まで指定できます。範囲外の値は拒否されるのではなく丸められるので、
limit=5000と指定しても、何も言わずに200件が返ってきます。 before- すでに手元にある中で最も古いメッセージのIDです。それより後のものが返ってきます。直前の応答が
nextに入れていた値を、そのまま渡し返してください。 nextnullはメールボックスの終端に達したことを意味します。これがリストの終わりを示す唯一の確実な合図です。ページが短いこと自体はその合図にはなりません。ページが短くなるのは、サーバーがそう決めたときだけだからです。
limitは1回の応答の上限を決め、nextはその応答がどこで止まったかを示し、beforeはその先にあるものを求めます。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
donenextがnullでない間ループすれば、どれほど大きく育っていても、メールボックス全体を取得できます。各呼び出しはオフセットではなくインデックス上の範囲読み取りなので、1000ページ目のコストも1ページ目と変わりません。
別のメールボックスのカーソルや、すでに期限切れになったカーソルを渡してもエラーにはなりません。空のページとnext: nullが返ってくるだけです。これは正しい挙動です — 代わりに最新のページを繰り返し返してしまうと、スクリプトはすでに処理済みのメールを再び受け取ることになるからです — ただし、これは古くなったカーソルが、リストの終端とまったく同じに見えてしまうことも意味します。
1通のメッセージを開くとき、開かなくてよいとき
一覧から得たIDと、それが配信されたメールボックスを組み合わせると、メッセージ本体が取得できます。どちらも必須です。あるメールボックスから漏れたIDを使って別のメールボックスを読むことはできません。すべての検索はアドレス単位でも絞り込まれているからです。
$ curl -sG https://grabmail.io/api/v1/message/3QK7ZB5M9WVXR2HD4TNFJ0PC6A \
--data-urlencode "mailbox=k7fq2m@grabmail.io"{
"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- プレーンテキスト部分です。これがあるならこちらを解析してください。安定していて、マークアップを含まず、6桁のコードが書かれていればそれはそのまま6桁のコードです。
html- HTML部分です。送信者がHTMLを送っていない場合は
nullになります。確認用のリンクは、ここにしか存在しないことがよくあります。 attachments- ファイル1つにつき1エントリで、それぞれに取得用のURLが組み立て済みで入っています。添付がない場合は
nullではなく空のリストになります。 expires_at- このメッセージが削除される日時で、
dateと同じRFC 3339形式です。自分で計算するのではなく、この値を読んでください — 保持期間は、外部から確実に把握できる設定ではありません。
多くの場合、この呼び出しは完全に省略できます。一覧にはすでに件名、送信者、日時、そして本文の短いプレビューが含まれており、そのメッセージが待っているものではないと判断するにはそれで十分です。メールボックス内のすべてのメッセージを取得してから、結局どれも探していたものではなかったと分かる — これがスクリプトを遅くする最もありがちな原因です。
ファイルを取り出す
すべての添付ファイルは、それぞれ自身のurlを持っています。ループを書く前に知っておく価値があるのは、これが絶対アドレスではなく、この配信元におけるパスだという点です — mailboxパラメータもすでに含まれています。配信元を前に付けて取得するだけで、他に渡すものも認可するものもありません。
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送信者がファイルにどんなラベルを付けていても、常にapplication/octet-streamをContent-Disposition: attachment付きで返します。これは意図的な設計です — 見知らぬ相手のtext/htmlをそのまま返してしまうと、添付ファイルがこの配信元上でページとして実行されかねません — そのため、種類が重要なスクリプトは、指示ではなくデータとして扱われるメッセージのJSONから、それを読み取ってください。
メッセージ全体は、ファイルも含めて5 MBが上限です。バイナリがbase64を経た後、その上限が実際に何を意味するのかは、それ自体が一つのテーマであり、添付ファイルについてのガイドで扱っています。
レート制限は1つではなく2つ
ここは知っておく価値があるのに見落とされがちな部分です。アドレスの一覧取得と、そこからの読み取りは別々に計測されています。この2つは同じリスクではないからです。アドレスを知っている人なら誰でもその一覧をポーリングできますが、メッセージを読むにはIDが必要で、推測できる余地はありません。
| 呼び出す対象 | 制限 | 実際の意味 |
|---|---|---|
GET /mailbox | アドレスごとに1秒に1リクエスト | 想定されているポーリングの頻度であり、このペースで絞られることはありません。これより速くしても拒否されるだけで、何の助けにもなりません。 |
GET /message/{id}、GET /attachment/{id}、DELETE | アドレスごとに、はるかに余裕のある枠 | 1ページ分のメッセージを、間を空けずに一気に処理できます。ポーリングを実行したのと同じ秒に、インターフェースがメッセージを開けるのはこのためです。 |
| すべてを合計すると | クライアントごとに1分間で1200リクエスト | 20個のアドレスを1秒に1回ポーリングする — 実際の自動化なら余裕を持って収まる量であり、同時に1つのホストが1万個のアドレスを渡り歩くことには歯止めがかかります。 |
これらのいずれかを超えると429が返り、待つべき秒数がRetry-Afterヘッダーに入っています。自分で考えた数値でバックオフするのではなく、これに従ってください。これは、サーバーがいつになれば許可するかを正確に教えてくれているものです。
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
}まだ届いていないメッセージをどう待つか — 再試行回数ではなく期限で考えること、そしてその期限が過ぎたときにどうするか — については、確認フローをテストするガイドで扱っています。そこにあるループは、スケジュール実行されるジョブが必要とするループと同じものです。
削除、そしてすべての土台にある下限
用が済んだメッセージは、保持期間が終わるのを待たずに、すぐに消すことができます。この呼び出しはべき等です。同じIDを2回削除しても、どちらも200が返るので、リトライされたリクエストが失敗のように見えることはありません。
$ curl -s -X DELETE -G https://grabmail.io/api/v1/message/3QK7ZB5M9WVXR2HD4TNFJ0PC6A \
--data-urlencode "mailbox=k7fq2m@grabmail.io"- 目的のものを手に入れたら削除する
- メッセージを処理してもそのまま残しておくスクリプトは、自分で見た内容のリストを保持していない限り、次の実行でもう一度そのメッセージを処理してしまいます。削除しておくほうが、記録管理としては安上がりです。
- プライバシーの担保として頼らない
- 到着から削除までの間、アドレスを知っている人なら誰でもそれを読めた可能性があります。削除はその窓を閉じるだけであり、なかったことにはできません。
- 何であれ5日で消える
- 既読でも未読でも、削除済みでもそうでなくても、メッセージは届いてから5日後には消えます。これは設定ではなく絶対的な上限であり、どんなパラメータを使っても延長はできません。
分岐に使うべきスラッグと、決して読んではいけないフィールド
すべての失敗は、同じ2つのフィールドを持つJSONで返ってきます。errorは安定した機械可読のスラッグで、messageは人間向けであり、いつでも文言が変わる可能性があります。後者で分岐すると、何も変わっていないはずの日にスクリプトが壊れることになります。
| ステータスとスラッグ | 起きたこと | スクリプトが取るべき対応 |
|---|---|---|
400 invalid_address | アドレスが指定されていないか、アドレスの形をしていません。 | 即座に失敗させてください。どれだけリトライしても、タイプミスは直りません。 |
400 bad_cursor | beforeがメッセージIDになっていません。 | 即座に失敗させ、自分で組み立てた値ではなくnextを渡し返しているか確認してください。 |
404 unknown_domain | そのドメインはここではホストされていません。 | 即座に失敗させてください。自分のドメインの場合、これはMXレコードの問題です — ドメインを接続するを参照してください。 |
404 not_found | そのメールボックスにそのメッセージは存在しないか、保持期間をすでに過ぎています。 | なくなったものとして扱ってください。有効なIDを間違ったメールボックスに対して読み取った場合も、同じ結果になります。 |
429 rate_limited | 上記のいずれかの制限に達しています。 | Retry-After秒だけ待ってから続行してください。これを失敗した実行としてカウントしないでください。 |
1時間ごとにアドレスを空にするジョブ
これまでのピースを組み合わせれば、スケジュール実行するジョブは短く書けます。ここで示すものは、アドレスに届いているすべてのメッセージを取得し、JSONとしてディスクに書き出してから削除します — そのため次の実行は空のメールボックスから始まり、同じメッセージを2度処理することは決してありません。
#!/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
done17 * * * * /usr/local/bin/drain.sh名前を付けておく価値のある4つの性質があります。放っておけるジョブと、見張っていなければならないジョブを分けるのは、この違いだからです。
- 2回実行しても問題ない。同時に起動した2つのコピーは、同じ作業を違う順序で行い、同じメッセージを削除しようとします。後から動いたほうは空のメールボックスを見つけて、そこで止まります。
- 削除より先に書き込む。ディスクが満杯になったり、プロセスが強制終了させられたりしても、次の実行時にはメッセージがまだメールボックスに残っています。順序を逆にすると、それが重要になるまさにその日に、メールを失うことになります。
- 読むのではなく、空にする。各メッセージは安全にディスクへ書き込まれ次第すぐに消えるため、次の一覧取得ではその次の200件が返ってきます — そのため、実行の合間に400件のメッセージを受け取ったメールボックスでも、最新の50件を残すことなく完全に空になります。
- 失敗したら、はっきり分かるようにする。終了コードが0以外であることが、cronに出力を送らせる仕組みです。自分のエラーを飲み込んでしまうジョブは、1か月前から壊れていても気づかれません。
このAPIがしてくれないこと
しないことが4つあり、そのどれもが意図的なもので、あとから追加されることもありません。静かに中途半端に動き続けているスクリプトから発見するより、今のうちに設計へ織り込んでおくほうがよいでしょう。
- 送信は一切しない
- 受信専用です。メッセージを送り出すエンドポイントは存在しません。だからこそ、ここにあるものを使って、自分が所有していないアドレスから送信することはできません。
- プッシュは一切しない
- Webhookもコールバックもありません。あなたが尋ね、それが答えるだけです。メールが届くまでブロックしていたいAIエージェントには、代わりにMCP経由の
wait_for_messageがあります — エージェント向けのガイドを参照してください。 - 検索は一切しない
- 送信者や件名を指定するクエリパラメータはありません。絞り込みは、一覧取得の結果に対して自分の側で行います — 一覧にプレビューが含まれているのは、これが理由の一つです。
- 公開ドメインでは、認証は一切行わない
- アドレスを知っている人なら誰でも、そのメールボックスを読めます。アドレスそのものが秘密のすべてなので、そのつもりで扱ってください。顧客の名前からアドレスを導き出したりせず、他人に読み上げられて困るようなものは、共有ドメインに向けないでください。
最後の項目への答えは、自分自身のドメインを持つことです。そのMXレコードをsmtp.grabmail.ioに向ければ、そのドメイン上のすべてのアドレスが、同じこの3つのエンドポイントで答えるようになります。覚えるべき2つ目のAPIも、ローテーションすべきキーもありません — さらに要望に応じて、ベアラーキーを持つ者だけが開ける非公開状態にすることもできます。ドメインを接続するには、DNSレコード1件だけで済みます。
10 smtp.grabmail.io
動かしっぱなしにする前に
誰にも見られずに動き続けるジョブについて、確認しておく価値のある6つのことです。
- 1アドレスにつき1秒に1回より速くポーリングしない。待つように言われたときは
Retry-Afterに従ってください。 nextを最後までたどる。1回の呼び出しでメールボックス全体が返ってくると思い込まないでください。- ステータスコードと
errorで分岐する。messageでは絶対に分岐しないでください。 - 残しておく必要があるものは、削除する前に書き出しておく。5日という期限は動かせない下限であることを忘れないでください。
- 抽出するものは、必ず自分のテンプレートに固定して照合する。ただの6桁のパターンは、先に届いた年号や価格、注文番号にも平気でマッチしてしまいます。
- 自分が管理していないドメインのアドレスは、公開されているものとみなす。重要なものは、自分が管理しているドメインに置いてください。
どれもアカウントを必要としません。公開ドメインでは足りなくなったとしても、変わるのはアドレスの中のドメイン部分だけです — 上に挙げた3つの呼び出しは、そのままの形で使えます。
質問
APIキーは必要ですか?
いいえ。公開ドメインでは、アカウントもトークンも登録も不要で、自分のドメインをここに向けた場合も、キーなしで同じエンドポイントが使えます。唯一の例外は、要望に応じて非公開にしたドメインで、その場合はAuthorization: Bearerヘッダーで読み取ります。
どのくらいの速さでポーリングできますか?
一覧取得は、アドレスごとに1秒に1回が想定されているリズムで、その速さで絞られることはありません。メッセージや添付ファイルの読み取りは別に計測され、はるかに余裕があるため、1ページ分のメッセージを一気に処理できます。すべてを合計すると、クライアントごとに1分間で1200リクエストが上限です。
メールボックス全体を読み終えたことは、どうすれば分かりますか?
nextがnullで返ってきたときです。短いページからそれを推測しないでください。ページの区切り方を決めるのはサーバー側であり、limitより短いページであること自体は、終端を意味しません。
ブラウザから呼び出すことはできますか?
できます。応答にはAccess-Control-Allow-Origin: *が付いているため、どのオリジンのページでも、間に自分のプロキシを挟むことなくエンドポイントを直接呼び出せます。ここでの認可はクッキーに依存しないため、そこまで開放してもコストはかかりません。
期限切れになったメッセージを取得しようとすると、どうなりますか?
存在したことのないIDの場合とまったく同じように、404とnot_foundが返ります。すべてのメッセージは、既読かどうかにかかわらず、届いてから5日後に削除され、どんなパラメータを使ってもそれを延長することはできません。
メールが届いたときにWebhookを受け取ることはできますか?
できません — REST APIは、尋ねて答えるだけの仕組みで、コールバックはありません。メッセージが届くまでブロックするコードが欲しいのであれば、MCPサーバーにwait_for_messageがあり、まさにそれを行うために用意されていて、エージェント向けのものです。
本番環境で公開アドレスを使っても安全ですか?
見知らぬ人に読まれても構わない内容に限ってです。アドレスを知っている人なら誰でも、サイトからと同じようにAPI経由でもそのメールボックスを読めます。それ以外のものには、自分が所有するドメインをここに向けてください — 呼び出し自体は変わりません。
一度も開いていないメッセージが既読になっているのはなぜですか?
何かがそれを開いたからです。APIでメッセージを読むとseenフラグが立ち、そのフラグはそのアドレスを見ている全員に共有されます。同じメールボックスをスクリプトと人間の両方が見ていると、互いに驚かされ続けることになります。そのため、絞り込みはseenではなく、すでに処理したIDを基準にしてください。
メッセージは削除しなければなりませんか?
いいえ — すべてのメッセージは5日後に自動的に期限切れになります。とはいえ、スケジュール実行するジョブでは削除しておく価値があります。空になったメールボックスは、すでに処理したものを示す最もシンプルな記録になるからです。


