Back
3 min read1 November 2026
Tutorials

SMS OTP in Tunisia: secure your app's sign-in with Node.js

Generate a verification code, send it by SMS with the Sendimo API, check it and protect it against abuse: a complete Node.js guide.

The Sendimo teamSMS platform in Tunisia

A one-time password (OTP) sent by SMS proves that a user owns the number they typed. It is the most common way to verify a sign-up, a sign-in or a payment. This guide shows how to do it properly in Node.js, from generating the code to checking it.

How it works

  1. The user enters their number.
  2. Your server generates a 6-digit code and keeps a hash of it (never the code itself) with an expiry time.
  3. The code goes out by SMS.
  4. The user types the code; your server compares the hashes and checks the expiry and the number of attempts.

1. Generate and store the code

Use crypto.randomInt, never Math.random, which is not built for security.

import { randomInt, createHash } from "node:crypto";

const hash = (code) => createHash("sha256").update(code).digest("hex");

export function createOtp(phone) {
  const code = String(randomInt(0, 1_000_000)).padStart(6, "0");
  const record = {
    phone,
    codeHash: hash(code),
    expiresAt: Date.now() + 5 * 60 * 1000, // 5 minutes
    attempts: 0,
  };
  return { code, record }; // save record in your database or in Redis
}

2. Send the code by SMS

Node.js 18 and later ship with fetch, so no dependency is needed.

export async function sendOtp(phone, code, requestId) {
  const response = await fetch("https://api.sendimo.tn/v1/messages", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SENDIMO_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": `otp-${requestId}`,
    },
    body: JSON.stringify({
      to: phone, // E.164 format: +21620123456
      from: "SENDIMO",
      text: `Your code is ${code}. It expires in 5 minutes.`,
    }),
  });

  if (!response.ok) {
    const { error } = await response.json();
    throw new Error(`${error.code}: ${error.message}`);
  }
  return response.json(); // { id, status: "queued", segments: 1, cost, ... }
}

The Idempotency-Key stops two codes from going out if your server retries the same request after a network failure.

3. Check the code

const MAX_ATTEMPTS = 5;

export function verifyOtp(record, input) {
  if (Date.now() > record.expiresAt) return { ok: false, reason: "expired" };
  if (record.attempts >= MAX_ATTEMPTS) return { ok: false, reason: "locked" };
  record.attempts += 1; // save it
  if (hash(input.trim()) !== record.codeHash) return { ok: false, reason: "wrong_code" };
  return { ok: true }; // delete the record: a code works only once
}

4. Protect against abuse

An open OTP form can be used to send SMS at your expense. A few simple rules:

  • Limit sends: for example 1 code per minute and 5 per hour for the same number, plus a limit per IP address.
  • Limit attempts: 5 tries per code, then a new code is required.
  • Expire quickly: 5 minutes is enough in most cases.
  • Don't reveal whether the number exists in your database: return the same message every time.

Writing a good OTP message

  • Stay in GSM-7 and under 160 characters to pay for one segment only: no emoji, and in French avoid ê, â, ô and ç, which force Unicode.
  • Put the code first in the message: it shows in the notification without opening the SMS.
  • State how long it is valid and, if needed, "never share this code".

Test before going live

With an sk_test_ key, +21600000001 always ends delivered and +21600000002 always ends failed. Nothing is sent or charged, so you can test a failed delivery as well as a successful one.

How much does an OTP cost?

A one-segment code costs from 0.100 DT to 0.150 DT depending on your plan, so 120 DT for 1,000 verifications with the Pro pack. The details are in our article on what an SMS costs in Tunisia.

Read next

Ready to send your SMS?

Launching soon in Tunisia. Join early access.