Tiga panggilan, dan tidak ada yang perlu disiapkan
Keseluruhan antarmukanya adalah tiga endpoint di bawah https://grabmail.io/api/v1, ditambah satu alamat untuk lampiran yang sudah disediakan siap pakai oleh endpoint lainnya. Tidak ada panggilan buat kotak surat, dan ketiadaannya bukan sebuah kelalaian: sebuah alamat mulai ada begitu surat tiba di sana, jadi tidak ada yang bisa dikerjakan oleh panggilan semacam itu.
| Panggilan | Yang dijawabnya | Yang Anda kirim |
|---|---|---|
GET /mailbox | Semua yang menunggu di sebuah alamat, yang terbaru lebih dulu. | address, dan secara opsional limit serta before |
GET /message/{id} | Satu pesan secara lengkap: bagian plain-text, bagian HTML, dan setiap lampiran dengan URL yang sudah jadi. | mailbox |
DELETE /message/{id} | Menghapusnya sekarang juga, alih-alih menunggu jendela retensi habis. | mailbox |
GET /attachment/{id} | Byte dari satu file, persis seperti saat tiba. | mailbox |
Setiap jawaban berbentuk JSON, termasuk setiap error. Setiap waktu memakai UTC dalam bentuk RFC 3339. Id pesan bersifat buram: kembalikan begitu saja, jangan pernah membongkarnya.
Panggilan pertama, dan jawaban dari sebuah alamat kosong
Pilih sebuah nama, tempelkan salah satu domain publik di belakangnya, lalu baca. Tidak ada yang perlu ada duluan, dan tidak ada yang tercipta hanya karena bertanya.
$ 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"
}
]
}Lima field, dan dua di antaranya lebih menarik daripada kelihatannya:
count- Berapa banyak pesan ada di jawaban ini — bukan berapa banyak yang disimpan kotak suratnya. Begitu Anda mengirim
limit, keduanya jadi dua angka yang berbeda. next- Cursor untuk halaman setelah ini, atau
nullkalau tidak ada lagi setelahnya. Ini adalah id dari pesan terakhir yang baru saja Anda terima, itulah sebabnya paging tidak membutuhkan panggilan tambahan untuk mengetahuinya. messages- Daftarnya sendiri, yang terbaru lebih dulu. Setiap entri sudah membawa
subject,from,date,seen,previewsingkat dari teksnya, apakah ada bagian HTML, dan berapa banyak lampirannya. alias- Alamat kedua yang mengantar ke sini juga, tapi tidak membocorkan apa pun soal alamat yang ini. Berikan ini ke formulir, bukan alamat aslinya; siapa pun yang akhirnya memilikinya dan mengetikkannya ke layanan ini akan menemukan kotak surat yang kosong.
address- Alamat sebagaimana dipahami sistem: sudah diubah ke huruf kecil dan di-trim. Bandingkan dengan yang Anda kirim kalau Anda menyusun alamatnya dari beberapa bagian.
Membaca melewati lima puluh pesan pertama
Satu panggilan menjawab dengan paling banyak lima puluh pesan secara default, dan dua ratus di titik paling maksimalnya. Sebuah catch-all yang sibuk bisa melewati keduanya dalam satu sore, dan bagian yang sering ditebak salah oleh pembaca adalah apa yang terjadi selanjutnya — karena ini bukan nomor halaman.
limit- Berapa banyak yang dikembalikan dalam panggilan ini, 1 sampai 200. Nilai di luar jangkauan itu dipangkas, bukan ditolak, jadi
limit=5000diam-diam memberi Anda 200. before- Id dari pesan paling lama yang sudah Anda pegang. Anda akan mendapat yang setelahnya. Kirim balik apa pun yang ditaruh jawaban sebelumnya di
next. nextnullberarti Anda sudah mencapai akhir kotak surat. Ini satu-satunya sinyal akhir-daftar yang bisa diandalkan: halaman yang pendek bukanlah sinyal itu, karena sebuah halaman hanya pendek kalau server yang memutuskannya begitu.
limit membatasi satu jawaban, next menamai di mana jawaban itu berhenti, dan before meminta apa yang ada setelahnya.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
doneBuat loop selama next bukan null, dan Anda akan mendapat seluruh isi kotak surat, sebesar apa pun ia bertambah. Setiap panggilan adalah pembacaan rentang pada sebuah index, bukan offset, jadi halaman keseribu berbiaya sama dengan halaman pertama.
Sebuah cursor dari kotak surat lain, atau yang sudah kedaluwarsa, bukanlah sebuah error: Anda akan mendapat halaman kosong dan next: null. Itu jawaban yang benar — mengulang halaman terbaru sebagai gantinya justru akan memberi skrip itu surat yang sudah pernah diprosesnya — tapi ini juga berarti sebuah cursor basi akan terlihat persis seperti akhir daftar.
Membuka satu pesan, dan kapan Anda tidak perlu melakukannya
Id dari daftar tadi, ditambah kotak surat tempat pesan itu dikirim, memberi Anda pesannya sendiri. Keduanya wajib: sebuah id yang bocor dari satu kotak masuk tidak bisa dipakai untuk membaca kotak masuk lain, karena setiap pencarian juga dibatasi lingkupnya pada alamat tersebut.
$ 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- Bagian plain-text-nya. Urai (parse) bagian ini kalau tersedia: bentuknya stabil, tidak membawa markup apa pun, dan sebuah kode enam digit di dalamnya ya kode enam digit itu sendiri.
html- Bagian HTML-nya, atau
nullkalau pengirim tidak menyertakannya. Tautan konfirmasi sering kali hanya ada di sini. attachments- Satu entri per file, masing-masing dengan URL untuk mengambilnya yang sudah jadi. Daftar kosong, bukan
null, kalau tidak ada satu pun. expires_at- Kapan pesan ini akan dihapus, dalam bentuk RFC 3339 yang sama seperti
date. Baca nilai ini alih-alih menghitungnya sendiri — jendela retensi bukan sebuah pengaturan yang bisa Anda pastikan dari luar.
Sangat sering Anda bisa melewatkan panggilan ini sama sekali. Daftarnya sudah mengembalikan subjek, pengirim, tanggal, dan preview singkat dari teksnya, yang sudah cukup untuk memutuskan bahwa sebuah pesan bukan yang sedang Anda tunggu. Mengambil setiap pesan dalam sebuah kotak surat hanya untuk mendapati Anda tidak menginginkan satu pun dari mereka adalah cara paling umum sebuah skrip menjadi lambat.
Mengambil sebuah file
Setiap lampiran membawa url-nya sendiri, dan hal yang penting diketahui sebelum menulis loop-nya: itu adalah sebuah path pada origin ini, bukan alamat lengkap — dengan parameter mailbox yang sudah ada di dalamnya. Tinggal tambahkan origin-nya di depan, ambil, dan tidak ada lagi yang perlu dikirim atau diotorisasi.
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"
doneIa selalu menjawab dengan application/octet-stream beserta Content-Disposition: attachment, apa pun label yang diberikan pengirim pada file itu. Ini disengaja — mengembalikan text/html milik orang asing apa adanya akan membuat sebuah lampiran bisa berjalan sebagai halaman di origin ini — jadi skrip yang peduli pada tipe filenya membaca itu dari JSON pesannya, di mana ia adalah data, bukan instruksi.
Keseluruhan pesan, berikut semua filenya, dibatasi hingga 5 MB. Apa arti batas itu setelah base64 selesai mengolah sebuah file biner adalah topik tersendiri, dan ada panduan tentang lampiran untuk itu.
Dua kuota laju, bukan satu
Inilah bagian yang penting diketahui dan mudah terlewat: daftar isi sebuah alamat dan pembacaan dari alamat itu dijatah secara terpisah, karena keduanya bukan risiko yang sama. Siapa pun yang tahu sebuah alamat bisa melakukan polling pada daftarnya; membaca sebuah pesan membutuhkan id, dan tidak ada yang bisa ditebak.
| Yang Anda panggil | Kuota | Artinya dalam praktik |
|---|---|---|
GET /mailbox | Satu request per detik, per alamat | Ritme polling yang memang dimaksudkan, dan tidak pernah diperlambat pada kecepatan itu. Lebih cepat dari itu akan ditolak, dan toh tidak akan membantu. |
GET /message/{id}, GET /attachment/{id}, DELETE | Jauh lebih longgar, per alamat | Kuras satu halaman pesan sekaligus tanpa jeda di antaranya. Inilah sebabnya sebuah antarmuka bisa membuka sebuah pesan di detik yang sama saat sebuah polling baru saja berjalan. |
| Semuanya, dijumlahkan | 1200 request per menit, per client | Dua puluh alamat yang di-polling sekali per detik — jauh melampaui otomatisasi nyata mana pun, dan sekaligus jadi rem bagi satu host yang mencoba menyusuri sepuluh ribu alamat. |
Melewati salah satu dari itu, Anda akan mendapat 429 dengan waktu tunggunya, dalam detik, di header Retry-After. Patuhi angka itu, bukannya mundur dengan angka karangan sendiri: itu adalah server yang memberi tahu Anda persis kapan ia akan bilang ya.
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
}Bagaimana menunggu sebuah pesan yang belum tiba — sebuah tenggat waktu, bukan hitungan percobaan ulang, dan apa yang harus dilakukan saat tenggat itu terlewati — adalah topik dari panduan menguji alur verifikasi. Loop di sana adalah loop yang sama yang dibutuhkan sebuah job terjadwal.
Menghapus, dan batas dasar di bawah semuanya
Pesan yang sudah selesai Anda pakai bisa langsung dihapus, tidak perlu menunggu jendela retensinya habis. Panggilan ini idempoten: menghapus id yang sama dua kali tetap menjawab 200 kedua-duanya, jadi sebuah request yang dicoba ulang tidak pernah terlihat seperti kegagalan.
$ curl -s -X DELETE -G https://grabmail.io/api/v1/message/3QK7ZB5M9WVXR2HD4TNFJ0PC6A \
--data-urlencode "mailbox=k7fq2m@grabmail.io"- Hapus begitu Anda sudah dapat yang dicari
- Skrip yang memproses sebuah pesan lalu membiarkannya tetap di sana akan memprosesnya lagi pada run berikutnya, kecuali skrip itu menyimpan daftarnya sendiri tentang apa yang sudah pernah dilihatnya. Menghapus adalah cara pembukuan yang lebih murah.
- Jangan mengandalkannya untuk privasi
- Antara kedatangan dan penghapusan, siapa pun yang tahu alamatnya bisa saja sudah membacanya. Menghapus menutup jendela itu; itu tidak membatalkan apa yang sudah terjadi.
- Semuanya lenyap di hari ke-5 apa pun yang terjadi
- Dibaca atau tidak, dihapus atau tidak, sebuah pesan lenyap 5 hari setelah kedatangannya. Ini batas keras, bukan sebuah pengaturan, dan tidak ada parameter yang bisa memperpanjangnya.
Slug yang jadi dasar percabangan, dan field yang jangan pernah dibaca
Setiap kegagalan berbentuk JSON dengan dua field yang sama. error adalah slug stabil yang bisa dibaca mesin; message untuk manusia dan bisa diubah kata-katanya kapan saja. Menjadikan yang kedua sebagai dasar percabangan adalah cara sebuah skrip menjadi rusak pada hari ketika tidak ada apa pun yang berubah.
| Status dan slug | Yang terjadi | Yang harus dilakukan skrip |
|---|---|---|
400 invalid_address | Alamatnya tidak ada, atau bentuknya bukan seperti sebuah alamat. | Gagalkan seketika. Percobaan ulang sebanyak apa pun tidak akan memperbaiki salah ketik. |
400 bad_cursor | before bukan sebuah id pesan. | Gagalkan seketika, dan periksa apakah Anda mengirim balik next, bukan sesuatu yang Anda susun sendiri. |
404 unknown_domain | Domain itu tidak di-hosting di sini. | Gagalkan seketika. Pada domain milik Anda sendiri, ini soal record MX-nya — lihat menghubungkan domain. |
404 not_found | Tidak ada pesan seperti itu di kotak surat tersebut, atau ia sudah melewati jendela retensinya. | Perlakukan sebagai sudah lenyap. Ini juga yang akan Anda dapat untuk sebuah id valid yang dibaca dengan kotak surat yang salah. |
429 rate_limited | Salah satu dari kuota di atas. | Tidur selama Retry-After detik lalu lanjutkan. Jangan pernah menghitungnya sebagai run yang gagal. |
Job yang menguras sebuah alamat tiap jam
Satukan semua bagiannya dan sebuah job terjadwal jadi singkat. Yang ini mengambil setiap pesan yang menunggu di sebuah alamat, menuliskannya ke disk sebagai JSON, lalu menghapusnya — jadi run berikutnya selalu mulai dari kotak surat kosong dan tidak akan pernah memproses pesan yang sama dua kali.
#!/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.shAda empat sifat yang layak disebutkan, karena itulah yang membedakan job yang bisa dibiarkan berjalan sendiri dari job yang harus terus diawasi:
- Aman dijalankan dua kali. Dua salinan yang dijalankan bersamaan mengerjakan pekerjaan yang sama dengan urutan berbeda dan menghapus pesan yang sama; yang kedua akan menemukan kotak surat kosong dan berhenti.
- Ia menulis sebelum menghapus. Kalau disknya penuh atau prosesnya di-kill, pesannya masih ada di kotak surat pada run berikutnya. Urutan sebaliknya akan kehilangan surat justru di hari saat itu benar-benar penting.
- Ia menguras, bukan sekadar membaca. Karena setiap pesan langsung pergi begitu aman tersimpan di disk, daftar berikutnya akan mengembalikan dua ratus pesan selanjutnya — jadi kotak surat yang menerima empat ratus pesan di antara dua run akan dikosongkan sepenuhnya, bukan cuma disisakan lima puluh yang terbaru.
- Ia gagal dengan berisik. Exit dengan kode bukan nol itulah yang membuat cron mengirimkan output-nya kepada Anda. Job yang menelan errornya sendiri adalah job yang sudah rusak selama sebulan.
Yang tidak akan dilakukan API ini untuk Anda
Empat hal yang tidak dilakukannya, masing-masing memang disengaja dan tidak satu pun akan ditambahkan nanti. Lebih baik merancang sistem Anda dengan mempertimbangkan ini sekarang daripada menemukannya lewat sebuah skrip yang diam-diam sudah setengah tidak berfungsi dengan benar:
- Ia tidak pernah mengirim
- Hanya menerima. Tidak ada endpoint yang mengirimkan pesan ke jaringan, itulah sebabnya tidak ada apa pun di sini yang bisa dipakai untuk mengirim dari alamat yang bukan milik Anda.
- Ia tidak pernah mendorong
- Tidak ada webhook dan tidak ada callback: Anda bertanya, ia menjawab. AI agent yang lebih memilih blocking sampai suratnya tiba punya
wait_for_messagelewat MCP sebagai gantinya — lihat panduan untuk agent. - Ia tidak pernah mencari
- Tidak ada parameter query untuk pengirim atau subjek. Penyaringan terjadi di sisi Anda, di atas daftarnya — salah satu alasan kenapa daftar itu membawa preview.
- Ia tidak pernah mengautentikasi, di domain publik
- Siapa pun yang tahu alamatnya bisa membaca kotak suratnya. Alamat itu adalah keseluruhan rahasianya, jadi perlakukan begitu: jangan pernah menurunkannya dari nama seorang pelanggan, dan jangan pernah arahkan apa pun yang keberatan Anda bacakan keras-keras ke sebuah domain publik yang dipakai bersama.
Jawaban untuk yang terakhir itu adalah sebuah domain milik Anda sendiri. Arahkan MX-nya ke smtp.grabmail.io dan setiap alamat di domain itu akan terjawab lewat tiga endpoint yang sama ini, tanpa API kedua yang perlu dipelajari dan tanpa key yang perlu dirotasi — dan, atas permintaan, bisa ditutup sehingga hanya bearer key yang membukanya. Menghubungkan domain hanya butuh satu record DNS.
10 smtp.grabmail.io
Sebelum Anda membiarkannya berjalan sendiri
Enam hal yang layak diperiksa pada sebuah job yang akan berjalan tanpa Anda awasi:
- Jangan polling lebih cepat dari sekali per detik per alamat, dan patuhi
Retry-Aftersaat Anda diminta menunggu. - Ikuti
nextsampai habis, alih-alih menganggap satu panggilan sudah mewakili seluruh kotak surat. - Buat percabangan berdasarkan kode status dan
error, jangan pernah berdasarkanmessage. - Tulis apa pun yang perlu Anda simpan sebelum menghapusnya, dan ingat bahwa 5 hari adalah batas dasar yang tidak bisa digeser.
- Kaitkan apa pun yang Anda ekstrak dengan template Anda sendiri. Pola enam digit polos akan dengan senang hati cocok dengan sebuah tahun, harga, atau nomor pesanan yang tiba lebih dulu.
- Anggap alamatnya bersifat publik kecuali berada di domain yang Anda kendalikan sendiri, dan taruh apa pun yang penting di domain yang seperti itu.
Tidak satu pun dari ini membutuhkan akun. Kalau Anda sudah melampaui kebutuhan domain publik, yang berubah hanyalah domain di alamatnya — ketiga panggilan di atas tetap persis sama.
Pertanyaan
Apakah saya perlu API key?
Tidak. Pada domain publik tidak ada akun, tidak ada token, dan tidak ada yang perlu didaftarkan, dan sebuah domain yang Anda arahkan ke sini pun menjawab lewat endpoint yang sama tanpa key juga. Satu-satunya pengecualian adalah domain yang sudah kami tutup atas permintaan, yang dibaca dengan header Authorization: Bearer.
Seberapa cepat saya boleh polling?
Sekali per detik per alamat untuk daftarnya, yang memang ritme yang dimaksudkan dan tidak pernah diperlambat. Membaca sebuah pesan atau lampiran dijatah secara terpisah dan jauh lebih longgar, jadi Anda bisa menguras satu halaman pesan sekaligus. Semuanya digabung dibatasi hingga 1200 request per menit per client.
Bagaimana saya tahu kalau sudah membaca seluruh kotak surat?
Ketika next kembali sebagai null. Jangan menyimpulkannya dari sebuah halaman yang pendek: server yang menentukan apa itu sebuah halaman, dan halaman yang lebih pendek dari limit bukan dengan sendirinya berarti akhir.
Bisakah saya memanggil ini dari browser?
Bisa. Setiap jawaban membawa header Access-Control-Allow-Origin: *, jadi sebuah halaman dari origin mana pun bisa memanggil endpoint-endpoint itu langsung tanpa proxy milik Anda di tengah-tengah. Otorisasi di sini tidak pernah berbentuk cookie, jadi membukanya selebar itu tidak ada ruginya.
Apa yang terjadi kalau saya meminta pesan yang sudah kedaluwarsa?
404 dengan not_found, persis seperti untuk id yang memang tidak pernah ada. Semuanya dihapus 5 hari setelah tiba, dibaca ataupun tidak, dan tidak ada parameter yang bisa memperpanjangnya.
Bisakah saya mendapat webhook saat surat tiba?
Tidak — REST API ini bersifat tanya-jawab, tanpa callback. Kalau yang Anda inginkan adalah kode yang blocking sampai pesannya tiba, server MCP punya wait_for_message, yang persis melakukan itu dan memang ditujukan untuk agent.
Amankah memakai alamat publik di production?
Hanya untuk hal-hal yang tidak Anda persoalkan kalau dibaca orang asing. Siapa pun yang tahu alamatnya bisa membaca kotak suratnya, lewat API persis seperti lewat situsnya. Untuk yang lainnya, arahkan sebuah domain milik Anda sendiri ke sini — panggilan-panggilannya tidak berubah.
Kenapa pesan yang belum pernah saya buka muncul sebagai sudah dibaca?
Karena sesuatu sudah membukanya. Membaca sebuah pesan lewat API mengatur flag seen-nya, dan flag itu dibagikan ke semua orang yang melihat alamat tersebut. Sebuah skrip dan seseorang yang memantau kotak surat yang sama akan terus saling mengejutkan, jadi saring berdasarkan id yang sudah Anda tangani, bukan berdasarkan seen.
Apakah saya harus menghapus pesan?
Tidak — semuanya kedaluwarsa dengan sendirinya setelah 5 hari. Menghapus tetap layak dilakukan dalam sebuah job terjadwal, karena kotak surat yang sudah dikosongkan adalah catatan paling sederhana yang mungkin ada tentang apa yang sudah Anda proses.


