# otpmock: complete integration guide for AI coding assistants This file is written for AI coding assistants (Claude, Cursor, Copilot, etc.) integrating otpmock into a user's project. It is self-contained: you should not need any other page. Follow the steps in order. ## What otpmock is otpmock is a hosted mock SMS inbox for automated tests. In a test, CI, preview or staging environment, the application sends SMS verification codes (one-time passwords) to otpmock instead of a real SMS provider. otpmock stores each message for 10 minutes, extracts the code, and lets tests read it over HTTPS. No real SMS is ever sent. - Website: https://otpmock.com - API base URL: https://api.otpmock.com - Emulated SMS provider APIs (tested with each provider's official Node.js SDK): Twilio (Programmable Messaging, Verify v2), Vonage (SMS API, Messages API, Verify v2), AWS SNS (Publish to a phone number), Infobip (SMS v2 and v3), Sinch (SMS batches), Plivo (send message), Netgsm (REST v2 SMS and OTP), İleti Merkezi (JSON SMS API). Plus a provider-neutral HTTP API that any app can call. - Accounts and API keys: https://otpmock.com/signup (free plan: 100 messages per month). ## Hard rules 1. Never change production behaviour. otpmock must only be active when an environment variable (`OTPMOCK_URL`) is set, and that variable must only be set in test/CI/staging environments. 2. Never hard-code the API key. Read it from `OTPMOCK_API_KEY`, or from the provider credential variable that the provider section below tells you to set to the otpmock key in the test environment. 3. Never commit API keys. Add them to the test environment's secret store or a git-ignored `.env.test` file. 4. Do not add test-only backdoors such as `if (phone === TEST_PHONE) code = "000000"`. Routing the provider to otpmock replaces the need for them. 5. Give every automated test its own phone number (use `randomPhone()`), so parallel workers never read each other's codes. 6. When waiting for a code, always pass a `since` timestamp taken just before the action that sends the SMS, so a code from a previous run is never used. ## Environment variables Add these to the test environment only: ``` OTPMOCK_URL=https://api.otpmock.com OTPMOCK_API_KEY=om_live_... # from https://otpmock.com/app ``` Each provider section below says which provider credential to set to the same otpmock API key in the test environment (for example `TWILIO_AUTH_TOKEN` for Twilio). Keep the real credentials for production. If the user has no API key yet, tell them to create one at https://otpmock.com/app (sign-up is free and takes an email). ## Step 1: detect how the app sends SMS Search the codebase before changing anything, and match it against the "Detect" hints in the provider sections of Step 2. - If the app uses one of the providers in Step 2, follow that provider's section. - If it uses another provider (Telnyx, legacy MessageBird REST, an in-house gateway...), follow Step 2C (generic API). - If it uses Firebase Phone Auth or AWS Cognito's built-in SMS: otpmock cannot intercept these, because the client SDK talks to Google/AWS directly. Tell the user to use the Firebase Auth emulator, or a Cognito custom SMS sender Lambda that calls otpmock in test environments (Step 2C). Stop here for those flows. - If the app uses a provider below but not from Node.js: the paths and payloads are the provider's own, so point that language's client at `https://api.otpmock.com` (keeping paths) with the credential described in the section. Only the Node.js SDKs are tested by otpmock; verify with one request (Step 4). ## Step 2: connect the provider (test environment only) In every case: enable otpmock only when `OTPMOCK_URL` is set, keep all send/verify calls unchanged, and keep production credentials for production. ### Twilio (Programmable Messaging and Verify v2) - Detect: `twilio` in package.json, `client.messages.create(`, `verify.v2.services(...)` - Official SDK tested: `twilio` - Emulated endpoints (host https://api.otpmock.com): - `POST /2010-04-01/Accounts/{AccountSid}/Messages.json`: Programmable Messaging: send an SMS - `POST /v2/Services/{ServiceSid}/Verifications`: Verify v2: start a verification (otpmock generates a 6-digit code) - `POST /v2/Services/{ServiceSid}/VerificationCheck`: Verify v2: check a code (approved / pending, 5 attempts) - Credentials: In the test environment, set `TWILIO_AUTH_TOKEN` to your otpmock API key. The Account SID can stay as it is; otpmock echoes it back. - The official SDK accepts a custom HTTP client. This one keeps every path and swaps the host, so both `api.twilio.com` and `verify.twilio.com` traffic reaches otpmock. Download it from https://otpmock.com/sdk/twilio-node-client.mjs. - Phone numbers: Send in E.164 (`+15550142`), as Twilio expects. - Note: Twilio's own test credentials only cover a few resources and not Verify; otpmock covers both Messaging and Verify. ```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, }); ``` ### Vonage (SMS API, Messages API and Verify v2) - Detect: `@vonage/server-sdk` or `nexmo` in package.json, `vonage.sms.send(`, `vonage.messages.send(`, `vonage.verify2.newRequest(` - Official SDK tested: `@vonage/server-sdk` - Emulated endpoints (host https://api.otpmock.com): - `POST /sms/json`: SMS API (rest.nexmo.com): send an SMS - `POST /v1/messages`: Messages API with channel `sms` and message_type `text` - `POST /v2/verify`: Verify v2: start a verification (otpmock generates the code; code_length and code are honoured) - `POST /v2/verify/{request_id}`: Verify v2: check a code (200 completed, 400 wrong, 410 after 3 wrong attempts) - `DELETE /v2/verify/{request_id}`: Verify v2: cancel - Credentials: In the test environment, set `VONAGE_API_SECRET` to your otpmock API key (SMS and Messages APIs use Basic auth). For Verify v2, which the SDK signs with a JWT, also set `VONAGE_APPLICATION_ID` to your otpmock API key; otpmock reads the JWT's `application_id` claim and does not check the signature, so your usual private key works. - The SDK has documented `restHost` and `apiHost` options. - Phone numbers: Vonage sends numbers without a leading `+` (`15550142`). otpmock stores them as `+15550142`, so tests can query the E.164 form. - Note: Like Vonage, otpmock rejects a second concurrent Verify request to the same number with 409 until the first completes, is cancelled or expires. ```js import { Vonage } from "@vonage/server-sdk"; export const vonage = new Vonage( { apiKey: process.env.VONAGE_API_KEY, apiSecret: process.env.VONAGE_API_SECRET, // otpmock API key in tests applicationId: process.env.VONAGE_APPLICATION_ID, // otpmock API key in tests (Verify v2) privateKey: process.env.VONAGE_PRIVATE_KEY, }, process.env.OTPMOCK_URL ? { restHost: process.env.OTPMOCK_URL, apiHost: process.env.OTPMOCK_URL } : {}, ); ``` ### AWS SNS (Publish to a phone number) - Detect: `@aws-sdk/client-sns` or `aws-sdk` in package.json, `PublishCommand` with `PhoneNumber`, `sns.publish({ PhoneNumber` - Official SDK tested: `@aws-sdk/client-sns` - Emulated endpoints (host https://api.otpmock.com): - `POST / (Action=Publish, PhoneNumber, Message)`: awsQuery protocol, XML response with MessageId - Credentials: In the test environment, use your otpmock API key as the access key ID. The secret access key can be any value; otpmock reads the access key ID from the SigV4 `Authorization` header and does not verify the signature. - Every AWS SDK supports a custom `endpoint`. - Phone numbers: Send in E.164 (`+15550142`), as SNS expects. - Note: Only `Publish` with `PhoneNumber` is emulated. Publishing to a topic (`TopicArn`) returns `InvalidParameter`. - Note: AWS Cognito's built-in SMS can't be intercepted. Use a Cognito custom SMS sender Lambda that calls otpmock in test environments. ```js import { SNSClient } from "@aws-sdk/client-sns"; export const sns = new SNSClient({ region: process.env.AWS_REGION ?? "us-east-1", ...(process.env.OTPMOCK_URL && { endpoint: process.env.OTPMOCK_URL, credentials: { accessKeyId: process.env.OTPMOCK_API_KEY, secretAccessKey: "unused" }, }), }); ``` ### Telnyx (Messaging API and Verify) - Detect: `telnyx` in package.json, `client.messages.send(`, `client.verifications.triggerSMS(` - Official SDK tested: `telnyx` - Emulated endpoints (host https://api.otpmock.com): - `POST /v2/messages`: Send an SMS - `POST /v2/verifications/sms`: Verify: send a code (otpmock generates it; `custom_code` is honoured) - `POST /v2/verifications/by_phone_number/{phone}/actions/verify`: Verify: check a code (`response_code` accepted / rejected) - Credentials: In the test environment, set your Telnyx API key variable to your otpmock API key. - The SDK has a documented `baseURL` option (and reads `TELNYX_BASE_URL`). Keep the `/v2` suffix. - Phone numbers: Send in E.164 (`+15550142`). ```js import Telnyx from "telnyx"; export const telnyx = new Telnyx({ apiKey: process.env.TELNYX_API_KEY, // otpmock API key in tests ...(process.env.OTPMOCK_URL && { baseURL: `${process.env.OTPMOCK_URL}/v2` }), }); ``` ### AWS End User Messaging (SMS SendTextMessage (pinpoint-sms-voice-v2)) - Detect: `@aws-sdk/client-pinpoint-sms-voice-v2` in package.json, `SendTextMessageCommand` - Official SDK tested: `@aws-sdk/client-pinpoint-sms-voice-v2` - Emulated endpoints (host https://api.otpmock.com): - `POST / (X-Amz-Target: PinpointSMSVoiceV2.SendTextMessage)`: awsJson1_0 protocol, returns `MessageId` - Credentials: In the test environment, use your otpmock API key as the access key ID. The secret access key can be any value; the SigV4 signature isn't checked. - Every AWS SDK client supports a custom `endpoint`. This is AWS's newer SMS service, the successor to sending SMS through SNS. - Phone numbers: Send in E.164 (`+15550142`). - Note: Only `SendTextMessage` is emulated. The destination-number verification operations aren't, because they need a pre-registered number record. ```js import { PinpointSMSVoiceV2Client } from "@aws-sdk/client-pinpoint-sms-voice-v2"; export const sms = new PinpointSMSVoiceV2Client({ region: process.env.AWS_REGION ?? "us-east-1", ...(process.env.OTPMOCK_URL && { endpoint: process.env.OTPMOCK_URL, credentials: { accessKeyId: process.env.OTPMOCK_API_KEY, secretAccessKey: "unused" }, }), }); ``` ### Telesign (SMS API (/v1/messaging)) - Detect: `telesignsdk` or `telesignenterprisesdk` in package.json, `client.sms.message(` - Official SDK tested: `telesignsdk` - Emulated endpoints (host https://api.otpmock.com): - `POST /v1/messaging`: Send an SMS (`phone_number`, `message`, `message_type`); `status.code` 290 on success - Credentials: In the test environment, use your otpmock API key as the Telesign customer ID. The API key can be any base64 value: the SDK signs requests with HMAC, and otpmock reads the customer ID without checking the signature. Basic auth also works. - The SDK takes the REST endpoint as its third constructor argument. - Phone numbers: Telesign uses digits with country code (`15550142`). otpmock stores `+15550142`. - Note: Clients should check `status.code` (290 = in progress), as with the real API. - Note: Telesign's Verify API lives on a second host and isn't emulated yet; SMS-based OTP that your app generates works today. ```js import TeleSignSDK from "telesignsdk"; export const telesign = new TeleSignSDK( process.env.TELESIGN_CUSTOMER_ID, // otpmock API key in tests process.env.TELESIGN_API_KEY, process.env.OTPMOCK_URL ?? "https://rest-api.telesign.com", ); ``` ### Bandwidth (Messaging API v2) - Detect: `bandwidth-sdk` in package.json, `MessagesApi`, `createMessage(` - Official SDK tested: `bandwidth-sdk` - Emulated endpoints (host https://api.otpmock.com): - `POST /api/v2/users/{accountId}/messages`: Send a message (`applicationId`, `to[]`, `from`, `text`), 202 - Credentials: In the test environment, use your otpmock API key as the Bandwidth password (Basic auth). Don't configure OAuth client credentials in tests. - The SDK's `basePath` option is ignored for sending messages, because each operation has its own server URL. Override that URL from `bandwidth-sdk/dist/base` once at startup in the test environment. - Phone numbers: Send in E.164 (`+15550142`). ```js import { Configuration, MessagesApi } from "bandwidth-sdk"; import { operationServerMap } from "bandwidth-sdk/dist/base"; if (process.env.OTPMOCK_URL) { operationServerMap["MessagesApi.createMessage"][0].url = `${process.env.OTPMOCK_URL}/api/v2`; } export const bandwidth = new MessagesApi( new Configuration({ username: process.env.BANDWIDTH_USERNAME, password: process.env.BANDWIDTH_PASSWORD, // otpmock API key in tests }), ); ``` ### Infobip (SMS v2 (/sms/2/text/advanced) and v3 (/sms/3/messages)) - Detect: `@infobip-api/sdk` in package.json, `infobip.channels.sms.send(`, requests to `*.api.infobip.com/sms/` - Official SDK tested: `@infobip-api/sdk` - Emulated endpoints (host https://api.otpmock.com): - `POST /sms/2/text/advanced`: Send SMS (v2), used by the Node.js SDK - `POST /sms/3/messages`: Send SMS (v3) - Credentials: In the test environment, set your Infobip API key variable to your otpmock API key. `App`, `Basic` and `Bearer` authorization are accepted. - Infobip gives every account its own base URL, so `baseUrl` is already a required option. - Phone numbers: Infobip sends numbers without a leading `+`. otpmock stores them in E.164 form. ```js import { Infobip, AuthType } from "@infobip-api/sdk"; export const infobip = new Infobip({ baseUrl: process.env.OTPMOCK_URL ?? process.env.INFOBIP_BASE_URL, apiKey: process.env.INFOBIP_API_KEY, // otpmock API key in tests authType: AuthType.ApiKey, }); ``` ### Sinch (SMS REST API batches) - Detect: `@sinch/sdk-core` in package.json, `sinch.sms.batches.send(`, requests to `*.sms.api.sinch.com/xms/v1/` - Official SDK tested: `@sinch/sdk-core` - Emulated endpoints (host https://api.otpmock.com): - `POST /xms/v1/{service_plan_id}/batches`: Send an SMS batch (mt_text) - Credentials: In the test environment, set your Sinch API token (used with `servicePlanId`) to your otpmock API key. - The SDK has a documented `smsHostname` option. - Phone numbers: Send in E.164 or without `+`; both land in the same inbox. - Note: Only the service plan ID + API token authentication mode is emulated, not the OAuth (project ID / key) mode. ```js import { SinchClient } from "@sinch/sdk-core"; export const sinch = new SinchClient({ servicePlanId: process.env.SINCH_SERVICE_PLAN_ID, apiToken: process.env.SINCH_API_TOKEN, // otpmock API key in tests ...(process.env.OTPMOCK_URL && { smsHostname: process.env.OTPMOCK_URL }), }); ``` ### Bird (MessageBird) (SMS API (platform.bird.com /v1/sms/messages)) - Detect: `@messagebird/sdk` in package.json, `new BirdClient(`, `bird.sms.send(`, requests to `*.platform.bird.com/v1/sms/messages` - Official SDK tested: `@messagebird/sdk` - Emulated endpoints (host https://api.otpmock.com): - `POST /v1/sms/messages`: Send one SMS (`to`, `from`, `text`, `category`), 202 with `status: "accepted"` - Credentials: In the test environment, set your Bird API key variable to your otpmock API key. With `baseUrl` set, the SDK no longer needs the `bk_{region}_` key prefix. - The official SDK has a `baseUrl` option that overrides the region host derived from the key. - Phone numbers: Send in E.164 (`+15550142`), as Bird expects. - Note: This is Bird's current SMS API. The legacy MessageBird API (`rest.messagebird.com/messages`) and its old `messagebird` npm package aren't emulated: that SDK can't be pointed at another host. - Note: When the monthly allowance is used up, otpmock returns Bird's `billing_error` (402) rather than a rate-limit error, so the SDK doesn't retry in a loop. ```js import { BirdClient } from "@messagebird/sdk"; export const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY, // otpmock API key in tests ...(process.env.OTPMOCK_URL && { baseUrl: process.env.OTPMOCK_URL }), }); ``` ### Plivo (Send message API) - Detect: `plivo` in package.json, `client.messages.create(` - Official SDK tested: `plivo` - Emulated endpoints (host https://api.otpmock.com): - `POST /v1/Account/{auth_id}/Message/`: Send an SMS (multiple recipients separated by `<`) - Credentials: In the test environment, set `PLIVO_AUTH_TOKEN` to your otpmock API key. The Auth ID can stay as it is. - The Plivo SDK accepts a `url` option that replaces its account base URL. It works but isn't documented by Plivo. - Phone numbers: Send in E.164 (`+15550142`) or without `+`. ```js import plivo from "plivo"; const authId = process.env.PLIVO_AUTH_ID; export const plivoClient = new plivo.Client( authId, process.env.PLIVO_AUTH_TOKEN, // otpmock API key in tests process.env.OTPMOCK_URL ? { url: `${process.env.OTPMOCK_URL}/v1/Account/${authId}` } : {}, ); ``` ### Netgsm (REST v2 SMS and OTP) - Detect: `@netgsm/sms` in package.json, `sendRestSms(`, `sendOtpSms(`, requests to `api.netgsm.com.tr/sms/rest/v2/` - Official SDK tested: `@netgsm/sms` - Emulated endpoints (host https://api.otpmock.com): - `POST /sms/rest/v2/send`: Send SMS (code "00" on success) - `POST /sms/rest/v2/otp`: Send an OTP SMS - Credentials: In the test environment, set the Netgsm username (or password) to your otpmock API key. Basic auth is read from either field. - The SDK has no base URL option, but its `baseURL` property can be set after construction (it's private only in the TypeScript types). - Phone numbers: Netgsm uses Turkish local numbers (`5321234567`). otpmock stores them as `+905321234567`; query that form in tests. - Note: The legacy XML and GET endpoints aren't emulated; the SDK uses REST v2. ```ts import { Netgsm } from "@netgsm/sms"; export const netgsm = new Netgsm({ username: process.env.NETGSM_USERNAME, // otpmock API key in tests password: process.env.NETGSM_PASSWORD, }); if (process.env.OTPMOCK_URL) (netgsm as any).baseURL = process.env.OTPMOCK_URL; ``` ### Verimor (SMS API v2 (GET /v2/send, POST /v2/send.json)) - Detect: requests to `sms.verimor.com.tr/v2/send` - Official SDK tested: `HTTP API` - Emulated endpoints (host https://api.otpmock.com): - `GET /v2/send`: Query string: `username`, `password`, `source_addr`, `msg`, `dest` (comma-separated) - `POST /v2/send.json`: JSON: `username`, `password`, `source_addr`, `messages[]` with `msg` and `dest` - Credentials: In the test environment, use your otpmock API key as the Verimor `password`. The username can stay as it is. - Verimor has no official Node.js SDK; apps call the HTTP API directly, so only the host changes. Responses are plain text: the campaign ID on success, `INSUFFICIENT_CREDITS`, `MISSING_MESSAGE` and similar codes on 400. - Phone numbers: Verimor uses `905321234567`. otpmock stores it as `+905321234567`; local `5321234567` also works. ```js const VERIMOR_URL = process.env.OTPMOCK_URL ?? "https://sms.verimor.com.tr"; const res = await fetch(`${VERIMOR_URL}/v2/send.json`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ username: process.env.VERIMOR_USERNAME, password: process.env.VERIMOR_PASSWORD, // otpmock API key in tests source_addr: "BASLIGIM", messages: [{ msg: `Doğrulama kodunuz: ${code}`, dest: "905321234567" }], }), }); const campaignId = await res.text(); ``` ### Mutlucell (XML SMS API (sndblkex)) - Detect: requests to `smsgw.mutlucell.com/smsgw-ws/sndblkex`, `` XML - Official SDK tested: `HTTP API` - Emulated endpoints (host https://api.otpmock.com): - `POST /smsgw-ws/sndblkex`: XML ``; response `$#` or an error code - Credentials: In the test environment, use your otpmock API key as `pwd` (Mutlucell accepts an API key in that attribute too). - Apps usually post the XML directly, so only the host changes. The official PHP package hard-codes its host and can't be redirected; post the XML yourself in tests. Errors come back as bare codes: `23` wrong credentials, `20` bad XML, `22` no credits. - Phone numbers: Mutlucell accepts any format (`05321234567`, `905321234567`, `532 123 45 67`). otpmock stores Turkish numbers as `+905321234567`. ```js const MUTLUCELL_URL = process.env.OTPMOCK_URL ?? "https://smsgw.mutlucell.com"; const xml = ` Doğrulama kodunuz: ${code}5321234567 `; const res = await fetch(`${MUTLUCELL_URL}/smsgw-ws/sndblkex`, { method: "POST", headers: { "content-type": "text/xml; charset=UTF-8" }, body: xml, }); const result = await res.text(); // "$12345678#1.0" or an error code ``` ### İleti Merkezi (JSON SMS API) - Detect: `@iletimerkezi/iletimerkezi-node` in package.json, `IletiMerkeziClient`, requests to `api.iletimerkezi.com/v1/send-sms/json` - Official SDK tested: `@iletimerkezi/iletimerkezi-node` - Emulated endpoints (host https://api.otpmock.com): - `POST /v1/send-sms/json`: Send SMS (`request.authentication.key`, `order.message.receipents.number`) - Credentials: In the test environment, use your otpmock API key as the İleti Merkezi API key. The hash can be any value. - The SDK has no base URL option, but its HTTP client's `baseUrl` can be set after construction. - Phone numbers: İleti Merkezi uses Turkish local numbers (`5051234567`). otpmock stores them as `+905051234567`. - Note: As with the real API, the HTTP status equals the status code in the body, so the SDK's `ok()` works unchanged. ```ts import { IletiMerkeziClient } from "@iletimerkezi/iletimerkezi-node"; export const ileti = new IletiMerkeziClient( process.env.ILETIMERKEZI_API_KEY, // otpmock API key in tests process.env.ILETIMERKEZI_API_HASH, process.env.ILETIMERKEZI_SENDER, ); if (process.env.OTPMOCK_URL) (ileti as any).httpClient.baseUrl = `${process.env.OTPMOCK_URL}/v1/`; ``` ### Twilio: the HTTP client file For Twilio, add this file to the project (for example `src/lib/twilio-node-client.mjs`; convert to `require`/`module.exports` for CommonJS). It is also downloadable from https://otpmock.com/sdk/twilio-node-client.mjs. ```js // otpmock HTTP client for the official `twilio` Node.js SDK (v5). // Sends every Twilio request (api.twilio.com and verify.twilio.com) to otpmock, // keeping the path. Use it only in test environments, and set TWILIO_AUTH_TOKEN // to your otpmock API key there. // // 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, // }); import twilio from "twilio"; export class OtpMockHttpClient extends twilio.RequestClient { constructor(baseUrl = "https://api.otpmock.com") { super(); this.baseUrl = baseUrl.replace(/\/+$/, ""); } request(opts) { const u = new URL(opts.uri); return super.request({ ...opts, uri: this.baseUrl + u.pathname + u.search }); } } ``` Twilio Verify behaviour: otpmock generates a 6-digit code, puts `Your verification code is: 123456` in the inbox, and approves a `VerificationCheck` with the right code (`status: "approved"`, `valid: true`). A wrong code returns `status: "pending"`, `valid: false`; the fifth wrong attempt cancels it. Checking an approved verification returns 404 (Twilio error 20404). Vonage Verify v2 behaviour: otpmock generates a code of `code_length` digits (default 4) or uses `code` if given, puts `Your {brand} verification code is: 1234` in the inbox, returns 200 `{"status":"completed"}` for the right code, 400 for a wrong code and 410 after 3 wrong codes. A second concurrent request to the same number returns 409. ### Step 2C: any other provider (generic API) Add a small test-only branch in the code that sends SMS: ```ts async function sendSms(to: string, body: string) { if (process.env.OTPMOCK_URL) { const res = await fetch(`${process.env.OTPMOCK_URL}/v1/messages/send`, { method: "POST", headers: { authorization: `Bearer ${process.env.OTPMOCK_API_KEY}`, "content-type": "application/json" }, body: JSON.stringify({ to, body }), }); if (!res.ok) throw new Error(`otpmock ${res.status}: ${await res.text()}`); return; } // ...existing provider call, unchanged } ``` If the app relies on the provider to generate and check codes (a "Verify"-style API), generate the code in the app for the otpmock branch, send it with the message, and compare it in the app. ## Step 3: read the code in tests Add the helper file to the test folder, for example `tests/otpmock.ts`. It has no dependencies and works in Node 18+. A JavaScript version is at https://otpmock.com/sdk/otpmock.mjs. ```ts // Test framework'lerinden (Playwright, Cypress task, Jest, Vitest...) kullanılacak // bağımlılıksız istemci. Node 18+ ve tarayıcıda çalışır (global fetch). export interface OtpMockOptions { baseUrl: string; apiKey: string; } export interface WaitOptions { // Bu zamandan (ms) önce gelen kodlar yok sayılır. Varsayılan: waitForCode çağrıldığı an - 5 sn. since?: number; timeout?: number; interval?: number; } export interface ReceivedCode { code: string; messageSid: string; body: string; receivedAt: number; } export class OtpMock { private baseUrl: string; private apiKey: string; constructor({ baseUrl, apiKey }: OtpMockOptions) { this.baseUrl = baseUrl.replace(/\/+$/, ""); this.apiKey = apiKey; } // Paralel testlerin çakışmaması için her teste ayrı bir sanal numara ver. // +1 555 01xx aralığı kurgusal numaralar için ayrılmıştır; geri kalanı rastgele. randomPhone(prefix = "+1555"): string { const rest = Array.from({ length: 12 - prefix.replace(/\D/g, "").length }, () => Math.floor(Math.random() * 10)).join(""); return prefix + rest; } async waitForCode(phone: string, opts: WaitOptions = {}): Promise { const { timeout = 15_000, interval = 300 } = opts; // Saat kayması payı: uygulama sunucusu ile test makinesinin saatleri birkaç saniye farklı olabilir. const since = opts.since ?? Date.now() - 5_000; const deadline = Date.now() + timeout; while (true) { const res = await this.request(`/v1/inbox/${encodeURIComponent(phone)}/code?since=${since}`); if (res.ok) return (await res.json()) as ReceivedCode; if (res.status !== 404) throw new Error(`otpmock: ${res.status} ${await res.text()}`); if (Date.now() >= deadline) throw new Error(`otpmock: ${phone} için ${timeout} ms içinde kod gelmedi`); await new Promise((r) => setTimeout(r, interval)); } } async clear(phone: string): Promise { await this.request(`/v1/inbox/${encodeURIComponent(phone)}`, { method: "DELETE" }); } // Test ortamında gerçek bir uygulama yoksa SMS gönderimini simüle etmek için. async send(to: string, body: string): Promise { const res = await this.request("/v1/messages/send", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ to, body }), }); if (!res.ok) throw new Error(`otpmock: ${res.status} ${await res.text()}`); } private request(path: string, init: RequestInit = {}): Promise { return fetch(this.baseUrl + path, { ...init, headers: { ...(init.headers as Record), authorization: `Bearer ${this.apiKey}` }, }); } } ``` ### Playwright ```ts import { test as base, expect } from "@playwright/test"; import { OtpMock } from "./otpmock"; 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); }, }); test("sign up with a phone number", async ({ page, otp, phone }) => { await page.goto("/signup"); await page.getByLabel("Phone").fill(phone); const since = Date.now() - 5_000; await page.getByRole("button", { name: "Send code" }).click(); const { code } = await otp.waitForCode(phone, { since }); await page.getByLabel("Verification code").fill(code); await expect(page.getByText("Welcome")).toBeVisible(); }); ``` Adapt the selectors to the user's app; keep the structure. ### Cypress Cypress tests run in the browser, so call the helper from a Node task: ```js // 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, since }) => otp.waitForCode(phone, { since }).then((r) => r.code) }); }, }, }); ``` ```js // cypress/e2e/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); const since = Date.now() - 5000; cy.contains("Send code").click(); cy.task("waitForCode", { phone, since }).then((code) => cy.get("[name=code]").type(code)); cy.contains("Welcome").should("be.visible"); }); ``` ### Other frameworks Poll `GET /v1/inbox/{phone}/code?since=` every 300 ms until it returns 200 (it returns 404 until a code arrives), with a timeout of about 15 seconds. ## Step 4: verify the integration Run these checks and report the results to the user: 1. With `OTPMOCK_URL` unset, the app still uses the real provider (no behaviour change in production). 2. With the test environment variables set, trigger one SMS from the app (or run one test), then: ``` curl "https://api.otpmock.com/v1/inbox/%2B15550142/code" -H "Authorization: Bearer $OTPMOCK_API_KEY" ``` It should return `{"code": "...", ...}` for the number used. The message also appears live at https://otpmock.com/app. 3. The new or updated end-to-end test passes, including when tests run in parallel. ## API reference Base URL `https://api.otpmock.com`. Authenticate with `Authorization: Bearer `, or HTTP Basic `:` (what Twilio SDKs send). Request bodies may be JSON or form-encoded. Timestamps are milliseconds since the Unix epoch. ### POST /v1/messages/send Store a message. Fields: `to` and `body` (required), `from` (optional). Returns 201: ```json { "sid": "SM3f1c...", "to": "+15550142", "from": null, "body": "Your code is 4819", "code": "4819", "source": "api", "receivedAt": 1791406301420, "status": "queued" } ``` ### GET /v1/inbox/{phone}/code?since={ms} The newest code sent to `{phone}` received at or after `since`. Returns 200 `{ "code": "4819", "messageSid": "SM3f1c...", "body": "Your code is 4819", "receivedAt": 1791406301420 }`, or 404 `{ "error": "no_code_yet" }`. ### GET /v1/inbox/{phone}/messages?limit={1-200} Messages for one number, newest first: `{ "messages": [ ... ] }`. ### GET /v1/messages?limit={1-200} All messages for the account, newest first. ### DELETE /v1/inbox/{phone} Deletes the number's messages and verifications: `{ "deleted": 2 }`. ### Provider-compatible endpoints Every endpoint listed in the provider sections of Step 2 is available at https://api.otpmock.com with the provider's own path, payload and response format. ### Twilio-compatible endpoints (form-encoded, Basic auth) - `POST /2010-04-01/Accounts/{AccountSid}/Messages.json` with `To`, `Body`, optional `From` / `MessagingServiceSid`. Returns 201 with a Twilio Message resource (`sid` starts with `SM`, `status: "queued"`). Missing `To`: 400, code 21604. Missing `Body`: 400, code 21602. - `POST /v2/Services/{ServiceSid}/Verifications` with `To`, optional `Channel` (default `sms`). Returns 201, `status: "pending"`. - `POST /v2/Services/{ServiceSid}/VerificationCheck` with `To`, `Code`. Returns 200 with `status` `approved` or `pending` and `valid`. No pending verification: 404, code 20404. ### Phone numbers and code extraction - Numbers are stored in E.164 form (`+` and digits): `+1 (555) 014-2`, `15550142` and `+15550142` are the same inbox, so providers that send numbers without `+` (Vonage, Infobip, Sinch) still match a test that queries `+15550142`. Turkish local numbers from Netgsm, İleti Merkezi, Verimor and Mutlucell (`5321234567`, `05321234567`) are stored as `+905321234567`. In URLs write `+` as `%2B`. - Codes are 4 to 8 digits. otpmock looks for a number next to a keyword (code, OTP, PIN, passcode, verification, and Turkish equivalents), then falls back to the first 4 to 8 digit number. Messages without one have `"code": null`. ### Errors and limits | Status | Meaning | |---|---| | 400 | Missing required field (Twilio paths include a Twilio error code) | | 401 | Missing or invalid API key | | 404 | No code yet, no pending verification, or unknown path | | 429 | Monthly message allowance used up (Twilio paths: code 20429). Resets on the 1st of the month (UTC). Reading still works. | - Messages and verifications are deleted 10 minutes after they arrive. - Each SMS sent and each Verify verification started counts as one message. Reading is free. - Plans: Free 100 messages/month (1 key), Solo 5,000 (3 keys), Team 25,000 (10 keys), Enterprise unlimited. ## Troubleshooting - `401`: the key is wrong or revoked, or (Twilio) `TWILIO_AUTH_TOKEN` in the test environment is not the otpmock key. - `waitForCode` times out: the app is still sending to the real provider (check that `OTPMOCK_URL` is set in the environment of the app server, not only the test runner), or the test reads a different number than the app sent to (check normalization), or `since` is later than the send. - An old code is returned: pass `since` taken just before triggering the SMS. - `429`: the monthly allowance is used up; see https://otpmock.com/app. Support: support@otpmock.com