Tutorial Integrasi OTP.ID di Golang: Kirim dan Verifikasi OTP dengan SDK Resmi

Tutorial Integrasi OTP.ID di Golang: Kirim dan Verifikasi OTP dengan SDK Resmi

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:

  1. End User memasukkan nomor HP di aplikasi milik Client.
  2. Aplikasi memanggil endpoint Go milik Client: `POST /otp/request`.
  3. Backend Go memakai SDK OTP.ID untuk memanggil `POST /v3/request`.
  4. OTP.ID mengirim Kode OTP lewat Channel yang dipilih.
  5. Backend Go menyimpan `otp_id` sementara.
  6. End User mengetik Kode OTP.
  7. Aplikasi memanggil `POST /otp/verify`.
  8. Backend Go memakai SDK OTP.ID untuk memverifikasi `otp_id` dan kode.
Alur aplikasi Client ke backend Go lalu ke API OTP.ID

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:

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:

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"})
}
Ilustrasi keamanan API key dan error handling OTP di backend Go

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:

  1. Jalankan `go mod init example.com/otp-id-go-demo`.
  2. Install SDK resmi dengan `go get github.com/otp-id/[email protected]`.
  3. Isi `OTP_ID_API_KEY` di environment server.
  4. Jalankan `go run .`.
  5. Panggil `/otp/request` dengan nomor uji.
  6. 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

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.

← SEMUA ARTIKEL