MeraOTP developer guides

OTP API

Clear steps, real API formats, and backend-only examples. Read every guide without signing in.

API v1Reviewed 27 October 2026Get integration help

Send a server-generated authentication code, save its message ID, and verify the code entered by your user. OTP uses wallet credit; it does not require a subscription.

1. Prepare one backend helper

Create and verify your MeraOTP account, add credit, and copy the key from the API key page. Set it as the MERAOTP_API_KEY environment variable on your server. Select your language below and copy the helper into your backend integration file. Add the request examples from the next sections after that helper.

Every request sends Authorization: Bearer YOUR_API_KEY. The helper returns the HTTP status separately from the decoded API response: PHP uses http_status and json; Node.js uses httpStatus and json. Those wrapper names are local helper values, not extra fields sent by MeraOTP.

PHP
<?php
// Put this helper at the top of your backend integration file.
function meraotp(string $method, string $path, ?array $payload = null, array $extraHeaders = []): array
{
    $key = getenv('MERAOTP_API_KEY');
    if (!$key) throw new RuntimeException('Set MERAOTP_API_KEY on your server first.');
    $ch = curl_init('https://meraotp.in/api/v1' . $path);
    $headers = array_merge([
        'Authorization: Bearer ' . $key,
        'Accept: application/json',
        'Content-Type: application/json',
    ], $extraHeaders);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_TIMEOUT => 20,
    ]);
    if ($payload !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload, JSON_THROW_ON_ERROR));
    $raw = curl_exec($ch);
    $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error = curl_error($ch);
    curl_close($ch);
    if ($raw === false) throw new RuntimeException('Connection failed: ' . $error);
    // Keep error JSON too: a message_id may be returned with an uncertain send.
    return ['http_status' => $status, 'json' => json_decode($raw, true, 512, JSON_THROW_ON_ERROR)];
}
Node.js
// Save as an .mjs backend file using Node.js with built-in fetch.
async function meraotp(method, path, payload, extraHeaders = {}) {
  const key = process.env.MERAOTP_API_KEY;
  if (!key) throw new Error('Set MERAOTP_API_KEY on your server first.');
  const response = await fetch('https://meraotp.in/api/v1' + path, {
    method,
    headers: {
      'Authorization': `Bearer ${key}`,
      'Accept': 'application/json',
      'Content-Type': 'application/json',
      ...extraHeaders
    },
    body: payload === undefined ? undefined : JSON.stringify(payload),
    signal: AbortSignal.timeout(20000),
    redirect: 'error'
  });
  // HTTP errors do not throw in fetch. Preserve both status and error JSON.
  return { httpStatus: response.status, json: await response.json() };
}
Backend only. Do not paste this key into browser JavaScript, a public WordPress page or a mobile app. Your own backend must authenticate the person requesting a code and apply per-user/phone resend limits.

2. Send an authentication OTP

POST https://meraotp.in/api/v1/otp/send

Send a JSON object with the fields below. MeraOTP generates the code; you do not supply the OTP, a brand variable or a custom SMS message. The registered OTP template has one OTP variable and the fixed Codegully signature.

mobile Required string
One valid Indian mobile number, for example 9876543210. Do not send a list of recipients.
purpose Optional; default login
login, signup, password_reset, transaction, verification or other. Choose the purpose of the user-requested authentication.
otp_length Optional; default 6
Use the integer 4, 5 or 6. For most integrations, leave the six-digit default.
reference Optional string
Your non-secret reference for the attempt. It is not a replacement for the returned message_id or your server-side session binding.
Idempotency-Key Recommended HTTP header
A unique key for this attempt: 8–80 letters, numbers, dots, underscores, colons or hyphens. Save it before sending. Reuse it only when retrying the same request with the same payload.
PHP
// Add this after the meraotp() helper above.
// Generate ONCE per login/signup attempt; save it before sending.
$attemptKey = 'login_' . bin2hex(random_bytes(12));
$reply = meraotp('POST', '/otp/send', [
    'mobile' => '9876543210', // Replace with the consenting user's number.
    'purpose' => 'login',
    'otp_length' => 6,
    'reference' => 'login_attempt_1001',
], ['Idempotency-Key: ' . $attemptKey]);
$result = $reply['json'];
$messageId = $result['data']['message_id'] ?? null;
// Save messageId against THIS user/session and inspect http_status + status.
// A network retry must reuse attemptKey and the same payload.
Node.js
import { randomUUID } from 'node:crypto';
// Add this after the meraotp() helper. Persist the key per attempt.
const attemptKey = 'login_' + randomUUID();
const reply = await meraotp('POST', '/otp/send', {
  mobile: '9876543210', // Replace with the consenting user's number.
  purpose: 'login',
  otp_length: 6,
  reference: 'login_attempt_1001'
}, { 'Idempotency-Key': attemptKey });
const result = reply.json;
const messageId = result.data?.message_id;
// Save messageId against this user/session. Inspect reply.httpStatus + status.
// Reuse attemptKey AND the payload if this same attempt needs a network retry.

Example response: HTTP 200, provider accepted

{
    "success": true,
    "code": "OTP_ACCEPTED",
    "message": "OTP request processed.",
    "data": {
        "message_id": "8f21c6f06f7d4a88a1c6b6ca8c8d3a20",
        "status": "accepted",
        "recipient": "******3210",
        "purpose": "login",
        "otp_length": 6,
        "idempotency_key": "login_example_attempt_1001",
        "cost": "0.69",
        "currency": "INR",
        "replayed": false,
        "created_at": "2026-10-27 11:00:00"
    }
}

message_id identifies this challenge. Save it on your server with the expected phone number, user/session and intended action. accepted means the provider accepted the send; it is not proof that the handset received it or that the person entered the code. The sample cost above is illustrative, not your current account rate.

replayed: true means an existing request was returned for the same idempotency key. If the request is still being processed or reconciled, the API can return HTTP 202 instead. Preserve its message ID and inspect the state rather than creating another send with a new key.

3. Verify the code your user enters

POST https://meraotp.in/api/v1/otp/verify
message_id Required
The 32-character hexadecimal ID returned by Send OTP, loaded from the current server-side attempt.
otp Required string
The user's entered 4, 5 or 6-digit code. Keep it a string. Codes expire after 10 minutes and verification attempts are limited.
PHP
// Load message_id from YOUR server-side login attempt, not another user's input.
$reply = meraotp('POST', '/otp/verify', [
    'message_id' => '8f21c6f06f7d4a88a1c6b6ca8c8d3a20',
    'otp' => '123456', // The code entered by this user; keep it as a string.
]);
$result = $reply['json'];
$verified = $reply['http_status'] === 200
    && ($result['success'] ?? false) === true
    && ($result['data']['verified'] ?? false) === true;
// Grant access only if verified AND the challenge belongs to this session.
// Mark your own login attempt consumed so it cannot unlock a second action.
Node.js
const reply = await meraotp('POST', '/otp/verify', {
  message_id: '8f21c6f06f7d4a88a1c6b6ca8c8d3a20', // From your server session.
  otp: '123456' // The string entered by this user.
});
const verified = reply.httpStatus === 200
  && reply.json.success === true
  && reply.json.data?.verified === true;
// Grant access only if verified and the challenge belongs to this session.
// Consume your own login attempt once; do not treat a repeated check as a new login.

Example verified response: HTTP 200

{
    "success": true,
    "code": "OTP_VERIFIED",
    "message": "OTP verified successfully.",
    "data": {
        "message_id": "8f21c6f06f7d4a88a1c6b6ca8c8d3a20",
        "verified": true,
        "verified_at": "2026-10-27 11:01:00"
    }
}

Grant access only when the authenticated response confirms data.verified === true and the challenge belongs to the current user's intended action. Mark your application attempt consumed and establish or rotate your own login session securely.

A repeat verification of an already verified challenge can return ALREADY_VERIFIED. That is not permission to reuse the same challenge for another account, a fresh login or a different transaction. Your application must enforce its own one-action binding.

4. Read delivery or verification status

GET https://meraotp.in/api/v1/otp/status/MESSAGE_ID

Send the same Bearer key. No request body is needed. Replace MESSAGE_ID with the ID saved for this attempt; status calls do not generate a new OTP.

PHP
$messageId = '8f21c6f06f7d4a88a1c6b6ca8c8d3a20';
$reply = meraotp('GET', '/otp/status/' . rawurlencode($messageId));
$result = $reply['json'];
$status = $result['data']['status'] ?? null;
// Read this status only when http_status is 200 and success is true.
Node.js
const messageId = '8f21c6f06f7d4a88a1c6b6ca8c8d3a20';
const reply = await meraotp('GET', '/otp/status/' + encodeURIComponent(messageId));
const status = reply.json.data?.status;
// Read this status only when httpStatus is 200 and success is true.

Example status response: HTTP 200

{
    "success": true,
    "code": "OTP_STATUS",
    "message": "OTP status retrieved.",
    "data": {
        "message_id": "8f21c6f06f7d4a88a1c6b6ca8c8d3a20",
        "status": "verified",
        "recipient": "******3210",
        "purpose": "login",
        "otp_length": 6,
        "cost": "0.69",
        "currency": "INR",
        "accepted_at": "2026-10-27 11:00:00",
        "delivered_at": null,
        "verified_at": "2026-10-27 11:01:00",
        "failure_code": null,
        "created_at": "2026-10-27 11:00:00",
        "updated_at": "2026-10-27 11:01:00"
    }
}
StatusMeaning
acceptedThe provider accepted the SMS request. It does not prove delivery.
deliveredA provider delivery report confirmed delivery. It does not authenticate the person by itself.
verifiedThe code was successfully verified. Still enforce your own session and action binding.
queued / unknownThe send is processing or its provider outcome needs reconciliation. Do not blindly duplicate it.
failedThe request failed or was rejected. Read the error/failure code before retrying.
expiredThe code can no longer be used. A fresh user-requested attempt needs a new key.

Retries, costs and the first test

Use one idempotency key per intended OTP attempt, not one key per HTTP retry. If the connection times out, first check any returned message ID or retry the same payload with the saved key. Changing the key could generate a second SMS. Changing the payload while reusing a key returns IDEMPOTENCY_CONFLICT.

If the account has insufficient credit, add credit before trying again. Never create an unbounded retry loop. For an expired code that the person explicitly requests to resend, create a new attempt with a new key and respect a resend cooldown.

  1. Send one OTP to your own consenting number and save the response.
  2. Enter a wrong code once and confirm your application denies access.
  3. Enter the correct code and confirm the intended user is authenticated.
  4. Refresh/repeat the verification and confirm it does not authenticate a different action.
  5. Review wallet activity and masked OTP records in the panel.

Provider-accepted OTP sends consume credit at the displayed account rate; delivery timing depends on operators and the recipient's network. For error meanings, see troubleshooting. For app/session protections, read security and safe retries.

Need a hand?

Send support the endpoint, request time, error code and a non-secret reference. Never send your API key, password, webhook secret or an OTP.

Contact developer support