Kalau backend Client ditulis dengan Go, integrasi OTP sebaiknya tetap dibuat sederhana: API key disimpan di server, aplikasi meminta OTP lewat backend, lalu backend memverifikasi kode yang diketik End User. Di tutorial ini, kita pakai SDK resmi OTP.ID untuk Go: github.com/otp-id/otp-id-go.
Fokus tutorial ini adalah flow backend. UI register/login tidak dibuat lengkap, tapi contoh endpoint sudah cukup untuk fondasi verifikasi nomor HP di aplikasi Go.
Kenapa pakai SDK resmi OTP.ID?
SDK resmi membantu developer mengurangi kode HTTP manual. Client tetap perlu memahami flow OTP, tetapi detail seperti base URL default, header Bearer token, decoding response envelope, dan tipe hasil sudah dibungkus di package otpid.
Untuk Go, SDK resmi OTP.ID dipublish sebagai module:
go get github.com/otp-id/[email protected]
Module ini memakai package otpid, mendukung Go 1.21+, dan tidak membawa dependency pihak ketiga. SDK juga tidak melakukan retry otomatis. Ini penting untuk OTP, karena retry yang tidak dikontrol bisa membuat End User menerima kode berkali-kali atau membuat flow verifikasi sulit ditelusuri.
Arsitektur demo yang akan dibuat
Alurnya seperti ini:
- End User memasukkan nomor HP di aplikasi milik Client.
- Aplikasi memanggil endpoint Go milik Client: `POST /otp/request`.
- Backend Go memakai SDK OTP.ID untuk memanggil `POST /v3/request`.
- OTP.ID mengirim Kode OTP lewat Channel yang dipilih.
- Backend Go menyimpan `otp_id` sementara.
- End User mengetik Kode OTP.
- Aplikasi memanggil `POST /otp/verify`.
- Backend Go memakai SDK OTP.ID untuk memverifikasi `otp_id` dan kode.

Dalam konteks OTP.ID, Client adalah bisnis atau aplikasi yang mengintegrasikan API OTP.ID. End User adalah pengguna akhir milik Client yang menerima Kode OTP.
Prasyarat
Siapkan:
- Go 1.21 atau lebih baru.
- API key OTP.ID dari dashboard Client.
- Kredit testing OTP.ID.
- Nomor uji aman dengan format internasional tanpa tanda `+`, misalnya `628xxxxxxxxxx`.
Buat project:
mkdir otp-id-go-demo
cd otp-id-go-demo
go mod init example.com/otp-id-go-demo
go get github.com/otp-id/[email protected]
Simpan API key di environment variable server:
export OTP_ID_API_KEY="<OTP_ID_API_KEY>"
Jangan menaruh API key di repository, mobile app, JavaScript browser, screenshot publik, atau log yang bisa dibaca banyak orang.
Membuat server Go minimal
Buat file main.go:
package main
import (
"context"
"encoding/json"
"errors"
"log"
"net/http"
"os"
"regexp"
"sync"
"time"
otpid "github.com/otp-id/otp-id-go"
)
type pendingOTP struct {
OTPID string
Destination string
RequestedAt time.Time
}
type server struct {
otpClient *otpid.Client
mu sync.Mutex
pending map[string]pendingOTP
}
func main() {
apiKey := os.Getenv("OTP_ID_API_KEY")
if apiKey == "" {
log.Fatal("OTP_ID_API_KEY belum diisi")
}
s := &server{
otpClient: otpid.NewClient(apiKey),
pending: make(map[string]pendingOTP),
}
mux := http.NewServeMux()
mux.HandleFunc("POST /otp/request", s.requestOTP)
mux.HandleFunc("POST /otp/verify", s.verifyOTP)
log.Println("demo berjalan di http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
Contoh ini memakai map di memory supaya mudah dibaca. Untuk production, gunakan database atau cache yang sesuai dengan flow aplikasi Client.
Membuat endpoint request OTP
Tambahkan helper request/response dan handler requestOTP:
type requestOTPBody struct {
Phone string `json:"phone"`
}
type verifyOTPBody struct {
Phone string `json:"phone"`
OTP string `json:"otp"`
}
var phonePattern = regexp.MustCompile(`^62\d{8,15}$`)
func writeJSON(w http.ResponseWriter, status int, payload any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(payload)
}
func (s *server) requestOTP(w http.ResponseWriter, r *http.Request) {
var body requestOTPBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"message": "body JSON tidak valid"})
return
}
if !phonePattern.MatchString(body.Phone) {
writeJSON(w, http.StatusBadRequest, map[string]string{"message": "nomor harus format 628xxxxxxxxxx"})
return
}
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
defer cancel()
result, err := s.otpClient.RequestOTP(ctx, otpid.OrderParams{
Channel: otpid.ChannelWhatsApp,
Destination: body.Phone,
Brand: "TokoAnda",
OtpLength: 6,
TTL: 300,
ExternalID: "go-demo:" + body.Phone,
})
if err != nil {
s.handleOTPError(w, err)
return
}
if result.Status == "failed" {
writeJSON(w, http.StatusBadGateway, map[string]string{"message": "OTP belum berhasil dikirim"})
return
}
s.mu.Lock()
s.pending[body.Phone] = pendingOTP{
OTPID: result.OtpID,
Destination: body.Phone,
RequestedAt: time.Now(),
}
s.mu.Unlock()
writeJSON(w, http.StatusOK, map[string]any{
"message": "OTP berhasil diminta",
"otp_id": result.OtpID,
"status": result.Status,
})
}
Beberapa hal yang penting:
- `ChannelWhatsApp` adalah contoh. Pilih Channel sesuai kebutuhan produk Client.
- `Destination` memakai nomor End User dalam format digit-only.
- `Brand` sebaiknya nama brand Client yang dikenali End User.
- `ExternalID` membantu Client menautkan request OTP ke record internal. Untuk behavior idempotency detail, ikuti kontrak V3 dan validasi sesuai kebutuhan aplikasi.
- Jika `result.Status == "failed"`, treat sebagai pengiriman gagal, bukan verifikasi gagal.
Membuat endpoint verify OTP
Tambahkan handler verifyOTP:
func (s *server) verifyOTP(w http.ResponseWriter, r *http.Request) {
var body verifyOTPBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"message": "body JSON tidak valid"})
return
}
if !phonePattern.MatchString(body.Phone) {
writeJSON(w, http.StatusBadRequest, map[string]string{"message": "nomor tidak valid"})
return
}
if body.OTP == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"message": "kode OTP wajib diisi"})
return
}
s.mu.Lock()
pending, ok := s.pending[body.Phone]
s.mu.Unlock()
if !ok {
writeJSON(w, http.StatusBadRequest, map[string]string{"message": "tidak ada request OTP aktif"})
return
}
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
defer cancel()
result, err := s.otpClient.VerifyOTP(ctx, pending.OTPID, body.OTP)
if err != nil {
s.handleOTPError(w, err)
return
}
if !result.Verified {
writeJSON(w, http.StatusBadRequest, map[string]any{
"message": "kode OTP salah",
"reason": result.Reason,
})
return
}
s.mu.Lock()
delete(s.pending, body.Phone)
s.mu.Unlock()
writeJSON(w, http.StatusOK, map[string]any{
"message": "nomor berhasil diverifikasi",
"verified": true,
})
}
Di kontrak OTP.ID V3 dan SDK Go resmi, kode salah bukan error network dan bukan panic. SDK mengembalikan VerifyResult{Verified:false, Reason:"mismatch"} tanpa error saat server merespons HTTP 200 untuk kode yang salah. Karena itu, aplikasi harus mengecek field Verified, bukan hanya mengecek err == nil.
Error handling dengan APIError
SDK Go resmi mengembalikan *otpid.APIError untuk error dari API OTP.ID. Gunakan errors.As agar handler bisa membaca kode error tanpa string matching manual.
func (s *server) handleOTPError(w http.ResponseWriter, err error) {
var apiErr *otpid.APIError
if errors.As(err, &apiErr) {
switch apiErr.Code {
case otpid.ErrCodeUnauthorized:
writeJSON(w, http.StatusInternalServerError, map[string]string{"message": "konfigurasi API key perlu dicek"})
case otpid.ErrCodeInsufficientBalance:
writeJSON(w, http.StatusPaymentRequired, map[string]string{"message": "kredit OTP tidak cukup"})
case otpid.ErrCodeRateLimited, otpid.ErrCodeDestinationRateLimited:
writeJSON(w, http.StatusTooManyRequests, map[string]string{"message": "terlalu banyak request OTP, coba lagi nanti"})
case otpid.ErrCodeOtpExpired:
writeJSON(w, http.StatusBadRequest, map[string]string{"message": "kode OTP sudah kedaluwarsa"})
case otpid.ErrCodeTooManyAttempts:
writeJSON(w, http.StatusBadRequest, map[string]string{"message": "terlalu banyak percobaan verifikasi"})
case otpid.ErrCodeAlreadyUsed:
writeJSON(w, http.StatusBadRequest, map[string]string{"message": "kode OTP sudah dipakai"})
default:
writeJSON(w, http.StatusBadGateway, map[string]string{"message": "request OTP gagal"})
}
return
}
writeJSON(w, http.StatusBadGateway, map[string]string{"message": "gagal menghubungi layanan OTP"})
}

Jangan meneruskan apiErr.Message mentah ke End User tanpa kurasi. Pesan teknis berguna untuk log internal developer, tapi End User cukup butuh instruksi yang jelas: kode salah, kode kedaluwarsa, atau tunggu sebelum meminta ulang.
Menjalankan demo lokal
Jalankan server:
go run .
Request OTP:
curl -X POST http://localhost:8080/otp/request \
-H "Content-Type: application/json" \
-d '{"phone":"628xxxxxxxxxx"}'
Verify OTP:
curl -X POST http://localhost:8080/otp/verify \
-H "Content-Type: application/json" \
-d '{"phone":"628xxxxxxxxxx","otp":"123456"}'
Ganti 123456 dengan Kode OTP yang diterima di nomor uji. Jangan memasukkan kode OTP asli ke dokumentasi publik, screenshot, atau log yang tidak perlu.
README/demo guide singkat
Struktur demo:
otp-id-go-demo/
├── go.mod
└── main.go
Langkah setup:
- Jalankan `go mod init example.com/otp-id-go-demo`.
- Install SDK resmi dengan `go get github.com/otp-id/[email protected]`.
- Isi `OTP_ID_API_KEY` di environment server.
- Jalankan `go run .`.
- Panggil `/otp/request` dengan nomor uji.
- Panggil `/otp/verify` dengan kode dari End User.
Untuk production, ganti penyimpanan map dengan database/cache, tambahkan rate limit di endpoint milik Client, dan pastikan log tidak menyimpan API key, nomor HP mentah, atau Kode OTP.
Checklist sebelum production
- [ ] API key hanya ada di server-side environment atau secret manager.
- [ ] Frontend/mobile tidak pernah memanggil API OTP.ID langsung.
- [ ] Nomor HP dinormalisasi sebelum dikirim sebagai `Destination`.
- [ ] `otp_id` disimpan dengan scope yang benar, misalnya per register attempt atau login attempt.
- [ ] `Verified:false` ditangani sebagai kode salah, bukan dianggap sukses.
- [ ] Error `INSUFFICIENT_BALANCE`, `RATE_LIMITED`, `DESTINATION_RATE_LIMITED`, `OTP_EXPIRED`, `TOO_MANY_ATTEMPTS`, dan `ALREADY_USED` punya respons aplikasi yang jelas.
- [ ] Tombol resend punya cooldown.
- [ ] Tidak ada API key, Kode OTP, atau nomor HP mentah di log yang tidak perlu.
- [ ] Contoh sudah dites dengan nomor uji aman sebelum dipakai End User sungguhan.
Penutup
Dengan SDK resmi OTP.ID, integrasi Go bisa dibuat lebih ringkas tanpa kehilangan kontrol di sisi backend Client. Kuncinya tetap sama: API key hanya di server, otp_id disimpan dengan benar, response Verified dicek eksplisit, dan semua error OTP diterjemahkan menjadi instruksi yang jelas untuk End User maupun tim operasional.
