DOCS Version 1

Documentation

Point your test environment's SMS traffic at otpmock, then read verification codes from your tests. Most teams are done in ten minutes.

AI Using Claude, Cursor or Copilot?

Your coding assistant can do the whole integration. Paste this prompt into it. It points the assistant to llms-full.txt, a single file with everything it needs: how to detect your SMS provider, the code to add, the API reference and a checklist.

Prompt for your AI assistant
Integrate otpmock into this project's test environment so our end-to-end tests can read SMS verification codes. First read https://otpmock.com/llms-full.txt and follow it exactly. Only change test, CI and staging configuration; production must keep using the real SMS provider. Read the API key from the OTPMOCK_API_KEY environment variable and never hard-code or commit it.

Quickstart

  1. Get an API key

    Create an account and copy your key from the dashboard. Treat it like a password: it reads every message sent to your inbox.

    In the environment where your tests and your test deployment run, set:

    .env.test
    OTPMOCK_URL=https://api.otpmock.com
    OTPMOCK_API_KEY=om_live_...
  2. Send your app's SMS to otpmock

    In the test environment only, point your SMS provider's SDK at otpmock and use your otpmock API key as the provider credential. See Providers.

  3. Read the code in your test

    Use the test helper or call GET /v1/inbox/{phone}/code yourself.

Authentication

Every API request needs your API key. Two forms are accepted:

  • Bearer token, for the otpmock API: Authorization: Bearer om_live_...
  • Provider-native credentials: HTTP Basic (Twilio, Vonage, Plivo, Netgsm), App (Infobip), the AWS SigV4 access key ID (AWS SNS), the Vonage JWT application_id, or the body key (İleti Merkezi). Each provider page says which field takes your otpmock key.

Requests without a valid key get 401. On Twilio-compatible paths the body follows Twilio's error format (code 20003).

Providers

otpmock emulates these SMS provider APIs with their real paths, payloads, responses and errors. Each is tested with the provider's official Node.js SDK and needs one configuration option in your test environment. Open a provider for its exact setup.

ProviderEmulated endpointsSetup
Twilio
twilio
POST /2010-04-01/Accounts/{AccountSid}/Messages.json
POST /v2/Services/{ServiceSid}/Verifications
POST /v2/Services/{ServiceSid}/VerificationCheck
Twilio guide
Vonage
@vonage/server-sdk
POST /sms/json
POST /v1/messages
POST /v2/verify
POST /v2/verify/{request_id}
DELETE /v2/verify/{request_id}
Vonage guide
AWS SNS
@aws-sdk/client-sns
POST / (Action=Publish, PhoneNumber, Message)AWS SNS guide
Telnyx
telnyx
POST /v2/messages
POST /v2/verifications/sms
POST /v2/verifications/by_phone_number/{phone}/actions/verify
Telnyx guide
AWS End User Messaging
@aws-sdk/client-pinpoint-sms-voice-v2
POST / (X-Amz-Target: PinpointSMSVoiceV2.SendTextMessage)AWS End User Messaging guide
Telesign
telesignsdk
POST /v1/messagingTelesign guide
Bandwidth
bandwidth-sdk
POST /api/v2/users/{accountId}/messagesBandwidth guide
Infobip
@infobip-api/sdk
POST /sms/2/text/advanced
POST /sms/3/messages
Infobip guide
Sinch
@sinch/sdk-core
POST /xms/v1/{service_plan_id}/batchesSinch guide
Bird (MessageBird)
@messagebird/sdk
POST /v1/sms/messagesBird (MessageBird) guide
Plivo
plivo
POST /v1/Account/{auth_id}/Message/Plivo guide
Netgsm
@netgsm/sms
POST /sms/rest/v2/send
POST /sms/rest/v2/otp
Netgsm guide
Verimor
HTTP API
GET /v2/send
POST /v2/send.json
Verimor guide
Mutlucell
HTTP API
POST /smsgw-ws/sndblkexMutlucell guide
İleti Merkezi
@iletimerkezi/iletimerkezi-node
POST /v1/send-sms/jsonİleti Merkezi guide

Example: Twilio for Node.js

The official twilio package (v5) accepts a custom HTTP client. This one keeps every path and rewrites the host, so both api.twilio.com and verify.twilio.com traffic lands on otpmock.

Download
curl -O https://otpmock.com/sdk/twilio-node-client.mjs
src/sms.js
import twilio from "twilio";
import { OtpMockHttpClient } from "./twilio-node-client.mjs";

export const sms = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN, {
  httpClient: process.env.OTPMOCK_URL ? new OtpMockHttpClient(process.env.OTPMOCK_URL) : undefined,
});

In the test environment, set TWILIO_AUTH_TOKEN to your otpmock API key. sms.messages.create(...), sms.verify.v2.services(sid).verifications.create(...) and verificationChecks.create(...) then work unchanged.

Other languages

We test against each provider's Node.js SDK. In other languages, configure the provider client's host or base URL to https://api.otpmock.com and keep paths intact. For Twilio:

Twilio hostReplace with
https://api.twilio.comhttps://api.otpmock.com
https://verify.twilio.comhttps://api.otpmock.com

If your app calls an SMS provider we don't emulate yet, you can still send to POST /v1/messages/send from a small test-only adapter.

The test helper

A single file with no dependencies, in TypeScript or plain JavaScript. It works in Node 18+ and anywhere with fetch.

Download
curl -O https://otpmock.com/sdk/otpmock.ts    # TypeScript (Playwright)
curl -O https://otpmock.com/sdk/otpmock.mjs   # JavaScript (Cypress, plain Node)
MethodWhat it does
randomPhone(prefix?)Returns a fresh fake number (default prefix +1555). Use one per test.
waitForCode(phone, opts?)Polls until a code arrives. Options: since (ms timestamp, default: 5 seconds before the call), timeout (default 15000), interval (default 300).
clear(phone)Deletes the number's messages and verifications.
send(to, body)Puts a message in the inbox yourself. Handy when testing the helper.

Playwright

fixtures.ts
import { test as base } from "@playwright/test";
import { OtpMock } from "./otpmock";

export const test = base.extend<{ otp: OtpMock; phone: string }>({
  otp: async ({}, use) => {
    await use(new OtpMock({ baseUrl: process.env.OTPMOCK_URL!, apiKey: process.env.OTPMOCK_API_KEY! }));
  },
  phone: async ({ otp }, use) => {
    const phone = otp.randomPhone();
    await use(phone);
    await otp.clear(phone);
  },
});
signup.spec.ts
import { expect } from "@playwright/test";
import { test } from "./fixtures";

test("sign up with a phone number", async ({ page, otp, phone }) => {
  await page.goto("/signup");
  await page.getByLabel("Phone").fill(phone);
  await page.getByRole("button", { name: "Send code" }).click();

  const { code } = await otp.waitForCode(phone);
  await page.getByLabel("Verification code").fill(code);
  await expect(page.getByText("Welcome")).toBeVisible();
});

Cypress

Cypress tests run in the browser, so call the helper from a Node task.

cypress.config.js
import { defineConfig } from "cypress";
import { OtpMock } from "./otpmock.mjs";

const otp = new OtpMock({ baseUrl: process.env.OTPMOCK_URL, apiKey: process.env.OTPMOCK_API_KEY });

export default defineConfig({
  e2e: {
    setupNodeEvents(on) {
      on("task", { waitForCode: (phone) => otp.waitForCode(phone).then((r) => r.code) });
    },
  },
});
signup.cy.js
it("signs up with a phone number", () => {
  const phone = "+1555" + Cypress._.random(1e7, 9e7);
  cy.visit("/signup");
  cy.get("[name=phone]").type(phone);
  cy.contains("Send code").click();
  cy.task("waitForCode", phone).then((code) => cy.get("[name=code]").type(code));
  cy.contains("Welcome").should("be.visible");
});

API endpoints

Base URL: https://api.otpmock.com. Request bodies can be JSON or form-encoded. Responses are JSON. Timestamps are milliseconds since the Unix epoch.

POST/v1/messages/send

Put a message in the inbox. Fields: to and body (required), from (optional). Returns 201.

Request
curl https://api.otpmock.com/v1/messages/send \
  -H "Authorization: Bearer $OTPMOCK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to": "+15550142", "body": "Your code is 4819"}'
Response
{
  "sid": "SM3f1c…",
  "to": "+15550142",
  "from": null,
  "body": "Your code is 4819",
  "code": "4819",
  "source": "api",
  "receivedAt": 1791406301420,
  "status": "queued"
}
GET/v1/inbox/{phone}/code

The most recent code sent to {phone}, or 404 with {"error": "no_code_yet"}. Pass ?since= (ms) to ignore anything older, which is how you avoid reading a code from a previous run.

Response
{ "code": "4819", "messageSid": "SM3f1c…", "body": "Your code is 4819", "receivedAt": 1791406301420 }
GET/v1/inbox/{phone}/messages

Messages for one number, newest first, as {"messages": [...]}. ?limit= from 1 to 200, default 50.

GET/v1/messages

Every message in your inbox, newest first. Same shape and limit as above. This is what the live inbox uses.

DELETE/v1/inbox/{phone}

Deletes the number's messages and verifications. Returns {"deleted": n} with the number of messages removed.

Provider-compatible endpoints

Every provider in the table above is served at https://api.otpmock.com with its own paths, parameters, response shapes and errors, so the official SDKs call them unchanged. Each provider page lists its endpoints. Twilio's, as an example:

POST/2010-04-01/Accounts/{AccountSid}/Messages.json

Parameters To, Body, and optionally From or MessagingServiceSid. Returns 201 with a Twilio Message resource (sid starting with SM, status: "queued"). Missing To gives error 21604; missing Body gives 21602.

POST/v2/Services/{ServiceSid}/Verifications

Parameters To and Channel (default sms). otpmock generates a 6-digit code, puts "Your verification code is: 123456" in the inbox, and returns 201 with status: "pending". Starting a new verification cancels the previous pending one for that number.

POST/v2/Services/{ServiceSid}/VerificationCheck

Parameters To and Code. The right code returns status: "approved" and valid: true. A wrong code returns status: "pending" and valid: false; the fifth wrong attempt cancels the verification. With no pending verification you get 404 and Twilio error 20404, which is also what happens if you check an approved code twice.

Phone numbers & codes

  • Numbers are stored in E.164 form: +1 (555) 014-2, 15550142 and +15550142 are the same inbox, so providers that drop the + still match. Turkish local numbers from Netgsm, İleti Merkezi, Verimor and Mutlucell (5321234567) are stored as +905321234567.
  • In URLs, write + as %2B. A literal + in the path also works.
  • Codes are 4 to 8 digits. otpmock first looks for one near a keyword (code, OTP, PIN, passcode, verification, and their Turkish equivalents), then falls back to the first 4 to 8 digit number in the text. Messages without one are stored with "code": null.

Limits & retention

WhatLimit
Message retention10 minutes, then deleted automatically
Monthly messagesDepends on your plan, counted per calendar month (UTC). Each SMS sent and each Verify verification started counts as one. Reading is free.
Over the limitSend endpoints return 429 until the 1st of next month (UTC) or an upgrade. Reads keep working.
Verify check attempts5 per verification
List page size200 messages

Errors

StatusMeaning
400A required field is missing. Twilio paths include a Twilio error code.
401Missing or invalid API key.
404No code yet, no pending verification, or unknown path.
429Monthly message allowance used up.
Stuck? Email support@otpmock.com with the request you sent and the response you got. Leave your API key out.