NEXO ROUTE
Guide

Send OTP from
Node.js

Send an OTP from Node.js in 7 lines. Native fetch, no SDK dependency. Production-ready error handling included.

Get an API key Full docs

1. Install

# Node.js 18+ has native fetch — no install needed.
# For older runtimes:
npm install node-fetch

2. 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

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.

Switch from

Use cases

SDK guides