Kode OTP sering terlihat sederhana: kirim angka, tunggu End User mengetik ulang, lalu lanjutkan proses. Di backend, detailnya lebih banyak. API key harus aman, nomor harus divalidasi, request ulang tidak boleh membuat spam, dan aplikasi perlu tahu kapan kode salah masih bisa dicoba lagi atau transaksi sudah kedaluwarsa.
Artikel ini membahas cara integrasi API OTP WhatsApp di Laravel memakai HTTP client server-side. Saat knowledge OTP.ID ini dipakai, SDK resmi tersedia untuk Go, PHP, Node.js, dan Python. Belum ada framework binding Laravel yang terdokumentasi sebagai SDK resmi, jadi contoh di bawah memakai HTTP backend Laravel langsung ke API V3 OTP.ID.
Kenapa integrasi OTP WhatsApp harus lewat backend Laravel
API key OTP.ID adalah kredensial Client. Artinya, key itu milik aplikasi atau bisnis yang mengintegrasikan OTP.ID, bukan milik End User. Jangan menaruh API key di JavaScript frontend, aplikasi mobile, atau bundle yang bisa dibongkar dari browser.
Alur yang aman biasanya seperti ini:
- End User memasukkan nomor HP di aplikasi.
- Frontend memanggil backend Laravel milik Client.
- Backend Laravel memanggil API OTP.ID dengan API key dari environment variable.
- OTP.ID mengirim Kode OTP via Channel WhatsApp.
- End User memasukkan Kode OTP.
- Backend Laravel memanggil endpoint verify OTP.ID.
- Aplikasi membuat session, menandai nomor verified, atau melanjutkan flow hanya setelah hasil verify benar.
Dengan pola ini, frontend tidak pernah melihat API key. Frontend hanya tahu status yang perlu ditampilkan ke End User.
Alur request dan verify di OTP.ID
API publik OTP.ID memakai V3. Untuk WhatsApp OTP, backend Laravel memanggil POST /v3/request dengan channel: "whatsapp". OTP.ID mengembalikan otp_id. Simpan nilai ini di sisi backend karena otp_id dibutuhkan saat memverifikasi kode.

Payload request yang umum dipakai:
{
"channel": "whatsapp",
"destination": "628xxxxxxxxxx",
"brand": "TokoAnda",
"otp_length": 6,
"ttl": 300,
"external_id": "login-12345"
}
Beberapa catatan penting:
destinationberisi nomor tujuan dalam format digit, misalnya628xxxxxxxxxx.brandadalah nama brand yang muncul di pesan untuk channel berbasis pesan.otp_lengthdi V3 berada dalam rentang 4 sampai 8 digit.ttldefault 300 detik dan berada dalam rentang 60 sampai 900 detik.external_idmembantu retry yang aman dalam window 24 jam, selama parameter request sama.
Saat End User memasukkan Kode OTP, backend memanggil POST /v3/verify dengan otp_id dan otp.
{
"otp_id": "OTP20260815ABCD000123",
"otp": "123456"
}
Kode salah bukan berarti server error. OTP.ID mengembalikan HTTP 200 dengan verified: false dan reason: "mismatch". Kasus seperti expired, terlalu banyak percobaan, atau OTP sudah dipakai akan muncul sebagai error V3 yang perlu ditangani berbeda.
Siapkan konfigurasi Laravel
Simpan konfigurasi OTP.ID di .env:
OTPID_BASE_URL=https://api.otp.id
OTPID_API_KEY=isi_api_key_dari_dashboard
OTPID_BRAND=TokoAnda
Lalu tambahkan ke config/services.php:
return [
// service lain...
'otpid' => [
'base_url' => env('OTPID_BASE_URL', 'https://api.otp.id'),
'api_key' => env('OTPID_API_KEY'),
'brand' => env('OTPID_BRAND', 'TokoAnda'),
],
];
Pastikan .env tidak masuk git. Kalau API key dicurigai bocor, rotate dari dashboard OTP.ID.
Buat service OTP.ID
Contoh berikut memakai Illuminate\Support\Facades\Http. Buat file app/Services/OtpIdClient.php.
<?php
namespace App\Services;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
class OtpIdClient
{
public function requestWhatsappOtp(string $destination, string $externalId): array
{
return $this->post('/v3/request', [
'channel' => 'whatsapp',
'destination' => $destination,
'brand' => config('services.otpid.brand'),
'otp_length' => 6,
'ttl' => 300,
'external_id' => $externalId,
]);
}
public function verifyOtp(string $otpId, string $otp): array
{
return $this->post('/v3/verify', [
'otp_id' => $otpId,
'otp' => $otp,
]);
}
private function post(string $path, array $payload): array
{
$baseUrl = rtrim((string) config('services.otpid.base_url'), '/');
$apiKey = (string) config('services.otpid.api_key');
try {
$response = Http::withToken($apiKey)
->acceptJson()
->asJson()
->timeout(15)
->post($baseUrl . $path, $payload)
->throw();
} catch (ConnectionException $e) {
throw new \RuntimeException('Tidak bisa menghubungi OTP.ID.', previous: $e);
} catch (RequestException $e) {
$body = $e->response?->json();
$code = data_get($body, 'error.code', 'OTPID_REQUEST_FAILED');
$message = data_get($body, 'error.message', 'Request OTP.ID gagal.');
throw new \RuntimeException($code . ': ' . $message, previous: $e);
}
return $response->json();
}
}
Contoh ini sengaja tipis. Di aplikasi production, biasanya Client menambahkan typed exception sendiri agar controller bisa membedakan INSUFFICIENT_BALANCE, DESTINATION_RATE_LIMITED, dan error lain tanpa membaca string message.
Endpoint Laravel untuk request OTP
Contoh controller sederhana:
<?php
namespace App\Http\Controllers;
use App\Services\OtpIdClient;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class OtpController extends Controller
{
public function request(Request $request, OtpIdClient $otpId): JsonResponse
{
$validated = $request->validate([
'phone' => ['required', 'regex:/^62[0-9]{8,15}$/'],
]);
$externalId = 'login-' . Str::uuid()->toString();
$result = $otpId->requestWhatsappOtp(
destination: $validated['phone'],
externalId: $externalId,
);
$data = data_get($result, 'data', []);
if (data_get($data, 'status') === 'failed') {
return response()->json([
'message' => 'Kode OTP belum bisa dikirim. Coba lagi atau pilih Channel lain.',
'failure' => data_get($data, 'failure'),
], 422);
}
// Simpan otp_id sesuai flow aplikasi: database, cache, atau session server-side.
// Jangan simpan Kode OTP plaintext ke log aplikasi.
session([
'otp_id' => data_get($data, 'otp_id'),
'otp_destination' => $validated['phone'],
]);
return response()->json([
'message' => 'Kode OTP dikirim.',
'otp_id' => data_get($data, 'otp_id'),
]);
}
}
Untuk flow login, simpan otp_id di tempat yang bisa dikaitkan dengan percobaan login tersebut. Untuk flow registrasi, kaitkan dengan nomor yang sedang diverifikasi. Jangan membuat session login permanen sebelum verify berhasil.
Endpoint Laravel untuk verify OTP
Setelah End User mengetik kode, backend memanggil OTP.ID.
public function verify(Request $request, OtpIdClient $otpId): JsonResponse
{
$validated = $request->validate([
'otp' => ['required', 'digits_between:4,8'],
]);
$otpIdValue = session('otp_id');
if (! $otpIdValue) {
return response()->json([
'message' => 'Request OTP tidak ditemukan. Minta kode baru.',
], 422);
}
$result = $otpId->verifyOtp($otpIdValue, $validated['otp']);
$data = data_get($result, 'data', []);
if (data_get($data, 'verified') === true) {
session()->forget(['otp_id', 'otp_destination']);
// Lanjutkan flow aplikasi: tandai nomor verified atau buat session login.
return response()->json([
'message' => 'Nomor berhasil diverifikasi.',
]);
}
if (data_get($data, 'reason') === 'mismatch') {
return response()->json([
'message' => 'Kode OTP salah.',
], 422);
}
return response()->json([
'message' => 'Kode OTP tidak bisa diverifikasi. Minta kode baru.',
'reason' => data_get($data, 'reason'),
], 422);
}

Di contoh ini, mismatch dibuat sebagai response 422 ke frontend agar UI bisa menampilkan pesan yang wajar. Di sisi OTP.ID sendiri, mismatch adalah HTTP 200 karena kode salah termasuk alur normal.
Error handling yang perlu dipikirkan sejak awal
Integrasi OTP yang enak dipakai bukan cuma jalur sukses. Siapkan mapping error di Laravel sejak awal.
| Kondisi dari OTP.ID | Arti untuk aplikasi | Respons UI yang masuk akal |
|---|---|---|
UNAUTHORIZED | API key kosong/salah | Jangan tampilkan detail ke End User; alert tim teknis. |
INSUFFICIENT_BALANCE | Kredit prepaid tidak cukup | Minta tim operasional top up kredit. |
DUPLICATE_EXTERNAL_ID | external_id dipakai ulang dengan parameter berbeda | Perbaiki logika retry di backend. |
RATE_LIMITED | API key melewati 20 request/detik | Tunda request dan cek lonjakan trafik. |
DESTINATION_RATE_LIMITED | Nomor yang sama terlalu sering meminta OTP | Tampilkan cooldown kirim ulang. |
CHANNEL_UNAVAILABLE | Channel sedang tidak tersedia | Beri opsi mencoba lagi atau Channel lain, sesuai desain aplikasi. |
status: "failed" | Pengiriman gagal dan tidak didebit | Aplikasi Client menentukan langkah berikutnya. |
Jangan menampilkan detail internal ke End User. Pesan seperti "kode salah", "minta kode baru", atau "coba lagi beberapa saat" biasanya lebih aman daripada membocorkan kondisi teknis.
Checklist sebelum production
Sebelum flow OTP Laravel dipakai di production, cek hal-hal ini:
- API key hanya ada di server Laravel, bukan di frontend.
- Nomor HP dinormalisasi ke format
628xxxxxxxxxxsebelum dikirim. - Tombol kirim ulang punya cooldown di UI. Rate limit server adalah pagar terakhir, bukan pengganti UX.
otp_iddisimpan server-side dan dikaitkan dengan flow yang benar.- Aplikasi tidak menulis Kode OTP atau API key ke log.
- Backend menangani mismatch, expired, too many attempts, already used, saldo kurang, dan rate limit.
- Login/session hanya dibuat setelah OTP verified.
- Tim operasional tahu cara cek kredit dan top up lewat dashboard.
Bacaan terkait
- API OTP WhatsApp: Cara Kirim dan Verifikasi Kode via OTP.ID
- Verifikasi Nomor HP dengan WhatsApp OTP
- Cara Integrasi API OTP WhatsApp di Node.js
- API OTP WhatsApp untuk Login yang Aman
Penutup
Integrasi API OTP WhatsApp di Laravel sebaiknya dibuat sederhana, tapi tidak asal tempel. Letakkan request dan verify di backend, simpan API key di environment variable, simpan otp_id dengan rapi, lalu tangani error yang memang akan terjadi di flow nyata.
Kalau Client ingin mulai mencoba, registrasi bisa dilakukan di https://app.otp.id. Untuk kontrak API terbaru, gunakan dokumentasi di https://docs.otp.id sebagai rujukan saat implementasi.
