Aplikasi Vue biasanya berhenti di satu pertanyaan saat mulai menambahkan OTP: “request OTP-nya dipanggil dari mana?”
Jawaban amannya: dari backend milik Client. Bukan dari komponen Vue, bukan dari composable di browser, dan bukan dari file env frontend yang ikut masuk ke bundle.
API key OTP.ID adalah kredensial server. Kalau key itu bocor ke browser, siapa pun yang membuka DevTools atau membaca bundle aplikasi bisa menyalinnya. Setelah itu, request OTP bisa dibuat tanpa melewati aturan aplikasi milik Client.
Di tutorial ini, Vue hanya bertugas mengambil nomor dari End User dan menampilkan form kode OTP. Backend Express/Node milik Client yang memanggil OTP.ID memakai SDK resmi @otp-id/sdk.
Arsitektur yang aman: Vue → backend Client → OTP.ID
Dalam konteks OTP.ID, Client adalah bisnis atau aplikasi yang mengintegrasikan OTP.ID. End User adalah orang yang menerima dan memasukkan Kode OTP di aplikasi Client.
Untuk project Vue, alurnya sebaiknya seperti ini:
- End User mengetik nomor di halaman Vue.
- Vue mengirim nomor itu ke backend Client, misalnya
POST /api/otp/request. - Backend Client memanggil OTP.ID lewat SDK
@otp-id/sdk. - OTP.ID mengirim Kode OTP ke End User.
- Vue menampilkan form input kode.
- Vue mengirim
otp_iddan kode ke backend Client, misalnyaPOST /api/otp/verify. - Backend Client memanggil OTP.ID untuk verifikasi.
- Backend mengembalikan hasil aman ke Vue: terverifikasi atau belum.

Bagian pentingnya ada di langkah 3 dan 7. Hanya backend yang tahu API key OTP.ID. Vue tidak perlu tahu endpoint OTP.ID langsung, tidak perlu menyimpan API key, dan tidak perlu memakai prefix env publik seperti VITE_ untuk secret.
Siapkan backend Node dan SDK @otp-id/sdk
SDK resmi Node/TypeScript OTP.ID dipublikasikan sebagai package npm @otp-id/sdk. Knowledge OTP.ID mencatat SDK ini memakai Node >=20, memanfaatkan fetch bawaan Node, dan menyediakan method seperti requestOtp, verifyOtp, otpStatus, account, serta createTopup.
Contoh struktur sederhana:
project-vue-otp/
├─ client/ # Vue app
└─ server/ # Express/Node backend
Install dependency di folder backend:
cd server
npm install express @otp-id/sdk
npm install -D typescript @types/node @types/express
Simpan API key di environment backend:
OTPID_API_KEY=<OTP_ID_API_KEY>
Jangan menaruhnya di .env Vue dengan nama seperti VITE_OTP_ID_API_KEY. Prefix VITE_ memang dibuat agar variabel bisa dibaca di browser. Itu cocok untuk konfigurasi publik, bukan untuk API key.
Endpoint request OTP di Express
Contoh berikut memakai TypeScript. Endpoint menerima nomor dari Vue, memvalidasi format dasar, lalu memanggil requestOtp dari SDK.
import express, { Request, Response } from 'express';
import { OtpIdClient, APIError } from '@otp-id/sdk';
const apiKey = process.env.OTPID_API_KEY;
if (!apiKey) {
throw new Error('OTPID_API_KEY is required');
}
const otp = new OtpIdClient(apiKey);
const app = express();
app.use(express.json());
function isValidIndonesianPhone(value: unknown): value is string {
return typeof value === 'string' && /^628\d{8,15}$/.test(value);
}
app.post('/api/otp/request', async (req: Request, res: Response) => {
const { phone } = req.body as { phone?: unknown };
if (!isValidIndonesianPhone(phone)) {
return res.status(400).json({ message: 'Nomor harus format 628xxxxxxxxxx.' });
}
try {
const result = await otp.requestOtp({
channel: 'whatsapp',
destination: phone,
brand: 'TokoAnda',
otp_length: 6,
ttl: 300,
});
return res.json({ otp_id: result.otp_id, status: result.status });
} catch (err) {
if (err instanceof APIError) {
return res.status(err.httpStatus || 500).json({ code: err.code, message: err.message });
}
return res.status(500).json({ message: 'Gagal meminta OTP.' });
}
});
Ada beberapa detail yang sengaja dibuat ketat.
Nomor yang dikirim ke OTP.ID memakai format 628xxxxxxxxxx. Backend mengembalikan otp_id ke Vue karena ID ini dibutuhkan saat verifikasi. Backend tidak mengembalikan API key, tidak mengirim detail internal, dan tidak menyimpan Kode OTP di browser.
Field destination, brand, otp_length, dan ttl mengikuti kontrak API V3 OTP.ID. Untuk channel berbasis nomor seperti WhatsApp, destination berisi nomor tujuan End User.
Endpoint verify OTP di Express
Saat End User memasukkan kode, Vue mengirim kode dan otp_id ke backend. Backend lalu memanggil verifyOtp.
app.post('/api/otp/verify', async (req: Request, res: Response) => {
const { otp_id, code } = req.body as { otp_id?: unknown; code?: unknown };
if (typeof otp_id !== 'string' || otp_id.trim() === '') {
return res.status(400).json({ message: 'otp_id wajib diisi.' });
}
if (typeof code !== 'string' || !/^\d{4,8}$/.test(code)) {
return res.status(400).json({ message: 'Kode OTP harus 4 sampai 8 digit.' });
}
try {
const result = await otp.verifyOtp(otp_id, code);
if (!result.verified) {
return res.status(400).json({ verified: false, reason: result.reason });
}
return res.json({ verified: true });
} catch (err) {
if (err instanceof APIError) {
return res.status(err.httpStatus || 500).json({ code: err.code, message: err.message });
}
return res.status(500).json({ message: 'Gagal memverifikasi OTP.' });
}
});
app.listen(3000, () => {
console.log('OTP backend listening on http://localhost:3000');
});

Di SDK OTP.ID, hasil verified:false dengan reason mismatch bukan exception. Itu hasil normal ketika kode yang dimasukkan End User tidak cocok. Karena itu, kode di atas menangani mismatch lewat cabang if (!result.verified), bukan lewat catch.
catch dipakai untuk error API atau transport yang memang perlu ditangani sebagai kegagalan request. SDK menyediakan APIError, sehingga backend bisa membaca err.code dan err.httpStatus untuk menentukan respons ke frontend.
SDK resmi tidak melakukan retry otomatis. Kalau aplikasi Client ingin menyediakan tombol “kirim ulang”, keputusan retry harus dibuat di backend atau flow aplikasi, bukan diserahkan diam-diam ke SDK. Ini penting supaya Client tetap bisa mengontrol biaya, cooldown UI, dan pengalaman End User.
Contoh Vue component sederhana
Di sisi Vue, tugasnya cukup memanggil backend Client. Contoh ini memakai Composition API.
<script setup lang="ts">
import { ref } from 'vue';
const phone = ref('628xxxxxxxxxx');
const code = ref('');
const otpId = ref('');
const message = ref('');
const loading = ref(false);
async function requestOtp() {
loading.value = true;
message.value = '';
try {
const response = await fetch('/api/otp/request', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone: phone.value }),
});
const data = await response.json();
if (!response.ok) {
message.value = data.message || 'Gagal meminta OTP.';
return;
}
otpId.value = data.otp_id;
message.value = 'Kode OTP sudah dikirim.';
} finally {
loading.value = false;
}
}
async function verifyOtp() {
loading.value = true;
message.value = '';
try {
const response = await fetch('/api/otp/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ otp_id: otpId.value, code: code.value }),
});
const data = await response.json();
if (!response.ok || !data.verified) {
message.value = data.reason === 'mismatch'
? 'Kode OTP belum cocok.'
: data.message || 'Verifikasi gagal.';
return;
}
message.value = 'Nomor berhasil diverifikasi.';
} finally {
loading.value = false;
}
}
</script>
<template>
<form @submit.prevent="otpId ? verifyOtp() : requestOtp()">
<label>
Nomor HP
<input v-model="phone" inputmode="tel" autocomplete="tel" />
</label>
<label v-if="otpId">
Kode OTP
<input v-model="code" inputmode="numeric" autocomplete="one-time-code" />
</label>
<button type="submit" :disabled="loading">
{{ otpId ? 'Verifikasi OTP' : 'Kirim OTP' }}
</button>
<p v-if="message">{{ message }}</p>
</form>
</template>
Perhatikan tidak ada import @otp-id/sdk di file Vue. Tidak ada OTPID_API_KEY. Tidak ada VITE_OTP_ID_API_KEY. Frontend hanya tahu route backend milik aplikasi sendiri.
Checklist sebelum production
Sebelum dipakai di production, cek beberapa hal ini di sisi Client:
- API key OTP.ID hanya berada di environment backend, misalnya
OTPID_API_KEY=<OTP_ID_API_KEY>. - Backend, bukan browser, yang mengimpor dan menjalankan
@otp-id/sdk. - UI punya cooldown tombol kirim ulang agar End User tidak menekan request berkali-kali.
- Backend menangani error seperti
RATE_LIMITED,DESTINATION_RATE_LIMITED,INSUFFICIENT_BALANCE,OTP_EXPIRED,TOO_MANY_ATTEMPTS, danALREADY_USED. - Kode salah ditangani sebagai hasil verifikasi normal:
verified:falsedengan reasonmismatch. - Nomor tujuan dinormalisasi ke format aman seperti
628xxxxxxxxxxsebelum dikirim ke OTP.ID. - Tidak ada log yang mencetak API key, Kode OTP, atau payload sensitif.
OTP.ID juga mencatat bahwa integrasi baru sebaiknya memakai API publik V3, dan SDK resmi Node membungkus endpoint V3 tersebut. Untuk uji awal, gunakan nomor sendiri dan jangan mengandalkan sandbox karena knowledge OTP.ID menyatakan sandbox belum tersedia.
Langkah berikutnya
Kalau alur di atas sudah berjalan, aplikasi Vue sudah punya pemisahan yang sehat: Vue mengurus input End User, backend Client mengurus credential dan komunikasi ke OTP.ID.
Ambil API key dari dashboard OTP.ID di https://app.otp.id, lalu cocokkan implementasi backend dengan dokumentasi terbaru di https://docs.otp.id.
