Kirim Notifikasi Status Pengiriman Paket dengan Go Fiber + MailAnvil
Pola notifikasi status paket dipakai di mana-mana di Indonesia: marketplace saat pesanan dikirim, kurir logistik saat paket diantar, sampai aplikasi food delivery saat kurir menjemput pesanan. Pengguna tidak mau buka aplikasi tiap 5 menit untuk cek resi — mereka menunggu email yang bilang "paket kamu sedang diantar".
Tutorial ini membangun satu endpoint Go Fiber yang menerima event status pengiriman lalu mengirim email transaksional via MailAnvil API. Total ~80 baris Go, tanpa library email pihak ketiga — cukup net/http bawaan.
Kenapa Go Fiber + MailAnvil
- Go Fiber cepat dan ringan — cocok untuk webhook murni yang kerjanya cuma menerima event dan meneruskan.
- MailAnvil dibangun di Cloudflare Workers, jadi request dari server Indonesia ke API di-route ke edge terdekat. Satu
POSTJSON, email langsung masuk antrian pengiriman lewat AWS SES. - Kuota gratis Rp0: 3.000 email/bulan, cukup untuk tahap development dan launch pertama. Untuk volume besar, pricing pakai Rupiah — jauh lebih masuk akal daripada invoice dolar untuk startup lokal.
Siapkan proyek
mkdir notifikasi-pengiriman && cd notifikasi-pengiriman
go mod init notifikasi-pengiriman
go get github.com/gofiber/fiber/v2
export MAILANVIL_KEY="re_api_key_kamu"
API key ambil dari dashboard MailAnvil setelah verifikasi domain (detail di bawah).
Struktur kode
Satu file main.go, tiga bagian: event payload, fungsi kirim email, dan handler Fiber.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"github.com/gofiber/fiber/v2"
)
// Event yang masuk dari sistem order/logistik kamu
type ShipmentEvent struct {
Resi string `json:"resi"`
Status string `json:"status"` // DIKEMAS | DIKIRIM | DIANTAR | TERKIRIM
Courier string `json:"courier"`
Email string `json:"email"`
City string `json:"city"`
Estimate string `json:"estimate"`
}
type mailBody struct {
From string `json:"from"`
To []string `json:"to"`
Subject string `json:"subject"`
HTML string `json:"html"`
}
const api = "https://api.mailanvil.com/v1/send"
var label = map[string]string{
"DIKEMAS": "Pesanan sedang dikemas",
"DIKIRIM": "Paket sudah diserahkan ke kurir",
"DIANTAR": "Paket sedang diantar ke alamat kamu",
"TERKIRIM": "Paket sudah tiba!",
}
func kirimEmail(e ShipmentEvent) error {
subject := fmt.Sprintf("Paket %s — %s", e.Resi, label[e.Status])
cityLine := ""
if e.City != "" {
cityLine = fmt.Sprintf("<p>Tujuan: <strong>%s</strong></p>", e.City)
}
etaLine := ""
if e.Estimate != "" {
etaLine = fmt.Sprintf("<p>Estimasi tiba: <strong>%s</strong></p>", e.Estimate)
}
html := fmt.Sprintf(`<div style="font-family:sans-serif;max-width:480px;margin:0 auto">
<p>Halo,</p>
<p><strong>%s</strong></p>
<p>Nomor resi: <code>%s</code> • Kurir: %s</p>
%s%s
<p>Terima kasih sudah belanja di toko kami.</p>
</div>`, label[e.Status], e.Resi, e.Courier, cityLine, etaLine)
payload, _ := json.Marshal(mailBody{
From: "[email protected]",
To: []string{e.Email},
Subject: subject,
HTML: html,
})
req, _ := http.NewRequest("POST", api, bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+os.Getenv("MAILANVIL_KEY"))
req.Header.Set("Content-Type", "application/json")
// Idempotency: kalau webhook retry karena timeout, email tidak dobel kirim
req.Header.Set("Idempotency-Key", "ship-"+e.Resi+"-"+e.Status)
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != 202 {
b, _ := io.ReadAll(res.Body)
return fmt.Errorf("mailanvil %d: %s", res.StatusCode, b)
}
return nil
}
func main() {
app := fiber.New()
app.Post("/webhooks/shipment", func(c *fiber.Ctx) error {
var e ShipmentEvent
if err := json.Unmarshal(c.Body(), &e); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "payload tidak valid"})
}
if e.Resi == "" || e.Email == "" || e.Status == "" {
return c.Status(400).JSON(fiber.Map{"error": "resi, email, status wajib diisi"})
}
if _, ok := label[e.Status]; !ok {
return c.Status(400).JSON(fiber.Map{"error": "status tidak dikenal"})
}
if err := kirimEmail(e); err != nil {
return c.Status(502).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true, "resi": e.Resi})
})
app.Listen(":3000")
}
Jalankan dan tes:
go run main.go
curl -X POST localhost:3000/webhooks/shipment \
-H "Content-Type: application/json" \
-d '{"resi":"JNE2609228871","status":"DIANTAR","courier":"JNE","email":"[email protected]","city":"Bandung","estimate":"Besok pagi"}'
Respons {"ok":true,"resi":"JNE2609228871"} berarti email sudah masuk antrian MailAnvil (kode 202 = queued, bukan terkirim instan — pipeline SES asinkron, status per-recipient bisa dipantau via dashboard atau webhook event).
Tiga detail yang sering kelewat
Verifikasi domain dulu. from harus domain yang sudah verifikasi DKIM — kalau belum, API menolak dengan 403. Ini kebijakan deliverability: tanpa DKIM, email notifikasi berpotensi nyangkut di spam. Prosesnya 5 menit lewat dashboard, tinggal tambah beberapa record DNS.
Idempotency-Key = anti dobel kirim. Webhook provider logistik terkenal suka retry. Karena key-nya deterministik (resi + status), pengiriman ulang event yang sama aman — MailAnvil mengenali replay dan tidak mengirim ulang.
Status dari sistem kamu, bukan tebak-tebakan. Handler memvalidasi status terhadap daftar yang dikenal, jadi typo "DIANTAR " (ada spasi) dari sistem kurir tidak bikin email kosong terkirim ke pelanggan.
Dari tutorial ke produksi
Pola ini langsung cocok untuk skenario nyata: marketplace yang kirim update "pesanan dikemas → dikirim → tiba", kurir logistik dengan notifikasi "paket sedang diantar", sampai toko online kecil yang pakai Google Sheet + cron sebagai sistem ordernya. Kalau backend kamu Express.js atau Laravel, strukturnya sama — ganti handler-nya saja.
Coba MailAnvil gratis di mailanvil.com — 3.000 email/bulan, Rp0, tanpa kartu kredit.