Bỏ qua điều hướng
Hướng Dẫn

Tích Hợp SMS API NodeJS: Gửi Tin Nhắn Tự Động từ Server Node

Tích hợp SMS API NodeJS 2026: code mẫu gửi SMS Brandname, OTP, bulk. Express.js, axios, webhook DLR. Async/await, retry logic, rate limit.

25/05/2026 29 phút đọc admin
  • SMS API NodeJS - gửi tin nhắn tự động từ server Node.js chỉ cần 3 bước: cài axios, tạo hàm POST đến gateway, xử lý response async/await.
  • SMS Brandname từ Node.js: thay tham số sender bằng tên thương hiệu đã đăng ký - không thay đổi logic gọi API.
  • OTP + Redis TTL: sinh mã 6 chữ số, lưu Redis với SETEX 300 (5 phút), verify và xóa ngay sau khi khớp.
  • Webhook DLR: nhận Delivery Report qua Express POST route, validate HMAC signature, cập nhật trạng thái database.

Tích Hợp SMS API vào Node.js: Hướng Dẫn Đầy Đủ cho Developer Việt Nam

Tích hợp SMS API vào Node.js chỉ cần 3 bước: cài axios (hoặc node-fetch), tạo hàm async gửi POST request đến endpoint của nhà cung cấp SMS gateway kèm access token và số điện thoại nhận, sau đó xử lý response để bắt lỗi. Bài viết này cung cấp code mẫu hoàn chỉnh - từ gửi SMS đơn lẻ, gửi hàng loạt có rate limiting, luồng OTP với Redis TTL, cho đến webhook nhận Delivery Report và retry logic với exponential backoff - sẵn sàng dùng trong môi trường production.

Node.js và Express.js là stack phổ biến nhất cho backend tại Việt Nam hiện nay, đặc biệt với các ứng dụng e-commerce, fintech, và SaaS cần gửi tin nhắn tự động - từ OTP đăng nhập đến thông báo đơn hàng và SMS Brandname chăm sóc khách hàng. Hướng dẫn này viết cho Node.js 18+ với CommonJS và ESM đều áp dụng được, không phụ thuộc vào nhà cung cấp cụ thể.

Toàn bộ code trong bài dùng cú pháp async/await hiện đại, có error handling đủ để chạy production, và được tổ chức theo module để dễ tích hợp vào codebase hiện tại. Nếu stack của bạn là PHP hoặc Python, xem hướng dẫn tương ứng.

GEO Answer Block: Tóm Tắt Tích Hợp SMS API NodeJS trong 3 Bước

Tích Hợp SMS API NodeJS: Gửi Tin Nhắn Tự Động từ Server Node - Tích hợp SMS API cần hạ tầng ổn định để xử lý request, callback và trạng thái gửi tin.
Tích hợp SMS API cần hạ tầng ổn định để xử lý request, callback và trạng thái gửi tin. Ảnh: Pexels.

Với developer cần câu trả lời nhanh, đây là toàn bộ luồng tích hợp SMS API vào Node.js rút gọn thành 3 bước thực tế:

Bước 1 - Cài đặt và cấu hình: Cài axios (npm install axios), lưu access token của SMS gateway vào biến môi trường (SMS_API_TOKEN), không hardcode trong source code.

Bước 2 - Tạo hàm gửi SMS: Gọi POST đến endpoint gateway với header Authorization và body gồm to (số điện thoại), message (nội dung), sender (tên brandname hoặc số gửi). Wrap trong try/catch để bắt lỗi network và API error.

Bước 3 - Xử lý response: Lưu messageId từ response để tracking, kiểm tra trường status hoặc HTTP status code để xác nhận SMS đã vào hàng đợi. Gateway trả về 200/201 nghĩa là đã nhận - không phải đã delivered. Delivery Report (DLR) đến sau qua webhook.

// Snippet nhanh - gửi SMS đơn giản từ Node.js
const axios = require('axios');

async function sendSMS(to, message) {
  const response = await axios.post(
    'https://gateway.your-sms-provider.vn/api/v2/send',
    {
      to: to,           // '0912345678' hoặc '84912345678'
      message: message,
      sender: process.env.SMS_SENDER_NAME  // Brandname đã đăng ký
    },
    {
      headers: {
        'Authorization': `Bearer ${process.env.SMS_API_TOKEN}`,
        'Content-Type': 'application/json'
      },
      timeout: 10000  // 10 giây timeout
    }
  );
  return response.data; // { messageId, status, ... }
}

So Sánh 3 HTTP Client Phổ Biến cho SMS API Node.js

Node.js không có HTTP client tích hợp sẵn đủ mạnh cho production use. Ba lựa chọn phổ biến nhất khi gọi SMS API là axios, node-fetch, và got - mỗi cái có trade-off khác nhau.

Tiêu chí axios node-fetch got
Module system CJS + ESM ESM only (v3+) ESM only (v12+)
Auto JSON parse Co sẵn Cần .json() thủ công Co sẵn
Interceptor / middleware Co sẵn Không Hooks API
Retry tích hợp sẵn Không (cần axios-retry) Không Co sẵn (got.extend)
Bundle size ~13KB ~3KB ~50KB
Phù hợp nhất cho Express.js CJS, dự án mới ESM project, serverless Khi cần retry phức tạp

Khuyến nghị thực tế: Với Express.js CommonJS (require/module.exports) - dùng axios. Với Next.js hoặc dự án ESM thuần - dùng node-fetch v2 (CJS compatible) hoặc native fetch có sẵn từ Node.js 18+. Khi cần retry logic phức tạp mà không muốn tự viết - dùng got nhưng phải migrate sang ESM.

Cài Đặt Môi Trường và Cấu Hình Dự Án

Tích Hợp SMS API NodeJS: Gửi Tin Nhắn Tự Động từ Server Node - Developer nên kiểm thử API với payload mẫu, lỗi xác thực và phản hồi DLR trước khi chạy thật.
Developer nên kiểm thử API với payload mẫu, lỗi xác thực và phản hồi DLR trước khi chạy thật. Ảnh: Pexels.

Trước khi viết code, cần chuẩn bị môi trường đúng cách để tránh lộ credential và dễ chuyển giữa staging/production.

Yêu cầu tối thiểu: Node.js 18 LTS (để có native fetchcrypto.randomInt), npm hoặc yarn, và tài khoản SMS gateway với access token. Cài packages cần thiết:

# Khởi tạo dự án
npm init -y

# HTTP client + utility
npm install axios

# Cho Express integration
npm install express

# Cho OTP với Redis
npm install ioredis

# Cho rate limiting bulk SMS
npm install p-limit

# Cho logging production
npm install winston

# Cho testing
npm install --save-dev jest axios-mock-adapter

Tạo file .env ở root và thêm vào .gitignore ngay. Không bao giờ commit access token vào git.

# .env - KHÔNG commit file này
SMS_API_TOKEN=your_access_token_here
SMS_API_BASE_URL=https://gateway.your-sms-provider.vn/api/v2
SMS_SENDER_NAME=YOURBRAND   # Tên brandname đã đăng ký
REDIS_URL=redis://localhost:6379
NODE_ENV=development

Code Mẫu Node.js Thuần: Gửi SMS Đơn Lẻ với Error Handling Đầy Đủ

Đây là module SMS client cơ bản - đóng gói toàn bộ logic gọi API vào 1 file, dễ import vào bất kỳ service nào. Phân biệt rõ 3 loại lỗi: lỗi validation (số điện thoại sai), lỗi API (4xx), lỗi network (timeout, connection refused).

// sms-client.js
require('dotenv').config();
const axios = require('axios');

const smsClient = axios.create({
  baseURL: process.env.SMS_API_BASE_URL,
  timeout: 15000,
  headers: {
    'Authorization': `Bearer ${process.env.SMS_API_TOKEN}`,
    'Content-Type': 'application/json'
  }
});

/**
 * Chuẩn hóa số điện thoại Việt Nam về dạng E.164
 * 0912345678 → 84912345678
 */
function normalizePhone(phone) {
  const cleaned = phone.replace(/\D/g, '');
  if (cleaned.startsWith('84')) return cleaned;
  if (cleaned.startsWith('0') && cleaned.length === 10) {
    return '84' + cleaned.slice(1);
  }
  throw new Error(`Số điện thoại không hợp lệ: ${phone}`);
}

/**
 * Gửi SMS đơn lẻ
 * @param {string} to  - Số điện thoại nhận (0912345678 hoặc 84912345678)
 * @param {string} message - Nội dung tin nhắn (tối đa 160 ký tự/SMS đơn)
 * @param {object} options - { sender, type } ghi đè mặc định
 * @returns {Promise<{messageId, status, cost}>}
 */
async function sendSMS(to, message, options = {}) {
  const phone = normalizePhone(to);

  if (!message || message.trim().length === 0) {
    throw new Error('Nội dung tin nhắn không được để trống');
  }

  const payload = {
    to: phone,
    message: message.trim(),
    sender: options.sender || process.env.SMS_SENDER_NAME,
    type: options.type || 2  // 2 = SMS Brandname, 1 = quảng cáo
  };

  try {
    const response = await smsClient.post('/send', payload);

    const result = response.data;

    // Kiểm tra kết quả từ gateway (cấu trúc response tùy nhà cung cấp)
    if (result.error_code !== 0 && result.error_code !== undefined) {
      throw new Error(`Gateway error ${result.error_code}: ${result.error_message}`);
    }

    return {
      messageId: result.message_id || result.id,
      status: result.status || 'queued',
      cost: result.cost || null
    };

  } catch (error) {
    if (error.response) {
      // Lỗi HTTP từ gateway (4xx, 5xx)
      const { status, data } = error.response;
      throw new Error(`SMS API error ${status}: ${JSON.stringify(data)}`);
    }
    if (error.request) {
      // Gửi request nhưng không nhận response (timeout, network)
      throw new Error(`SMS gateway không phản hồi (timeout/network): ${error.message}`);
    }
    // Lỗi khác (validation, config)
    throw error;
  }
}

module.exports = { sendSMS, normalizePhone };

Lỗi phổ biến nhất developer mắc phải: không chuẩn hóa số điện thoại về E.164 trước khi gửi. Số 0912345678 sẽ bị gateway từ chối hoặc gửi ra ngoài nước nếu không có prefix quốc gia 84. Module normalizePhone ở trên giải quyết vấn đề này.

Gửi SMS Brandname Hàng Loạt với Rate Limiting

Tích Hợp SMS API NodeJS: Gửi Tin Nhắn Tự Động từ Server Node - SMS Gateway là lớp kết nối giữa ứng dụng doanh nghiệp và hạ tầng gửi tin của nhà mạng.
SMS Gateway là lớp kết nối giữa ứng dụng doanh nghiệp và hạ tầng gửi tin của nhà mạng. Ảnh: Pexels.

Với SMS Brandname hàng loạt - thông báo khuyến mãi, chăm sóc khách hàng theo segment - cần kiểm soát concurrency để không vượt giới hạn API của nhà cung cấp (thường 50-200 request/giây). Gửi tất cả song song bằng Promise.all sẽ dẫn đến lỗi 429 (Too Many Requests).

Developer cần biết: hầu hết SMS gateway tại Việt Nam giới hạn 10-50 concurrent request. Một số nhà cung cấp tính phí retry nếu lỗi do bạn gửi quá nhanh. Dùng p-limit để kiểm soát concurrency, kết hợp delay giữa batch:

// bulk-sms.js
const pLimit = require('p-limit');
const { sendSMS } = require('./sms-client');

/**
 * Gửi SMS hàng loạt với rate limiting
 * @param {Array<{phone, message}>} recipients - Danh sách người nhận
 * @param {object} options
 * @param {number} options.concurrency - Số request đồng thời tối đa (mặc định: 5)
 * @param {number} options.delayMs - Delay giữa các batch (mặc định: 200ms)
 * @param {string} options.sender - Tên brandname
 */
async function sendBulkSMS(recipients, options = {}) {
  const {
    concurrency = 5,
    delayMs = 200,
    sender = process.env.SMS_SENDER_NAME
  } = options;

  const limit = pLimit(concurrency);

  const results = {
    success: [],
    failed: [],
    total: recipients.length
  };

  // Chia thành batch 100 số/lần
  const batchSize = 100;
  const batches = [];
  for (let i = 0; i < recipients.length; i += batchSize) {
    batches.push(recipients.slice(i, i + batchSize));
  }

  console.log(`Gửi ${recipients.length} SMS, ${batches.length} batch, concurrency=${concurrency}`);

  for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
    const batch = batches[batchIndex];
    console.log(`Batch ${batchIndex + 1}/${batches.length}: ${batch.length} số`);

    const batchPromises = batch.map(({ phone, message }) =>
      limit(async () => {
        try {
          const result = await sendSMS(phone, message, { sender });
          results.success.push({ phone, messageId: result.messageId });
        } catch (error) {
          results.failed.push({ phone, error: error.message });
        }
      })
    );

    await Promise.all(batchPromises);

    // Delay giữa batch (trừ batch cuối)
    if (batchIndex < batches.length - 1) {
      await new Promise(resolve => setTimeout(resolve, delayMs));
    }
  }

  console.log(`Hoàn thành: ${results.success.length} thành công, ${results.failed.length} thất bại`);
  return results;
}

// Sử dụng
async function main() {
  const recipients = [
    { phone: '0912345678', message: 'BRAND: Ưu đãi đặc biệt 20% hôm nay. Xem: https://bit.ly/abc' },
    { phone: '0987654321', message: 'BRAND: Ưu đãi đặc biệt 20% hôm nay. Xem: https://bit.ly/abc' },
    // ... thêm số điện thoại
  ];

  const result = await sendBulkSMS(recipients, {
    concurrency: 10,
    delayMs: 300,
    sender: 'MYBRAND'
  });

  console.log(`Báo cáo: ${result.success.length}/${result.total} thành công`);
  if (result.failed.length > 0) {
    console.log('Thất bại:', result.failed);
  }
}

module.exports = { sendBulkSMS };

Luồng SMS OTP với Redis Cache TTL

SMS OTP là use case phổ biến nhất khi tích hợp SMS API vào Node.js - xác thực đăng nhập 2FA, xác nhận giao dịch, đăng ký tài khoản. Logic cần đảm bảo 3 điều: OTP hết hạn tự động, OTP bị vô hiệu sau khi verify thành công (không dùng lại được), và giới hạn số lần thử sai.

Với developer tích hợp OTP lần đầu, lỗi thường gặp nhất là dùng database SQL để lưu OTP thay vì Redis. OTP là dữ liệu tạm thời với TTL - đây là use case Redis được sinh ra để xử lý. SQL cần cleanup job định kỳ; Redis tự xóa sau TTL.

// otp-service.js
const Redis = require('ioredis');
const { sendSMS } = require('./sms-client');

const redis = new Redis(process.env.REDIS_URL);

const OTP_TTL_SECONDS = 300;    // 5 phút
const MAX_VERIFY_ATTEMPTS = 5;  // Tối đa 5 lần thử
const OTP_LENGTH = 6;

/**
 * Sinh OTP ngẫu nhiên 6 chữ số
 * Dùng crypto.randomInt thay Math.random() - an toàn hơn
 */
function generateOTP() {
  const { randomInt } = require('crypto');
  return String(randomInt(100000, 999999));
}

/**
 * Gửi OTP về số điện thoại và lưu vào Redis
 * Key pattern: otp:{phone}
 */
async function sendOTP(phone, purpose = 'login') {
  const otp = generateOTP();
  const key = `otp:${phone}`;
  const attemptsKey = `otp_attempts:${phone}`;

  // Kiểm tra có OTP chưa hết hạn không (chống spam)
  const existing = await redis.get(key);
  if (existing) {
    const ttl = await redis.ttl(key);
    throw new Error(`OTP đã được gửi. Thử lại sau ${ttl} giây.`);
  }

  // Lưu OTP vào Redis với TTL 5 phút
  const otpData = JSON.stringify({
    code: otp,
    phone: phone,
    purpose: purpose,
    createdAt: Date.now()
  });

  await redis.setex(key, OTP_TTL_SECONDS, otpData);

  // Reset bộ đếm số lần thử sai
  await redis.del(attemptsKey);

  // Gửi SMS
  const message = `[${process.env.SMS_SENDER_NAME}] Ma xac thuc cua ban la: ${otp}. Co hieu luc trong 5 phut. Khong chia se ma nay cho bat ky ai.`;

  try {
    const result = await sendSMS(phone, message, { type: 6 }); // type 6 = OTP/transactional
    return {
      success: true,
      messageId: result.messageId,
      expiresIn: OTP_TTL_SECONDS
    };
  } catch (smsError) {
    // Nếu gửi SMS thất bại, xóa OTP khỏi Redis
    await redis.del(key);
    throw smsError;
  }
}

/**
 * Verify OTP - trả về true nếu đúng, throw nếu sai/hết hạn
 */
async function verifyOTP(phone, inputCode) {
  const key = `otp:${phone}`;
  const attemptsKey = `otp_attempts:${phone}`;

  // Kiểm tra số lần thử sai
  const attempts = parseInt(await redis.get(attemptsKey) || '0');
  if (attempts >= MAX_VERIFY_ATTEMPTS) {
    throw new Error(`Đã thử sai ${MAX_VERIFY_ATTEMPTS} lần. Yêu cầu OTP mới.`);
  }

  // Lấy OTP từ Redis
  const stored = await redis.get(key);
  if (!stored) {
    throw new Error('OTP không tồn tại hoặc đã hết hạn.');
  }

  const otpData = JSON.parse(stored);

  if (otpData.code !== inputCode.trim()) {
    // Tăng bộ đếm thử sai, TTL 10 phút
    await redis.incr(attemptsKey);
    await redis.expire(attemptsKey, 600);
    const remaining = MAX_VERIFY_ATTEMPTS - attempts - 1;
    throw new Error(`OTP không đúng. Còn ${remaining} lần thử.`);
  }

  // OTP đúng - xóa ngay để không dùng lại được
  await redis.del(key);
  await redis.del(attemptsKey);

  return {
    success: true,
    phone: otpData.phone,
    purpose: otpData.purpose
  };
}

module.exports = { sendOTP, verifyOTP };

Tích Hợp với Express.js: Route Handler và Middleware

Phần lớn dự án Node.js dùng Express.js làm web framework. Dưới đây là pattern tổ chức route cho SMS - tách biệt validation, business logic và response formatting.

Với developer Express.js, điều quan trọng là đặt SMS logic vào service layer (không viết trực tiếp trong route handler) để dễ test và tái sử dụng. Route handler chỉ làm nhiệm vụ: validate input, gọi service, format response.

// routes/sms.routes.js
const express = require('express');
const router = express.Router();
const { sendOTP, verifyOTP } = require('../services/otp-service');
const { sendSMS } = require('../services/sms-client');

// Middleware kiểm tra API key nội bộ (gọi từ service khác trong hệ thống)
function requireInternalAuth(req, res, next) {
  const token = req.headers['x-internal-token'];
  if (token !== process.env.INTERNAL_API_TOKEN) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  next();
}

// Middleware validate số điện thoại Việt Nam
function validatePhone(req, res, next) {
  const phone = req.body.phone || req.body.to;
  if (!phone) {
    return res.status(400).json({ error: 'Thiếu số điện thoại' });
  }
  const cleaned = String(phone).replace(/\D/g, '');
  const valid = /^(84|0)(3|5|7|8|9)[0-9]{8}$/.test(cleaned);
  if (!valid) {
    return res.status(400).json({ error: 'Số điện thoại không hợp lệ' });
  }
  req.body.phone = cleaned;
  next();
}

/**
 * POST /api/sms/otp/send
 * Body: { phone, purpose? }
 */
router.post('/otp/send', validatePhone, async (req, res) => {
  const { phone, purpose = 'login' } = req.body;

  try {
    const result = await sendOTP(phone, purpose);
    res.json({
      success: true,
      message: 'OTP đã được gửi',
      expiresIn: result.expiresIn,
      messageId: result.messageId
    });
  } catch (error) {
    const statusCode = error.message.includes('đã được gửi') ? 429 : 500;
    res.status(statusCode).json({ error: error.message });
  }
});

/**
 * POST /api/sms/otp/verify
 * Body: { phone, code }
 */
router.post('/otp/verify', validatePhone, async (req, res) => {
  const { phone, code } = req.body;

  if (!code || !/^\d{6}$/.test(code)) {
    return res.status(400).json({ error: 'Mã OTP phải là 6 chữ số' });
  }

  try {
    const result = await verifyOTP(phone, code);
    res.json({ success: true, phone: result.phone });
  } catch (error) {
    const statusCode = error.message.includes('hết hạn') ? 410 : 400;
    res.status(statusCode).json({ error: error.message });
  }
});

/**
 * POST /api/sms/send
 * Body: { to, message, sender? } - Yêu cầu internal auth
 */
router.post('/send', requireInternalAuth, validatePhone, async (req, res) => {
  const { phone, message, sender } = req.body;

  if (!message || message.trim().length === 0) {
    return res.status(400).json({ error: 'Nội dung tin nhắn không được để trống' });
  }

  try {
    const result = await sendSMS(phone, message, { sender });
    res.json({ success: true, messageId: result.messageId, status: result.status });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

module.exports = router;

// app.js - Đăng ký route
// const smsRoutes = require('./routes/sms.routes');
// app.use('/api/sms', express.json(), smsRoutes);

Webhook Nhận Delivery Report (DLR) - Validate Signature và Xử Lý Trạng Thái

Delivery Report (DLR) là HTTP callback mà SMS gateway gửi về server của bạn khi tin nhắn thay đổi trạng thái. Node.js/Express cần 1 route POST công khai để nhận DLR. Đây là điểm nhiều developer bỏ qua, dẫn đến không biết SMS thất bại để xử lý fallback.

Các trạng thái DLR cần xử lý: DELIVERED (thiết bị đã nhận), UNDELIVERED (không gửi được), EXPIRED (hết hạn validity period), FAILED (lỗi hệ thống), BUFFERED (đang chờ thiết bị online). Với UNDELIVERED và EXPIRED, cân nhắc fallback sang Zalo ZNS hoặc email.

// routes/webhook.routes.js
const express = require('express');
const router = express.Router();
const crypto = require('crypto');

/**
 * Validate HMAC signature từ SMS gateway
 * Mỗi nhà cung cấp có cách ký khác nhau - đọc docs để biết:
 * - Header chứa signature (VD: X-SMS-Signature, X-Webhook-Secret)
 * - Thuật toán: HMAC-SHA256 phổ biến nhất
 * - Payload dùng để ký: raw body (KHÔNG parse JSON trước)
 */
function validateWebhookSignature(req, res, next) {
  const signature = req.headers['x-sms-signature'];
  const webhookSecret = process.env.SMS_WEBHOOK_SECRET;

  if (!webhookSecret) {
    // Nếu nhà cung cấp không có signature - bỏ qua (kém an toàn hơn)
    console.warn('WARNING: SMS_WEBHOOK_SECRET chưa cấu hình');
    return next();
  }

  if (!signature) {
    return res.status(401).json({ error: 'Missing signature' });
  }

  // Tính HMAC từ raw body
  const expectedSig = crypto
    .createHmac('sha256', webhookSecret)
    .update(req.rawBody || JSON.stringify(req.body))
    .digest('hex');

  const isValid = crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expectedSig, 'hex')
  );

  if (!isValid) {
    console.error('Invalid webhook signature - có thể là request giả mạo');
    return res.status(403).json({ error: 'Invalid signature' });
  }

  next();
}

/**
 * POST /webhook/sms/dlr
 * Nhận Delivery Report từ SMS gateway
 *
 * Lưu ý: URL này phải public (không qua auth middleware)
 * Cần cấu hình URL này trong dashboard của nhà cung cấp
 */
router.post('/sms/dlr', validateWebhookSignature, async (req, res) => {
  // Trả 200 ngay lập tức để gateway không retry
  res.status(200).json({ received: true });

  // Xử lý bất đồng bộ sau khi đã response
  setImmediate(async () => {
    try {
      const dlr = req.body;

      // Map trạng thái về dạng chuẩn (tùy gateway có tên khác nhau)
      const statusMap = {
        'DELIVERED': 'delivered',
        'UNDELIVERED': 'failed',
        'EXPIRED': 'expired',
        'FAILED': 'failed',
        'BUFFERED': 'pending',
        '1': 'delivered',   // Một số gateway dùng mã số
        '2': 'failed',
        '8': 'expired'
      };

      const messageId = dlr.message_id || dlr.messageId || dlr.msgid;
      const rawStatus = dlr.status || dlr.dlr_status || dlr.delivery_status;
      const status = statusMap[rawStatus] || 'unknown';
      const deliveredAt = dlr.delivered_at || new Date().toISOString();

      console.log(`DLR received: messageId=${messageId}, status=${status}`);

      // Cập nhật database (ví dụ với Sequelize/Prisma)
      // await SmsLog.update(
      //   { status, deliveredAt, rawDlr: JSON.stringify(dlr) },
      //   { where: { messageId } }
      // );

      // Nếu thất bại - trigger fallback
      if (status === 'failed' || status === 'expired') {
        console.log(`SMS thất bại cho messageId=${messageId}, cân nhắc fallback`);
        // await triggerFallback(messageId); // Gửi email hoặc Zalo ZNS
      }

    } catch (err) {
      console.error('Lỗi xử lý DLR:', err);
    }
  });
});

module.exports = router;

Retry Logic với Exponential Backoff khi API Timeout

SMS gateway đôi khi trả về lỗi 429 (rate limit) hoặc 5xx (server overload) - đặc biệt trong giờ cao điểm. Retry ngay lập tức làm vấn đề tệ hơn. Exponential backoff với jitter là pattern chuẩn: mỗi lần retry chờ lâu hơn gấp đôi, cộng thêm delay ngẫu nhiên để tránh nhiều process retry đồng thời.

// retry.js
const { sendSMS } = require('./sms-client');

/**
 * Retry với exponential backoff + jitter
 * Chỉ retry với lỗi tạm thời (429, 503, timeout)
 * Không retry với lỗi cố định (401, 400, số điện thoại sai)
 */
async function sendSMSWithRetry(to, message, options = {}) {
  const {
    maxRetries = 3,
    baseDelay = 1000,    // 1 giây
    maxDelay = 30000,    // 30 giây tối đa
    sender = undefined
  } = options;

  // Lỗi không nên retry
  const NON_RETRYABLE_CODES = [400, 401, 403, 404, 422];

  let lastError;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const result = await sendSMS(to, message, { sender });
      if (attempt > 0) {
        console.log(`SMS gửi thành công sau ${attempt} lần retry`);
      }
      return result;

    } catch (error) {
      lastError = error;

      // Kiểm tra có nên retry không
      const statusCode = error.response?.status;
      if (statusCode && NON_RETRYABLE_CODES.includes(statusCode)) {
        throw error; // Không retry
      }

      if (attempt === maxRetries) {
        break; // Hết lần retry
      }

      // Tính delay: base * 2^attempt + jitter ngẫu nhiên
      const exponentialDelay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
      const jitter = Math.random() * 1000; // ±1 giây ngẫu nhiên
      const delay = exponentialDelay + jitter;

      console.log(
        `Attempt ${attempt + 1} thất bại: ${error.message}. ` +
        `Retry sau ${Math.round(delay / 1000)}s...`
      );

      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  throw new Error(
    `Gửi SMS thất bại sau ${maxRetries} lần retry. Lỗi cuối: ${lastError.message}`
  );
}

// Sử dụng
async function example() {
  try {
    const result = await sendSMSWithRetry(
      '0912345678',
      'Mã xác thực của bạn là 123456',
      { maxRetries: 3, baseDelay: 1000 }
    );
    console.log('Thành công:', result.messageId);
  } catch (error) {
    console.error('Thất bại sau tất cả retry:', error.message);
    // Log vào hệ thống monitoring, alert on-call nếu cần
  }
}

module.exports = { sendSMSWithRetry };

Testing và Monitoring: Jest Mock và Winston Logger

Test SMS service mà không gọi API thực là yêu cầu cơ bản trong CI/CD pipeline. Jest + axios-mock-adapter cho phép mock toàn bộ HTTP call, kiểm tra error handling mà không tốn SMS credit. Kết hợp với Winston để log có cấu trúc giúp debug production.

Với team có CI/CD (GitHub Actions, GitLab CI), bắt buộc mock SMS call trong test environment. Không mock = test cần real credential = không chạy được trong CI = test bị bỏ qua = bug lọt production.

// __tests__/sms-client.test.js
const axios = require('axios');
const MockAdapter = require('axios-mock-adapter');
const { sendSMS, normalizePhone } = require('../sms-client');

const mock = new MockAdapter(axios);

describe('SMS Client', () => {
  afterEach(() => {
    mock.reset();
  });

  test('Gửi SMS thành công - trả về messageId', async () => {
    mock.onPost('/send').reply(200, {
      error_code: 0,
      message_id: 'msg_abc123',
      status: 'queued'
    });

    const result = await sendSMS('0912345678', 'Test message');

    expect(result.messageId).toBe('msg_abc123');
    expect(result.status).toBe('queued');
  });

  test('Số điện thoại 10 số chuyển về E.164 đúng', () => {
    expect(normalizePhone('0912345678')).toBe('84912345678');
    expect(normalizePhone('84912345678')).toBe('84912345678');
  });

  test('Số điện thoại sai format - throw error', () => {
    expect(() => normalizePhone('12345')).toThrow('không hợp lệ');
  });

  test('API trả lỗi 429 - throw error có status code', async () => {
    mock.onPost('/send').reply(429, {
      error: 'Rate limit exceeded'
    });

    await expect(sendSMS('0912345678', 'Test')).rejects.toThrow('SMS API error 429');
  });

  test('Network timeout - throw error rõ ràng', async () => {
    mock.onPost('/send').timeout();

    await expect(sendSMS('0912345678', 'Test')).rejects.toThrow(
      'SMS gateway không phản hồi'
    );
  });
});

// logger.js - Winston structured logging
const winston = require('winston');

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({
      filename: 'logs/sms-error.log',
      level: 'error'
    }),
    new winston.transports.File({
      filename: 'logs/sms-combined.log'
    })
  ]
});

// Dùng trong sms-client.js thay console.log
// logger.info('SMS sent', { messageId, phone: to, status });
// logger.error('SMS failed', { error: error.message, phone: to });

module.exports = logger;

Câu Hỏi Thường Gặp

Nên dùng axios, node-fetch hay got để gọi SMS API trong Node.js?

Axios là lựa chọn phổ biến nhất cho dự án Node.js hiện tại nhờ cú pháp gọn, tự động parse JSON và interceptor dễ cấu hình. node-fetch phù hợp nếu dự án đã dùng fetch API (isomorphic code) - lưu ý v3+ chỉ hỗ trợ ESM. got là lựa chọn tốt nhất khi cần retry tích hợp sẵn và stream support, nhưng cũng chỉ ESM từ v12+. Với dự án CommonJS/Express mới, axios là lựa chọn an toàn nhất - ít config nhất để chạy được ngay.

SMS Brandname API NodeJS khác gì so với gửi SMS số thường?

SMS Brandname hiển thị tên thương hiệu doanh nghiệp thay vì số điện thoại ở mục người gửi. Về mặt kỹ thuật trong Node.js, bạn chỉ cần thay tham số sender (hoặc from) từ số điện thoại thành tên brandname đã đăng ký - toàn bộ logic gọi API giữ nguyên. Brandname phải đăng ký với Bộ TT&TT và được nhà cung cấp gateway phê duyệt trước khi sử dụng; quá trình này thường mất 3-7 ngày làm việc.

TTL bao nhiêu giây là phù hợp cho OTP lưu trong Redis?

OTP đăng nhập: 300 giây (5 phút) là phổ biến nhất, cân bằng giữa UX và bảo mật. OTP giao dịch tài chính: 120-180 giây (2-3 phút). OTP đăng ký tài khoản: 600 giây (10 phút). NIST SP 800-63B khuyến nghị không quá 10 phút cho mọi trường hợp. Luôn xóa OTP ngay sau khi verify thành công bằng redis.del(key) - không đợi hết TTL tự hết hạn.

Làm thế nào để giới hạn rate khi gửi SMS hàng loạt từ Node.js?

Dùng p-limit hoặc bottleneck để giới hạn concurrency - ví dụ pLimit(5) đảm bảo chỉ 5 request chạy đồng thời. Thêm delay giữa batch (setTimeout 200-500ms). Theo dõi response header X-RateLimit-Remaining nếu nhà cung cấp hỗ trợ. Với danh sách lớn hơn 10.000 số, chia batch 100-500 số/lần và dùng queue (Bull/BullMQ) để xử lý bền vững qua server restart.

Webhook DLR là gì và tại sao Node.js cần nhận DLR?

DLR (Delivery Report) là callback HTTP POST mà SMS gateway gửi về server của bạn khi tin nhắn thay đổi trạng thái: delivered, failed, expired, hoặc undelivered. Node.js/Express tạo route POST nhận DLR để cập nhật trạng thái vào database và kích hoạt retry hoặc fallback sang kênh khác (email, Zalo ZNS) khi SMS thất bại. Không nhận DLR đồng nghĩa với việc bạn không biết bao nhiêu phần trăm SMS thực sự đến tay người dùng.

Exponential backoff cần implement như thế nào trong Node.js?

Công thức: delay = min(baseDelay * 2^attempt, maxDelay) + Math.random() * 1000. Ví dụ với baseDelay=1000ms: lần 1 chờ ~1s, lần 2 ~2s, lần 3 ~4s. Jitter (phần random) tránh thundering herd khi nhiều request retry cùng lúc. Giới hạn tối đa 3-5 lần retry và maxDelay 30 giây. Chỉ retry với lỗi 429 (rate limit) hoặc 5xx (server error) - không retry lỗi 4xx cố định như 400, 401, 403.

Cách test gửi SMS từ Node.js mà không tốn phí thực tế?

Mock axios với Jest (jest.mock('axios') hoặc axios-mock-adapter) để unit test không gọi API thực. Dùng nock để intercept HTTP request trong integration test. Với webhook DLR, dùng ngrok để expose localhost ra internet rồi test bằng curl gửi POST giả lập. Nếu nhà cung cấp SMS gateway có sandbox/test mode - luôn dùng sandbox trong development để không tốn credit và không làm phiền user thật.

Node.js SMS API có khác gì so với tích hợp bằng PHP hay Python không?

Logic nghiệp vụ (endpoint, parameter, authentication) giống nhau vì đều gọi cùng REST API của SMS gateway. Khác biệt ở cú pháp và ecosystem: Node.js dùng axios + async/await, PHP dùng cURL + Guzzle, Python dùng requests/httpx. Node.js có lợi thế khi build real-time app (WebSocket, event loop không chặn) hoặc khi cả frontend lẫn backend cùng dùng JavaScript.

Kết Luận

Tích hợp SMS API vào Node.js không phức tạp nếu có code mẫu đúng pattern. Điểm cần nhớ: chuẩn hóa số điện thoại về E.164 trước khi gửi, lưu OTP vào Redis với TTL thay vì SQL, dùng p-limit cho gửi hàng loạt, implement retry với exponential backoff, và luôn nhận DLR để theo dõi tỷ lệ giao nhận thực tế. Để triển khai vào production, bạn cần tài khoản SMS gateway với SMS Brandname đã được phê duyệt.

Cần tư vấn chọn gói SMS API phù hợp cho dự án Node.js hoặc hỗ trợ kỹ thuật tích hợp? Liên hệ ngay:

Cần tư vấn miễn phí?
Đội ngũ chuyên gia sẵn sàng hỗ trợ bạn qua Zalo ngay hôm nay
Tư vấn miễn phí