03-7440020+1 917-277-5362+44 203 146-8524
Log in עברית
Sign up free
DEVELOPERS

OTP libraries for Node.js, Python and PHP

Send an SMS verification code and check it in a few lines, without building the code generation, expiry, attempt limit and resend wait yourself.

Sending a code is one API call (full explanation here). The work is everything around it: generate a random code, store it safely, check it is still valid, block guessing, and not send ten SMS to someone who clicks "resend" ten times. The Meser10 libraries do all of that, and are written to behave the same way in all three languages.

What the libraries do

  • ✓Generate a random code (6 digits by default) and send it by SMS through your Meser10 account
  • ✓Store the code only as a keyed hash (HMAC), never as text
  • ✓A code works once, expires after 5 minutes by default, and locks after 5 wrong attempts
  • ✓A new code for the same number only after a wait (60 seconds by default)
  • ✓Limits are counted with an atomic operation in a store shared by all your servers, so parallel requests cannot get around them
  • ✓No external dependencies (PHP needs only the json and curl extensions), with automated tests on Node 18, 20 and 22, Python 3.8 and 3.12, and PHP 7.4 and 8.3

Install

Node.js, from npm:

npm install @meser10/otp

Python, straight from GitHub:

pip install "git+https://github.com/meser10/meser10-otp#subdirectory=python"

PHP, with Composer:

composer config repositories.meser10-otp vcs https://github.com/meser10/meser10-otp
composer require meser10/otp:^1.0

What you need from Meser10: SMS credit, an API key and an approved sender, up to 11 Latin letters or digits, or a mobile number verified on the account. You also need a random secret of at least 16 characters, the same on all servers, which signs the stored codes.

Node.js

const { Meser10Otp, Meser10Error } = require('@meser10/otp');

const otp = new Meser10Otp({
  apiKey: process.env.MESER10_API_KEY,
  sender: 'MyShop',
  secret: process.env.OTP_SECRET,
  message: 'Your MyShop code is {code}',
});

async function main(userInput) {
  await otp.sendCode('054-123-4567');
  try {
    await otp.verifyCode('054-123-4567', userInput); // true
  } catch (e) {
    if (e instanceof Meser10Error) console.log(e.code); // wrong_code, locked...
  }
}

In Next.js, one route that handles both sending and checking:

// app/api/otp/route.js
import { Meser10Otp, Meser10Error } from '@meser10/otp';
import { redisStore } from '@/lib/otp-store';

const otp = new Meser10Otp({ apiKey: process.env.MESER10_API_KEY, sender: 'MyShop', secret: process.env.OTP_SECRET, store: redisStore });

export async function POST(req) {
  const { phone, code } = await req.json();
  try {
    if (code) return Response.json({ ok: await otp.verifyCode(phone, code) });
    await otp.sendCode(phone);
    return Response.json({ sent: true });
  } catch (e) {
    if (e instanceof Meser10Error) return Response.json({ error: e.code }, { status: 400 });
    throw e;
  }
}

Python

import os
from meser10_otp import Meser10Otp, Meser10Error

otp = Meser10Otp(os.environ["MESER10_API_KEY"], sender="MyShop", secret=os.environ["OTP_SECRET"])

otp.send_code("054-123-4567")
try:
    otp.verify_code("054-123-4567", user_input)   # True
except Meser10Error as e:
    print(e.code)

PHP

use Meser10\Otp\Otp;
use Meser10\Otp\RedisStore;
use Meser10\Otp\Meser10Error;

$redis = new Redis();
$redis->connect('127.0.0.1');

$otp = new Otp([
    'apiKey' => getenv('MESER10_API_KEY'),
    'sender' => 'MyShop',
    'secret' => getenv('OTP_SECRET'),
    'store'  => new RedisStore($redis),
]);

$otp->sendCode('054-123-4567');
try {
    $otp->verifyCode('054-123-4567', $_POST['code'] ?? '');   // true
} catch (Meser10Error $e) {
    echo $e->errorCode;
}

A shared store

On a single long-running server you can use the in-memory default of Node and Python. In every other case, several servers, several workers or serverless, pass a store shared by all of them. The store needs four operations: get, set, delete and an atomic incr. The incr is what keeps the attempt limit and the resend wait intact when several requests arrive at the same moment. The README of each library has a ready Redis store.

Options

OptionDefaultWhat it sets
codeLength6Code length, 4 to 8 digits
ttlSeconds300How long the code is valid
maxAttempts5Wrong attempts before the code locks
resendSeconds60Wait before a new code for the same number
messageYour verification code is {code}The message text. {code} is replaced with the code

In Python the option names are lowercase with underscores, for example code_length and ttl_seconds.

Errors

Every error comes with a fixed code, so you can show the visitor the right message:

CodeWhat happened
invalid_phoneThe number is not an Israeli mobile. Nothing was sent
too_soonA new code was requested before the wait ended
api_errorMeser10 refused the send, for example a sender that is not approved. The reason is in the error message
networkMeser10 could not be reached
no_codeNo active code for this number: none was sent, it was already used, or it expired
expiredThe code expired (usually reported as no_code, because the code is already deleted)
wrong_codeThe code is wrong. The message says how many attempts are left
lockedToo many wrong attempts. A new code is needed
bad_responseAn unexpected reply from the Meser10 server
configA wrong setting, for example a secret shorter than 16 characters, no store in PHP, or a value out of range

Add a limit per IP address and per account on top of the library as well: every code is a paid SMS.

Open source

The code of all three libraries is on GitHub, under the MIT license, with the automated tests:

https://github.com/meser10/meser10-otp

Get an API key

Open a free Meser10 account, top up SMS credit and get your API key and sender.

Sign up freeContact us

Frequently asked questions

How much do the libraries cost?

The libraries are free and open source (MIT license). Each code sent is one SMS from the balance on your Meser10 account, at your SMS rate.

Why do I need a shared store?

The code, the attempt counter and the resend wait have to be visible to every server that handles requests. The default store in Node and Python keeps them in the memory of one process, which only suits a single long-running server. On serverless or several servers, pass a store on Redis or your database.

Why is there no session default in PHP?

A visitor can delete the cookie and reset the session, and the limits with it. So PHP requires a shared store, and a ready RedisStore is included.

Can I send to numbers outside Israel?

By default the libraries accept only Israeli mobile numbers, so a code is not sent to a stranger by mistake. You can pass your own function to parse the number.

Is there a WordPress plugin?

Yes. Meser10 OTP adds a code at sign-up, at WooCommerce checkout and at login.