← Back to all posts
2026-09-22 · MailAnvil Team

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

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.