Send OTP from
Node.js
Send an OTP from Node.js in 7 lines. Native fetch, no SDK dependency. Production-ready error handling included.
1. Install
# Node.js 18+ has native fetch — no install needed.
# For older runtimes:
npm install node-fetch2. Send your first OTP
Set NEXO_KEY in your environment, then run:
// send-otp.ts (Node.js 18+, TypeScript)
const API_KEY = process.env.NEXO_KEY!;
interface SendOtpResponse {
ok: boolean;
id?: string;
carrier?: string;
status?: 'sent';
cost?: number;
balance?: number;
error?: string;
deposit_url?: string;
}
export async function sendOtp(phone: string, code: string): Promise<SendOtpResponse> {
const res = await fetch('https://nexoroute.dev/api/v1/send-otp', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ phone, code })
});
const data = (await res.json()) as SendOtpResponse;
if (!data.ok) {
// Common errors: insufficient_balance, invalid_phone, no_route, ip_not_allowed
throw new Error(`NEXO send failed: ${data.error}`);
}
return data;
}
// Usage
const result = await sendOtp('+84981234567', '482917');
console.log(`Dispatched id=${result.id} via ${result.carrier}`);
console.log(`Charged $${result.cost}, wallet balance $${result.balance}`);3. Receive delivery webhooks
We POST a signed event to your URL when each OTP reaches a terminal state (delivered or failed). Verify the HMAC before trusting the payload.
// webhook-receiver.ts (Express)
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const WEBHOOK_SECRET = process.env.NEXO_WEBHOOK_SECRET!;
// SHA-256 of your plaintext secret (we only store the hash on our side)
const secretKey = crypto.createHash('sha256').update(WEBHOOK_SECRET).digest();
app.post('/webhooks/nexo', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.header('X-NEXO-Signature') ?? '';
const expected = 'sha256=' + crypto
.createHmac('sha256', secretKey)
.update(req.body)
.digest('hex');
// Timing-safe comparison
if (sig.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).send('bad signature');
}
const event = JSON.parse(req.body.toString());
console.log(`OTP ${event.id} → ${event.status}`);
// event = { type, id, phone, carrier, status, cost, error_code, created_at, completed_at }
res.status(200).send('ok');
});
app.listen(3000);Common pitfalls
- Native fetch in Node.js <18 throws — install node-fetch or upgrade runtime
- express.raw() before JSON parser — HMAC verification needs the unparsed buffer
- Compare signatures with crypto.timingSafeEqual to avoid timing attacks
- Phone must be E.164 (+84xxxxxxxxx), not local (0xxx) — convert before calling
- Webhook must respond 2xx in <5s — defer work to a queue if you have slow processing
FAQ
Do you have an official npm package?
Not yet. The API is small enough that a 30-line wrapper is usually all you need. We may publish @nexoroute/node when the API surface stabilises.
How do I handle the insufficient_balance error?
The response includes a deposit_url field — redirect or surface it to your operations team. We do not auto-fail-over to a backup provider.
Can I use this in a serverless function (Vercel / Lambda)?
Yes — the API is HTTP-only, no persistent connection. Cold start latency adds 100-300ms to first call, otherwise identical.
How do I unit-test code that calls sendOtp without spending credit?
Mock the fetch call. We do not provide a sandbox URL — production OTPs are charged against your wallet, and free signup credits are limited.
Ship OTP in 7 lines of Node.js
5 free test OTPs on signup. Real Vietnamese carriers, real delivery.
Use cases