Free
Try itApp → otpmock → Test
- 100 messages a month
- 1 user, 1 API key
- All 15 SMS provider APIs
- Live inbox
SMS Mock inbox for end-to-end tests
otpmock speaks the API of the SMS provider you already use: Twilio, Vonage, Telnyx, AWS SNS, AWS End User Messaging, Infobip, Sinch, Bird, Plivo, Telesign, Bandwidth, Netgsm, İleti Merkezi, Verimor or Mutlucell. Your app sends verification codes the same way it does in production. We catch every message before it gets anywhere near a carrier, and hand the code to Playwright, Cypress, Selenium, or whoever is clicking through staging.
Free for 100 messages a month. No card required.
This board is a demo. Nothing on it ever reached a phone, and nothing your tests send will either.
Works with Playwright / Cypress / Selenium / WebdriverIO / Puppeteer / TestCafe / Appium / Maestro / Detox / Postman / Robot Framework / k6 / any HTTP client
1 The problem
The flow works. The suite doesn't, because step four is waiting for a text message. Every common workaround costs you something.
You pay for every message on every CI run. Delivery takes as long as the carrier decides, so tests time out at random. And you still need a phone, or a paid inbox, to read the code back.
if (phone === TEST_PHONE) code = "000000" works fine until one config mistake ships it to production. Then it's a key to every account on that number.
Fine for plain SMS. Useless with Verify APIs (Twilio Verify, Infobip 2FA, Sinch Verification…), where the provider generates the code and your fake never sees it. Then you build it again for the next project.
2 How it works
otpmock answers in your SMS provider's own API format, so your application code can't tell the difference. Only the test environment's configuration changes.
Your app
Calls your provider's SDK: messages.create, sms.send, a Verify request. Same SDK, same code path as production.
otpmock
Answers in your provider's exact response format, keeps the message for 10 minutes and pulls the code out of the text.
Carrier network never contacted
Your test
Asks the inbox over HTTPS and types the code in. Verify checks pass with the right code, exactly like the real provider.
The whole integration, per provider: one option on the client you already create. Production keeps talking to your provider; anything with OTPMOCK_URL set talks to us. Pick your language and provider below, or see all providers.
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 os
from urllib.parse import urlparse
from twilio.http.http_client import TwilioHttpClient
from twilio.rest import Client
class OtpMockHttpClient(TwilioHttpClient):
"""Sends Twilio SDK requests to otpmock, keeping path, params and auth."""
def __init__(self, base):
super().__init__()
self.base = base.rstrip("/")
def request(self, method, url, *args, **kwargs):
u = urlparse(url)
return super().request(method, self.base + u.path + ("?" + u.query if u.query else ""), *args, **kwargs)
otpmock = os.environ.get("OTPMOCK_URL")
client = Client(
os.environ["TWILIO_ACCOUNT_SID"],
os.environ["TWILIO_AUTH_TOKEN"], # otpmock API key in tests
http_client=OtpMockHttpClient(otpmock) if otpmock else None,
) import com.twilio.http.*;
import java.net.URI;
// Sends Twilio SDK requests to otpmock, keeping path, params and auth.
public class OtpMockTwilioHttpClient extends NetworkHttpClient {
private final String base;
public OtpMockTwilioHttpClient(String base) { this.base = base.replaceAll("/+$", ""); }
@Override
public Response makeRequest(Request request) {
Request routed = new Request(request.getMethod(), base + URI.create(request.getUrl()).getRawPath());
request.getQueryParams().forEach((k, vs) -> vs.forEach(v -> routed.addQueryParam(k, v)));
request.getPostParams().forEach((k, vs) -> vs.forEach(v -> routed.addPostParam(k, v)));
request.getHeaderParams().forEach((k, vs) -> vs.forEach(v -> routed.addHeaderParam(k, v)));
routed.setAuth(request.getUsername(), request.getPassword());
return super.makeRequest(routed);
}
}
// Where you build the client:
String otpmock = System.getenv("OTPMOCK_URL");
TwilioRestClient.Builder builder = new TwilioRestClient.Builder(accountSid, authToken); // otpmock API key in tests
if (otpmock != null) builder.httpClient(new OtpMockTwilioHttpClient(otpmock));
TwilioRestClient client = builder.build();
// Message.creator(...).create(client); using Twilio.Clients;
using Twilio.Http;
// Sends requests to otpmock, keeping path, query and auth headers.
class OtpMockHandler : DelegatingHandler
{
private readonly Uri _base;
public OtpMockHandler(string baseUrl) : base(new HttpClientHandler()) => _base = new Uri(baseUrl);
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
{
request.RequestUri = new Uri(_base, request.RequestUri!.PathAndQuery);
return base.SendAsync(request, ct);
}
}
var otpmock = Environment.GetEnvironmentVariable("OTPMOCK_URL");
var client = new TwilioRestClient(
accountSid,
authToken, // otpmock API key in tests
httpClient: otpmock is null ? null : new SystemNetHttpClient(new HttpClient(new OtpMockHandler(otpmock))));
// await MessageResource.CreateAsync(..., client: client); use Twilio\Http\CurlClient;
use Twilio\Rest\Client;
// Sends Twilio SDK requests to otpmock, keeping path, params and auth.
class OtpMockTwilioClient extends CurlClient {
public function __construct(private string $base) { parent::__construct(); }
public function request(string $method, string $url, array $params = [], array $data = [], array $headers = [],
?string $user = null, ?string $password = null, ?int $timeout = null, ?\Twilio\AuthStrategy\AuthStrategy $authStrategy = null): \Twilio\Http\Response {
return parent::request($method, rtrim($this->base, '/') . parse_url($url, PHP_URL_PATH), $params, $data, $headers, $user, $password, $timeout, $authStrategy);
}
}
$otpmock = getenv('OTPMOCK_URL');
$client = new Client(
getenv('TWILIO_ACCOUNT_SID'),
getenv('TWILIO_AUTH_TOKEN'), // otpmock API key in tests
null, null,
$otpmock ? new OtpMockTwilioClient($otpmock) : null,
); require "twilio-ruby"
require "uri"
# Sends Twilio SDK requests to otpmock, keeping path, params and auth.
class OtpMockHttpClient < Twilio::HTTP::Client
def initialize(base)
super()
@base = URI(base)
end
def request(_host, _port, method, url, *rest)
u = URI(url)
path = u.path + (u.query ? "?#{u.query}" : "")
super("#{@base.scheme}://#{@base.host}", @base.port, method, "#{@base.scheme}://#{@base.host}:#{@base.port}#{path}", *rest)
end
end
otpmock = ENV["OTPMOCK_URL"]
client = Twilio::REST::Client.new(
ENV["TWILIO_ACCOUNT_SID"],
ENV["TWILIO_AUTH_TOKEN"], # otpmock API key in tests
nil, nil,
otpmock ? OtpMockHttpClient.new(otpmock) : nil
) // otpmockTransport sends SDK requests to otpmock, keeping path, query and auth.
type otpmockTransport struct{ base *url.URL }
func (t otpmockTransport) RoundTrip(r *http.Request) (*http.Response, error) {
r = r.Clone(r.Context())
r.URL.Scheme, r.URL.Host, r.Host = t.base.Scheme, t.base.Host, t.base.Host
return http.DefaultTransport.RoundTrip(r)
}
func newTwilio() *twilio.RestClient {
// token = otpmock API key in tests. twilio-go only accepts letters and digits here,
// so use the key without underscores (om_live_abc… -> omliveabc…); otpmock accepts both forms.
sid, token := os.Getenv("TWILIO_ACCOUNT_SID"), os.Getenv("TWILIO_AUTH_TOKEN")
params := twilio.ClientParams{Username: sid, Password: token}
if base, err := url.Parse(os.Getenv("OTPMOCK_URL")); err == nil && base.Host != "" {
c := &client.Client{Credentials: client.NewCredentials(sid, token), HTTPClient: &http.Client{Transport: otpmockTransport{base}}}
c.SetAccountSid(sid)
params.Client = c
}
return twilio.NewRestClientWithParams(params)
} 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 } : {},
); import os
from urllib.parse import urlparse
from vonage import Auth, HttpClientOptions, Vonage
otpmock = os.environ.get("OTPMOCK_URL") # e.g. https://api.otpmock.com
host = urlparse(otpmock).netloc if otpmock else None
vonage = Vonage(
Auth(api_key=os.environ["VONAGE_API_KEY"], api_secret=os.environ["VONAGE_API_SECRET"]), # secret = otpmock key in tests
HttpClientOptions(api_host=host, rest_host=host) if host else None,
)
# Verify v2 uses an application JWT (otpmock reads application_id, ignores the signature)
verify = Vonage(
Auth(application_id=os.environ["VONAGE_APPLICATION_ID"], # otpmock API key in tests
private_key=os.environ["VONAGE_PRIVATE_KEY"]),
HttpClientOptions(api_host=host) if host else None,
).verify import com.vonage.client.HttpConfig;
import com.vonage.client.VonageClient;
String otpmock = System.getenv("OTPMOCK_URL");
VonageClient.Builder builder = VonageClient.builder()
.apiKey(System.getenv("VONAGE_API_KEY"))
.apiSecret(System.getenv("VONAGE_API_SECRET")); // otpmock API key in tests
if (otpmock != null) builder.httpConfig(HttpConfig.builder().baseUri(otpmock).build());
VonageClient vonage = builder.build(); // The Vonage .NET SDK reads its hosts from configuration (appsettings.json).
// In the test environment:
{
"vonage": {
"Url.Rest": "https://api.otpmock.com",
"Url.Api": "https://api.otpmock.com"
}
}
// Code stays the same:
var vonage = new VonageClient(Credentials.FromApiKeyAndSecret(apiKey, apiSecret)); // secret = otpmock key in tests
// Verify v2 over HTTP (Basic auth with key and secret):
var vonageUrl = Environment.GetEnvironmentVariable("OTPMOCK_URL") ?? "https://api.nexmo.com";
var http = new HttpClient { BaseAddress = new Uri(vonageUrl) };
http.DefaultRequestHeaders.Authorization = new("Basic",
Convert.ToBase64String(Encoding.UTF8.GetBytes($"{apiKey}:{apiSecret}")));
// POST /v2/verify { brand, workflow = [{ channel = "sms", to }] } -> request_id
// POST /v2/verify/{request_id} { code } -> { status = "completed" } use Vonage\Client;
use Vonage\Client\Credentials\Basic;
$otpmock = getenv('OTPMOCK_URL');
$vonage = new Client(
new Basic(getenv('VONAGE_API_KEY'), getenv('VONAGE_API_SECRET')), // secret = otpmock key in tests
$otpmock ? ['base_rest_url' => $otpmock, 'base_api_url' => $otpmock] : [],
);
// Verify v2: application JWT (application_id = otpmock key in tests; signature not checked).
// verify2() hard-codes api.nexmo.com, so tests pass a host-rewriting Guzzle client.
$stack = \GuzzleHttp\HandlerStack::create();
if ($otpmock) {
$u = parse_url($otpmock);
$stack->push(\GuzzleHttp\Middleware::mapRequest(fn ($r) => $r->withUri(
$r->getUri()->withScheme($u['scheme'])->withHost($u['host'])->withPort($u['port'] ?? null))));
}
$verify = new Client(
new \Vonage\Client\Credentials\Keypair(file_get_contents(getenv('VONAGE_PRIVATE_KEY_PATH')), getenv('VONAGE_APPLICATION_ID')),
[], new \GuzzleHttp\Client(['handler' => $stack]),
); require "vonage"
require "uri"
otpmock = ENV["OTPMOCK_URL"] && URI(ENV["OTPMOCK_URL"])
host = otpmock && "#{otpmock.host}:#{otpmock.port}"
vonage = Vonage::Client.new(
api_key: ENV["VONAGE_API_KEY"],
api_secret: ENV["VONAGE_API_SECRET"], # otpmock API key in tests
**(host ? { api_host: host, rest_host: host } : {})
)
# Verify v2 (JWT): otpmock reads application_id as the key; signature not checked
verify = Vonage::Client.new(
application_id: ENV["VONAGE_APPLICATION_ID"], # otpmock API key in tests
private_key: File.read(ENV["VONAGE_PRIVATE_KEY_PATH"]),
**(host ? { api_host: host } : {})
).verify2 auth := vonage.CreateAuthFromKeySecret(os.Getenv("VONAGE_API_KEY"), os.Getenv("VONAGE_API_SECRET")) // secret = otpmock key in tests
smsClient := vonage.NewSMSClient(auth)
if base := os.Getenv("OTPMOCK_URL"); base != "" {
smsClient.Config.BasePath = base + "/sms"
}
// Verify v2 (net/http): POST {base}/v2/verify, then POST {base}/v2/verify/{request_id} {"code": ...}
verifyBase := "https://api.nexmo.com"
if base := os.Getenv("OTPMOCK_URL"); base != "" {
verifyBase = base
}
req, _ := http.NewRequest("POST", verifyBase+"/v2/verify", bytes.NewReader(body))
req.SetBasicAuth(os.Getenv("VONAGE_API_KEY"), os.Getenv("VONAGE_API_SECRET")) // secret = otpmock key in tests 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" },
}),
}); import os
import boto3
sns = boto3.client(
"sns",
region_name=os.environ.get("AWS_REGION", "us-east-1"),
endpoint_url=os.environ.get("OTPMOCK_URL"), # None in production
)
# In tests, set AWS_ACCESS_KEY_ID to your otpmock API key (any secret works). import java.net.URI;
import software.amazon.awssdk.services.sns.SnsClient;
String otpmock = System.getenv("OTPMOCK_URL");
var builder = SnsClient.builder();
if (otpmock != null) builder.endpointOverride(URI.create(otpmock));
SnsClient sns = builder.build();
// In tests, set AWS_ACCESS_KEY_ID to your otpmock API key (any secret works). using Amazon.SimpleNotificationService;
var otpmock = Environment.GetEnvironmentVariable("OTPMOCK_URL");
var config = new AmazonSimpleNotificationServiceConfig();
if (otpmock is not null) { config.ServiceURL = otpmock; config.AuthenticationRegion = "us-east-1"; }
var sns = new AmazonSimpleNotificationServiceClient(config);
// In tests, set AWS_ACCESS_KEY_ID to your otpmock API key (any secret works). use Aws\Sns\SnsClient;
$otpmock = getenv('OTPMOCK_URL');
$sns = new SnsClient(array_filter([
'region' => getenv('AWS_REGION') ?: 'us-east-1',
'version' => 'latest',
'endpoint' => $otpmock ?: null,
]));
// In tests, set AWS_ACCESS_KEY_ID to your otpmock API key (any secret works). require "aws-sdk-sns"
sns = Aws::SNS::Client.new(
region: ENV.fetch("AWS_REGION", "us-east-1"),
**(ENV["OTPMOCK_URL"] ? { endpoint: ENV["OTPMOCK_URL"] } : {})
)
# In tests, set AWS_ACCESS_KEY_ID to your otpmock API key (any secret works). cfg, err := config.LoadDefaultConfig(ctx) // in tests, AWS_ACCESS_KEY_ID = otpmock API key (any secret works)
if err != nil {
return err
}
client := sns.NewFromConfig(cfg, func(o *sns.Options) {
if base := os.Getenv("OTPMOCK_URL"); base != "" {
o.BaseEndpoint = aws.String(base)
}
}) 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` }),
}); import os
from telnyx import Telnyx
otpmock = os.environ.get("OTPMOCK_URL")
telnyx = Telnyx(
api_key=os.environ["TELNYX_API_KEY"], # otpmock API key in tests
base_url=f"{otpmock}/v2" if otpmock else None,
) import com.telnyx.sdk.client.TelnyxClient;
import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient;
String otpmock = System.getenv("OTPMOCK_URL");
TelnyxOkHttpClient.Builder builder = TelnyxOkHttpClient.builder()
.apiKey(System.getenv("TELNYX_API_KEY")); // otpmock API key in tests
if (otpmock != null) builder.baseUrl(otpmock + "/v2"); // keep the /v2 suffix
TelnyxClient telnyx = builder.build();
// Messaging: telnyx.messages().send(...); Verify: telnyx.verifications().triggerSms(...) using Telnyx;
TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); // otpmock API key in tests
var otpmock = Environment.GetEnvironmentVariable("OTPMOCK_URL");
if (otpmock is not null) TelnyxConfiguration.SetApiBase(otpmock + "/v2");
var messages = new MessageService(); use Telnyx\Client;
$otpmock = getenv('OTPMOCK_URL');
$telnyx = new Client(
apiKey: getenv('TELNYX_API_KEY'), // otpmock API key in tests
baseUrl: $otpmock ? rtrim($otpmock, '/') . '/v2' : null,
); require "telnyx"
telnyx = Telnyx::Client.new(
api_key: ENV["TELNYX_API_KEY"], # otpmock API key in tests
**(ENV["OTPMOCK_URL"] ? { base_url: "#{ENV["OTPMOCK_URL"]}/v2" } : {})
) opts := []option.RequestOption{option.WithAPIKey(os.Getenv("TELNYX_API_KEY"))} // otpmock API key in tests
if base := os.Getenv("OTPMOCK_URL"); base != "" {
opts = append(opts, option.WithBaseURL(base+"/v2/"))
}
client := telnyx.NewClient(opts...) 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" },
}),
}); import os
import boto3
sms = boto3.client(
"pinpoint-sms-voice-v2",
region_name=os.environ.get("AWS_REGION", "us-east-1"),
endpoint_url=os.environ.get("OTPMOCK_URL"), # None in production
)
# In tests, set AWS_ACCESS_KEY_ID to your otpmock API key (any secret works). import java.net.URI;
import software.amazon.awssdk.services.pinpointsmsvoicev2.PinpointSmsVoiceV2Client;
String otpmock = System.getenv("OTPMOCK_URL");
var builder = PinpointSmsVoiceV2Client.builder();
if (otpmock != null) builder.endpointOverride(URI.create(otpmock));
PinpointSmsVoiceV2Client sms = builder.build();
// In tests, set AWS_ACCESS_KEY_ID to your otpmock API key (any secret works). using Amazon.PinpointSMSVoiceV2;
var otpmock = Environment.GetEnvironmentVariable("OTPMOCK_URL");
var config = new AmazonPinpointSMSVoiceV2Config();
if (otpmock is not null) { config.ServiceURL = otpmock; config.AuthenticationRegion = "us-east-1"; }
var sms = new AmazonPinpointSMSVoiceV2Client(config);
// In tests, set AWS_ACCESS_KEY_ID to your otpmock API key (any secret works). use Aws\PinpointSMSVoiceV2\PinpointSMSVoiceV2Client;
$otpmock = getenv('OTPMOCK_URL');
$sms = new PinpointSMSVoiceV2Client(array_filter([
'region' => getenv('AWS_REGION') ?: 'us-east-1',
'version' => 'latest',
'endpoint' => $otpmock ?: null,
]));
// In tests, set AWS_ACCESS_KEY_ID to your otpmock API key (any secret works). require "aws-sdk-pinpointsmsvoicev2"
sms = Aws::PinpointSMSVoiceV2::Client.new(
region: ENV.fetch("AWS_REGION", "us-east-1"),
**(ENV["OTPMOCK_URL"] ? { endpoint: ENV["OTPMOCK_URL"] } : {})
)
# In tests, set AWS_ACCESS_KEY_ID to your otpmock API key (any secret works). cfg, err := config.LoadDefaultConfig(ctx) // in tests, AWS_ACCESS_KEY_ID = otpmock API key (any secret works)
if err != nil {
return err
}
client := pinpointsmsvoicev2.NewFromConfig(cfg, func(o *pinpointsmsvoicev2.Options) {
if base := os.Getenv("OTPMOCK_URL"); base != "" {
o.BaseEndpoint = aws.String(base)
}
}) 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",
); import os
from telesign.messaging import MessagingClient
telesign = MessagingClient(
os.environ["TELESIGN_CUSTOMER_ID"], # otpmock API key in tests
os.environ["TELESIGN_API_KEY"],
rest_endpoint=os.environ.get("OTPMOCK_URL", "https://rest-api.telesign.com"),
) import com.telesign.MessagingClient;
MessagingClient telesign = new MessagingClient(
System.getenv("TELESIGN_CUSTOMER_ID"), // otpmock API key in tests
System.getenv("TELESIGN_API_KEY"),
System.getenv().getOrDefault("OTPMOCK_URL", "https://rest-api.telesign.com")); using Telesign;
var messaging = new MessagingClient(
Environment.GetEnvironmentVariable("TELESIGN_CUSTOMER_ID"), // otpmock API key in tests
Environment.GetEnvironmentVariable("TELESIGN_API_KEY"),
Environment.GetEnvironmentVariable("OTPMOCK_URL") ?? "https://rest-api.telesign.com",
"csharp_telesign", null, null); use telesign\sdk\messaging\MessagingClient;
$telesign = new MessagingClient(
getenv('TELESIGN_CUSTOMER_ID'), // otpmock API key in tests
getenv('TELESIGN_API_KEY'),
getenv('OTPMOCK_URL') ?: 'https://rest-api.telesign.com',
);
// SMS Verify (HTTP; the SDK has no verify client): Basic customer ID : API key.
$verifyUrl = getenv('OTPMOCK_URL') ?: 'https://rest-ww.telesign.com';
$auth = [getenv('TELESIGN_CUSTOMER_ID'), getenv('TELESIGN_API_KEY')];
$ref = json_decode((string) (new \GuzzleHttp\Client())->post("$verifyUrl/v1/verify/sms", [
'auth' => $auth, 'form_params' => ['phone_number' => '15550142'],
])->getBody(), true)['reference_id'];
// then GET "$verifyUrl/v1/verify/$ref?verify_code=..." -> verify.code_state "VALID" require "telesign"
messaging = Telesign::MessagingClient.new(
ENV["TELESIGN_CUSTOMER_ID"], # otpmock API key in tests
ENV["TELESIGN_API_KEY"],
rest_endpoint: ENV.fetch("OTPMOCK_URL", "https://rest-api.telesign.com")
) // Telesign has no official Go SDK; call its HTTP API with net/http, only the host changes.
base := os.Getenv("OTPMOCK_URL")
if base == "" {
base = "https://rest-api.telesign.com"
}
form := url.Values{"phone_number": {"15550142"}, "message": {msg}, "message_type": {"OTP"}}
req, _ := http.NewRequest("POST", base+"/v1/messaging", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetBasicAuth(os.Getenv("TELESIGN_CUSTOMER_ID"), os.Getenv("TELESIGN_API_KEY")) // customer ID = otpmock key in tests
res, err := http.DefaultClient.Do(req) // body status.code 290 = in progress 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
}),
); import os
from urllib.parse import urlparse
import bandwidth
class OtpMockApiClient(bandwidth.ApiClient):
"""Each Bandwidth operation has its own host; send them all to otpmock."""
def call_api(self, method, url, *args, **kwargs):
u = urlparse(url)
url = os.environ["OTPMOCK_URL"].rstrip("/") + u.path + ("?" + u.query if u.query else "")
return super().call_api(method, url, *args, **kwargs)
config = bandwidth.Configuration(
username=os.environ["BANDWIDTH_USERNAME"],
password=os.environ["BANDWIDTH_PASSWORD"], # otpmock API key in tests
)
client = OtpMockApiClient(config) if os.environ.get("OTPMOCK_URL") else bandwidth.ApiClient(config)
messages = bandwidth.MessagesApi(client)String base = System.getenv().getOrDefault("OTPMOCK_URL", "https://messaging.bandwidth.com");
String auth = Base64.getEncoder().encodeToString(
(System.getenv("BANDWIDTH_USERNAME") + ":" + System.getenv("BANDWIDTH_PASSWORD")).getBytes()); // password = otpmock API key in tests
HttpRequest req = HttpRequest.newBuilder(URI.create(base + "/api/v2/users/" + accountId + "/messages"))
.header("Authorization", "Basic " + auth)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json)) // {"applicationId":"...","to":["+1..."],"from":"+1...","text":"..."}
.build(); // The Bandwidth .NET package has no host option, so call the Messaging API directly.
var baseUrl = Environment.GetEnvironmentVariable("OTPMOCK_URL") ?? "https://messaging.bandwidth.com";
var accountId = Environment.GetEnvironmentVariable("BANDWIDTH_ACCOUNT_ID");
var auth = Convert.ToBase64String(Encoding.UTF8.GetBytes(
$"{Environment.GetEnvironmentVariable("BANDWIDTH_USERNAME")}:{Environment.GetEnvironmentVariable("BANDWIDTH_PASSWORD")}")); // password = otpmock key in tests
using var http = new HttpClient();
var req = new HttpRequestMessage(HttpMethod.Post, $"{baseUrl}/api/v2/users/{accountId}/messages")
{
Content = JsonContent.Create(new { applicationId, to = new[] { phone }, from, text }),
};
req.Headers.Authorization = new AuthenticationHeaderValue("Basic", auth);
(await http.SendAsync(req)).EnsureSuccessStatusCode(); use Bandwidth\Api\MessagesApi;
use Bandwidth\Configuration;
use GuzzleHttp\{Client, HandlerStack, Middleware};
use Psr\Http\Message\RequestInterface;
// createMessage has its own server URL, so tests swap the host on the Guzzle client.
$stack = HandlerStack::create();
if ($otpmock = getenv('OTPMOCK_URL')) {
$u = parse_url($otpmock);
$stack->push(Middleware::mapRequest(fn (RequestInterface $r) => $r->withUri(
$r->getUri()->withScheme($u['scheme'])->withHost($u['host'])->withPort($u['port'] ?? null))));
}
$config = (new Configuration())
->setUsername(getenv('BANDWIDTH_USERNAME'))
->setPassword(getenv('BANDWIDTH_PASSWORD')); // otpmock API key in tests
$bandwidth = new MessagesApi(new Client(['handler' => $stack]), $config); require "net/http"
require "json"
BANDWIDTH_URL = ENV.fetch("OTPMOCK_URL", "https://messaging.bandwidth.com")
uri = URI("#{BANDWIDTH_URL}/api/v2/users/#{ENV["BANDWIDTH_ACCOUNT_ID"]}/messages")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
req.basic_auth(ENV["BANDWIDTH_USERNAME"], ENV["BANDWIDTH_PASSWORD"]) # password = otpmock API key in tests
req.body = { applicationId: ENV["BANDWIDTH_APP_ID"], to: ["+15550142"], from: "+15550000001", text: "Your code is #{code}" }.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) } // Bandwidth has no official Go SDK; call its HTTP API with net/http, only the host changes.
base := os.Getenv("OTPMOCK_URL")
if base == "" {
base = "https://messaging.bandwidth.com"
}
body, _ := json.Marshal(map[string]any{
"applicationId": os.Getenv("BANDWIDTH_APPLICATION_ID"), "to": []string{"+15550142"},
"from": os.Getenv("BANDWIDTH_NUMBER"), "text": msg,
})
req, _ := http.NewRequest("POST", base+"/api/v2/users/"+os.Getenv("BANDWIDTH_ACCOUNT_ID")+"/messages", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.SetBasicAuth(os.Getenv("BANDWIDTH_USERNAME"), os.Getenv("BANDWIDTH_PASSWORD")) // password = otpmock key in tests
res, err := http.DefaultClient.Do(req) // 202 Accepted 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,
}); import os
from infobip_api_client.api.sms_api import SmsApi
from infobip_api_client.api_client import ApiClient, Configuration
config = Configuration(
host=os.environ.get("OTPMOCK_URL") or os.environ["INFOBIP_BASE_URL"],
api_key=os.environ["INFOBIP_API_KEY"], # otpmock API key in tests
)
sms = SmsApi(ApiClient(config)) import com.infobip.*;
import com.infobip.api.SmsApi;
String base = System.getenv().getOrDefault("OTPMOCK_URL", System.getenv("INFOBIP_BASE_URL"));
ApiClient client = ApiClient.forApiKey(ApiKey.from(System.getenv("INFOBIP_API_KEY"))) // otpmock API key in tests
.withBaseUrl(BaseUrl.from(base))
.build();
SmsApi sms = new SmsApi(client); using Infobip.Api.Client.Api;
using Infobip.Api.Client.Client;
var baseUrl = Environment.GetEnvironmentVariable("OTPMOCK_URL")
?? Environment.GetEnvironmentVariable("INFOBIP_BASE_URL");
var config = new Configuration(
Environment.GetEnvironmentVariable("INFOBIP_API_KEY"), // otpmock API key in tests
new Uri(baseUrl));
var sms = new SmsApi(config); use Infobip\Api\SmsApi;
use Infobip\Configuration;
$config = new Configuration(
host: getenv('OTPMOCK_URL') ?: getenv('INFOBIP_BASE_URL'),
apiKey: getenv('INFOBIP_API_KEY'), // otpmock API key in tests
);
$sms = new SmsApi(config: $config); require "net/http"
require "json"
# Infobip has no official Ruby SDK; apps call the REST API, so only the host changes.
INFOBIP_URL = ENV["OTPMOCK_URL"] || ENV["INFOBIP_BASE_URL"]
res = Net::HTTP.post(
URI("#{INFOBIP_URL}/sms/2/text/advanced"),
{ messages: [{ from: "InfoSMS", destinations: [{ to: "15550142" }], text: "Your PIN is #{code}" }] }.to_json,
"Content-Type" => "application/json",
"Authorization" => "App #{ENV["INFOBIP_API_KEY"]}" # otpmock API key in tests
) cfg := infobip.NewConfiguration()
cfg.Host = os.Getenv("INFOBIP_BASE_URL")
if base := os.Getenv("OTPMOCK_URL"); base != "" {
cfg.Host = base
}
client := api.NewAPIClient(cfg)
auth := context.WithValue(ctx, infobip.ContextAPIKeys, map[string]infobip.APIKey{
"APIKeyHeader": {Key: os.Getenv("INFOBIP_API_KEY")}, // otpmock API key in tests
}) 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
applicationKey: process.env.SINCH_APPLICATION_KEY, // otpmock API key in tests (Verification)
applicationSecret: process.env.SINCH_APPLICATION_SECRET,
...(process.env.OTPMOCK_URL && { smsHostname: process.env.OTPMOCK_URL, verificationHostname: process.env.OTPMOCK_URL }),
}); import os
from sinch import SinchClient
sinch = SinchClient(
service_plan_id=os.environ["SINCH_SERVICE_PLAN_ID"],
sms_api_token=os.environ["SINCH_API_TOKEN"], # otpmock API key in tests
sms_region="us",
)
if os.environ.get("OTPMOCK_URL"):
sinch.configuration.sms_domain_with_service_plan_id = os.environ["OTPMOCK_URL"]
# Verification API (HTTP): Application auth, key = otpmock API key in tests
VERIFY_URL = os.environ.get("OTPMOCK_URL", "https://verification.api.sinch.com")
VERIFY_AUTH = {"Authorization": f"Application {os.environ['SINCH_APP_KEY']}:{os.environ['SINCH_APP_SECRET']}"}
# POST {VERIFY_URL}/verification/v1/verifications, PUT ./id/{id} {"method": "sms", "sms": {"code": ...}} import com.sinch.sdk.SinchClient;
import com.sinch.sdk.models.*;
String otpmock = System.getenv("OTPMOCK_URL");
Configuration.Builder config = Configuration.builder()
.setSmsServicePlanId(System.getenv("SINCH_SERVICE_PLAN_ID"))
.setSmsApiToken(System.getenv("SINCH_API_TOKEN")) // otpmock API key in tests
.setApplicationKey(System.getenv("SINCH_APPLICATION_KEY")) // Verification: otpmock API key in tests
.setApplicationSecret(System.getenv("SINCH_APPLICATION_SECRET"));
if (otpmock != null) {
config.setSmsContext(SmsContext.builder().setSmsUrl(otpmock).setSmsRegion(SMSRegion.US).build());
config.setVerificationContext(VerificationContext.builder().setVerificationUrl(otpmock).build());
}
SinchClient sinch = new SinchClient(config.build()); using Sinch;
using Sinch.SMS;
var otpmock = Environment.GetEnvironmentVariable("OTPMOCK_URL");
var sinch = new SinchClient(default, default, default, options =>
{
options.UseServicePlanIdWithSms(
Environment.GetEnvironmentVariable("SINCH_SERVICE_PLAN_ID"),
Environment.GetEnvironmentVariable("SINCH_API_TOKEN"), // otpmock API key in tests
SmsServicePlanIdRegion.Us);
if (otpmock is not null) options.ApiUrlOverrides = new ApiUrlOverrides { SmsUrl = otpmock };
});
// Verification over HTTP (application key = otpmock API key in tests; signature isn't checked):
var verifyHttp = new HttpClient { BaseAddress = new Uri(otpmock ?? "https://verification.api.sinch.com") };
verifyHttp.DefaultRequestHeaders.Authorization = new("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes(
$"{Environment.GetEnvironmentVariable("SINCH_APPLICATION_KEY")}:{Environment.GetEnvironmentVariable("SINCH_APPLICATION_SECRET")}")));
// POST /verification/v1/verifications { identity = { type = "number", endpoint }, method = "sms" } -> id
// PUT /verification/v1/verifications/id/{id} { method = "sms", sms = { code } } -> status "SUCCESSFUL" use GuzzleHttp\Client;
$sinchUrl = getenv('OTPMOCK_URL') ?: 'https://us.sms.api.sinch.com';
$planId = getenv('SINCH_SERVICE_PLAN_ID');
(new Client())->post("$sinchUrl/xms/v1/$planId/batches", [
'headers' => ['Authorization' => 'Bearer ' . getenv('SINCH_API_TOKEN')], // otpmock API key in tests
'json' => ['from' => '12345', 'to' => ['+15550142'], 'body' => "Your code is $code"],
]);
// Verification API: application key/secret auth.
$verifyUrl = getenv('OTPMOCK_URL') ?: 'https://verification.api.sinch.com';
$appAuth = ['Authorization' => 'Application ' . getenv('SINCH_APPLICATION_KEY') . ':' . $signature]; // key = otpmock API key in tests
(new Client())->post("$verifyUrl/verification/v1/verifications", [
'headers' => $appAuth,
'json' => ['identity' => ['type' => 'number', 'endpoint' => '+15550142'], 'method' => 'sms'],
]); require "net/http"
require "json"
# Sinch has no official Ruby SDK; apps call the REST API, so only the host changes.
SINCH_URL = ENV.fetch("OTPMOCK_URL", "https://us.sms.api.sinch.com")
res = Net::HTTP.post(
URI("#{SINCH_URL}/xms/v1/#{ENV["SINCH_SERVICE_PLAN_ID"]}/batches"),
{ to: ["+15550142"], from: "12345", body: "Your code: #{code}" }.to_json,
"Content-Type" => "application/json",
"Authorization" => "Bearer #{ENV["SINCH_API_TOKEN"]}" # otpmock API key in tests
) // Sinch has no official Go SDK; call its HTTP API with net/http, only the host changes.
base := os.Getenv("OTPMOCK_URL")
if base == "" {
base = "https://us.sms.api.sinch.com"
}
body, _ := json.Marshal(map[string]any{"to": []string{"+15550142"}, "from": os.Getenv("SINCH_NUMBER"), "body": msg})
req, _ := http.NewRequest("POST", base+"/xms/v1/"+os.Getenv("SINCH_SERVICE_PLAN_ID")+"/batches", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+os.Getenv("SINCH_API_TOKEN")) // otpmock API key in tests
res, err := http.DefaultClient.Do(req)
// Verification: POST {base}/verification/v1/verifications, then PUT .../verifications/id/{id}.
// Production host is https://verification.api.sinch.com. Basic auth with the application key and secret:
req.SetBasicAuth(os.Getenv("SINCH_APPLICATION_KEY"), os.Getenv("SINCH_APPLICATION_SECRET")) // key = otpmock key in tests 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 }),
}); import os
import requests
BIRD_URL = os.environ.get("OTPMOCK_URL", "https://us-west-1.platform.bird.com")
requests.post(
f"{BIRD_URL}/v1/sms/messages",
headers={"Authorization": f"Bearer {os.environ['BIRD_API_KEY']}"}, # otpmock API key in tests
json={"from": "+15557654321", "to": "+15550142", "text": f"Your code is {code}", "category": "authentication"},
).raise_for_status() // Bird has no Java SDK; call the REST API and swap the host.
String base = System.getenv().getOrDefault("OTPMOCK_URL", "https://eu.platform.bird.com");
HttpRequest req = HttpRequest.newBuilder(URI.create(base + "/v1/sms/messages"))
.header("Authorization", "Bearer " + System.getenv("BIRD_API_KEY")) // otpmock API key in tests
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json)) // {"from":"+1...","to":"+1...","text":"...","category":"authentication"}
.build(); // Bird has no official .NET SDK; call the REST API with HttpClient.
var birdUrl = Environment.GetEnvironmentVariable("OTPMOCK_URL") ?? "https://eu-west-1.platform.bird.com";
var http = new HttpClient { BaseAddress = new Uri(birdUrl) };
http.DefaultRequestHeaders.Add("Authorization",
"Bearer " + Environment.GetEnvironmentVariable("BIRD_API_KEY")); // otpmock API key in tests
await http.PostAsJsonAsync("/v1/sms/messages",
new { from = "+15557654321", to = phone, text = $"Your code is {code}", category = "authentication" }); use GuzzleHttp\Client;
$birdUrl = getenv('OTPMOCK_URL') ?: 'https://us-west-1.platform.bird.com';
(new Client())->post("$birdUrl/v1/sms/messages", [
'headers' => ['Authorization' => 'Bearer ' . getenv('BIRD_API_KEY')], // otpmock API key in tests
'json' => ['from' => '+15557654321', 'to' => '+15550142', 'text' => "Your code is $code", 'category' => 'authentication'],
]); require "net/http"
require "json"
# Bird has no official Ruby SDK; apps call the REST API, so only the host changes.
BIRD_URL = ENV.fetch("OTPMOCK_URL", "https://api.bird.com")
res = Net::HTTP.post(
URI("#{BIRD_URL}/v1/sms/messages"),
{ from: "+15557654321", to: "+15550142", text: "Your code is #{code}", category: "authentication" }.to_json,
"Content-Type" => "application/json",
"Authorization" => "Bearer #{ENV["BIRD_API_KEY"]}" # otpmock API key in tests
) // Bird has no official Go SDK; call its HTTP API with net/http, only the host changes.
base := os.Getenv("OTPMOCK_URL")
if base == "" {
base = "https://eu.platform.bird.com"
}
body, _ := json.Marshal(map[string]any{"from": os.Getenv("BIRD_NUMBER"), "to": "+15550142", "text": msg, "category": "authentication"})
req, _ := http.NewRequest("POST", base+"/v1/sms/messages", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+os.Getenv("BIRD_API_KEY")) // otpmock API key in tests
res, err := http.DefaultClient.Do(req) // 202, status "accepted" 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}` } : {},
); import os
from urllib.parse import urlparse
import plivo
plivo_client = plivo.RestClient(
os.environ["PLIVO_AUTH_ID"],
os.environ["PLIVO_AUTH_TOKEN"], # otpmock API key in tests
)
otpmock = os.environ.get("OTPMOCK_URL")
if otpmock: # the SDK has no host option: rewrite the host of each request
_send = plivo_client.session.send
def _send_to_otpmock(request, *args, **kwargs):
u = urlparse(request.url)
request.url = otpmock.rstrip("/") + u.path + ("?" + u.query if u.query else "")
return _send(request, *args, **kwargs)
plivo_client.session.send = _send_to_otpmock import com.fasterxml.jackson.databind.module.SimpleModule;
import com.plivo.api.PlivoClient;
import com.plivo.api.models.base.LogLevel;
import okhttp3.OkHttpClient;
String otpmock = System.getenv("OTPMOCK_URL");
String authToken = System.getenv("PLIVO_AUTH_TOKEN"); // otpmock API key in tests
PlivoClient plivo = otpmock == null
? new PlivoClient(authId, authToken)
: new PlivoClient(authId, authToken, new OkHttpClient.Builder(), otpmock + "/v1/", new SimpleModule(), LogLevel.NONE);
// Message.creator(src, dst, text).client(plivo).create();
// VerifySession.creator(...).client(plivo).create(); // The Plivo .NET SDK has no base URL option; in apps that must be testable, call the REST API.
var plivoUrl = Environment.GetEnvironmentVariable("OTPMOCK_URL") ?? "https://api.plivo.com";
var authId = Environment.GetEnvironmentVariable("PLIVO_AUTH_ID");
var authToken = Environment.GetEnvironmentVariable("PLIVO_AUTH_TOKEN"); // otpmock API key in tests
var http = new HttpClient { BaseAddress = new Uri(plivoUrl) };
http.DefaultRequestHeaders.Authorization = new("Basic",
Convert.ToBase64String(Encoding.UTF8.GetBytes($"{authId}:{authToken}")));
await http.PostAsJsonAsync($"/v1/Account/{authId}/Message/",
new { src = "+15550000001", dst = phone, text = $"Your code is {code}" }); use GuzzleHttp\Client;
// plivo/plivo-php hard-codes api.plivo.com, so call the REST path directly.
$plivoUrl = getenv('OTPMOCK_URL') ?: 'https://api.plivo.com';
$authId = getenv('PLIVO_AUTH_ID');
(new Client())->post("$plivoUrl/v1/Account/$authId/Message/", [
'auth' => [$authId, getenv('PLIVO_AUTH_TOKEN')], // auth token = otpmock API key in tests
'json' => ['src' => '+15550000001', 'dst' => '+15550142', 'text' => "Your code is $code"],
]); require "net/http"
require "json"
PLIVO_URL = ENV.fetch("OTPMOCK_URL", "https://api.plivo.com")
auth_id = ENV["PLIVO_AUTH_ID"]
uri = URI("#{PLIVO_URL}/v1/Account/#{auth_id}/Message/")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
req.basic_auth(auth_id, ENV["PLIVO_AUTH_TOKEN"]) # otpmock API key in tests
req.body = { src: "+15550000001", dst: "+15550142", text: "Your code is #{code}" }.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) } // otpmockTransport sends SDK requests to otpmock, keeping path, query and auth.
type otpmockTransport struct{ base *url.URL }
func (t otpmockTransport) RoundTrip(r *http.Request) (*http.Response, error) {
r = r.Clone(r.Context())
r.URL.Scheme, r.URL.Host, r.Host = t.base.Scheme, t.base.Host, t.base.Host
return http.DefaultTransport.RoundTrip(r)
}
func newPlivo() (*plivo.Client, error) {
opts := &plivo.ClientOptions{}
if base, err := url.Parse(os.Getenv("OTPMOCK_URL")); err == nil && base.Host != "" {
opts.HttpClient = &http.Client{Transport: otpmockTransport{base}}
}
return plivo.NewClient(os.Getenv("PLIVO_AUTH_ID"), os.Getenv("PLIVO_AUTH_TOKEN"), opts) // token = otpmock API key in tests
} 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; import os
import requests
NETGSM_URL = os.environ.get("OTPMOCK_URL", "https://api.netgsm.com.tr")
requests.post(
f"{NETGSM_URL}/sms/rest/v2/send",
auth=(os.environ["NETGSM_USERNAME"], os.environ["NETGSM_PASSWORD"]), # username = otpmock API key in tests
json={"msgheader": "BASLIGIM", "encoding": "TR", "messages": [{"msg": f"Doğrulama kodunuz: {code}", "no": "5321234567"}]},
).raise_for_status()String base = System.getenv().getOrDefault("OTPMOCK_URL", "https://api.netgsm.com.tr");
String auth = Base64.getEncoder().encodeToString(
(System.getenv("NETGSM_USERNAME") + ":" + System.getenv("NETGSM_PASSWORD")).getBytes()); // username = otpmock API key in tests
HttpRequest req = HttpRequest.newBuilder(URI.create(base + "/sms/rest/v2/send"))
.header("Authorization", "Basic " + auth)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json)) // {"msgheader":"...","encoding":"TR","messages":[{"msg":"...","no":"5321234567"}]}
.build(); // Netgsm has no official .NET SDK; call REST v2 with HttpClient.
var netgsmUrl = Environment.GetEnvironmentVariable("OTPMOCK_URL") ?? "https://api.netgsm.com.tr";
var user = Environment.GetEnvironmentVariable("NETGSM_USERNAME"); // otpmock API key in tests
var pass = Environment.GetEnvironmentVariable("NETGSM_PASSWORD");
var http = new HttpClient { BaseAddress = new Uri(netgsmUrl) };
http.DefaultRequestHeaders.Authorization = new("Basic",
Convert.ToBase64String(Encoding.UTF8.GetBytes($"{user}:{pass}")));
await http.PostAsJsonAsync("/sms/rest/v2/send", new
{
msgheader = "BASLIGIM", encoding = "TR",
messages = new[] { new { msg = $"Doğrulama kodunuz: {code}", no = "5321234567" } },
}); use GuzzleHttp\Client;
$netgsmUrl = getenv('OTPMOCK_URL') ?: 'https://api.netgsm.com.tr';
(new Client())->post("$netgsmUrl/sms/rest/v2/send", [
'auth' => [getenv('NETGSM_USERNAME'), getenv('NETGSM_PASSWORD')], // username = otpmock API key in tests
'json' => ['msgheader' => 'BASLIGIM', 'encoding' => 'TR', 'messages' => [['msg' => "Doğrulama kodunuz: $code", 'no' => '5321234567']]],
]); require "net/http"
require "json"
# Netgsm has no official Ruby SDK; apps call REST v2, so only the host changes.
NETGSM_URL = ENV.fetch("OTPMOCK_URL", "https://api.netgsm.com.tr")
uri = URI("#{NETGSM_URL}/sms/rest/v2/send")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
req.basic_auth(ENV["NETGSM_USERNAME"], ENV["NETGSM_PASSWORD"]) # username = otpmock API key in tests
req.body = { msgheader: "BASLIGIM", encoding: "TR", messages: [{ msg: "Doğrulama kodunuz: #{code}", no: "5321234567" }] }.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) } // Netgsm has no official Go SDK; call its HTTP API with net/http, only the host changes.
base := os.Getenv("OTPMOCK_URL")
if base == "" {
base = "https://api.netgsm.com.tr"
}
body, _ := json.Marshal(map[string]any{
"msgheader": os.Getenv("NETGSM_HEADER"), "encoding": "TR",
"messages": []map[string]string{{"msg": msg, "no": "5321234567"}},
})
req, _ := http.NewRequest("POST", base+"/sms/rest/v2/send", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.SetBasicAuth(os.Getenv("NETGSM_USERNAME"), os.Getenv("NETGSM_PASSWORD")) // username = otpmock key in tests
res, err := http.DefaultClient.Do(req) // body code "00" = queuedconst 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(); import os
import requests
VERIMOR_URL = os.environ.get("OTPMOCK_URL", "https://sms.verimor.com.tr")
res = requests.post(f"{VERIMOR_URL}/v2/send.json", json={
"username": os.environ["VERIMOR_USERNAME"],
"password": os.environ["VERIMOR_PASSWORD"], # otpmock API key in tests
"source_addr": "BASLIGIM",
"messages": [{"msg": f"Doğrulama kodunuz: {code}", "dest": "905321234567"}],
})
campaign_id = res.textString base = System.getenv().getOrDefault("OTPMOCK_URL", "https://sms.verimor.com.tr");
// json: {"username":"...","password":"<VERIMOR_PASSWORD, otpmock API key in tests>","source_addr":"BASLIGIM",
// "messages":[{"msg":"...","dest":"905321234567"}]}
HttpRequest req = HttpRequest.newBuilder(URI.create(base + "/v2/send.json"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
// Response body is the campaign ID as plain text.var verimorUrl = Environment.GetEnvironmentVariable("OTPMOCK_URL") ?? "https://sms.verimor.com.tr";
var http = new HttpClient { BaseAddress = new Uri(verimorUrl) };
var res = await http.PostAsJsonAsync("/v2/send.json", new
{
username = Environment.GetEnvironmentVariable("VERIMOR_USERNAME"),
password = Environment.GetEnvironmentVariable("VERIMOR_PASSWORD"), // otpmock API key in tests
source_addr = "BASLIGIM",
messages = new[] { new { msg = $"Doğrulama kodunuz: {code}", dest = "905321234567" } },
});
var campaignId = await res.Content.ReadAsStringAsync(); use GuzzleHttp\Client;
$verimorUrl = getenv('OTPMOCK_URL') ?: 'https://sms.verimor.com.tr';
$campaignId = (string) (new Client())->post("$verimorUrl/v2/send.json", [
'json' => [
'username' => getenv('VERIMOR_USERNAME'),
'password' => getenv('VERIMOR_PASSWORD'), // otpmock API key in tests
'source_addr' => 'BASLIGIM',
'messages' => [['msg' => "Doğrulama kodunuz: $code", 'dest' => '905321234567']],
],
])->getBody(); require "net/http"
require "json"
# Verimor has no official Ruby SDK; apps call the REST API, so only the host changes.
VERIMOR_URL = ENV.fetch("OTPMOCK_URL", "https://sms.verimor.com.tr")
res = Net::HTTP.post(
URI("#{VERIMOR_URL}/v2/send.json"),
{
username: ENV["VERIMOR_USERNAME"],
password: ENV["VERIMOR_PASSWORD"], # otpmock API key in tests
source_addr: "BASLIGIM",
messages: [{ msg: "Doğrulama kodunuz: #{code}", dest: "905321234567" }],
}.to_json,
"Content-Type" => "application/json"
)
campaign_id = res.body // Verimor has no official Go SDK; call its HTTP API with net/http, only the host changes.
base := os.Getenv("OTPMOCK_URL")
if base == "" {
base = "https://sms.verimor.com.tr"
}
body, _ := json.Marshal(map[string]any{
"username": os.Getenv("VERIMOR_USERNAME"),
"password": os.Getenv("VERIMOR_PASSWORD"), // otpmock API key in tests
"source_addr": "BASLIGIM",
"messages": []map[string]string{{"msg": msg, "dest": "905321234567"}},
})
res, err := http.Post(base+"/v2/send.json", "application/json", bytes.NewReader(body)) // plain-text campaign IDconst MUTLUCELL_URL = process.env.OTPMOCK_URL ?? "https://smsgw.mutlucell.com";
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<smspack ka="${process.env.MUTLUCELL_USER}" pwd="${process.env.MUTLUCELL_PASSWORD}" org="MUTLUCELL">
<mesaj><metin>Doğrulama kodunuz: ${code}</metin><nums>5321234567</nums></mesaj>
</smspack>`;
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 import os
import requests
MUTLUCELL_URL = os.environ.get("OTPMOCK_URL", "https://smsgw.mutlucell.com")
xml = f"""<?xml version="1.0" encoding="UTF-8"?>
<smspack ka="{os.environ['MUTLUCELL_USER']}" pwd="{os.environ['MUTLUCELL_PASSWORD']}" org="MUTLUCELL">
<mesaj><metin>Doğrulama kodunuz: {code}</metin><nums>5321234567</nums></mesaj>
</smspack>""" # pwd = otpmock API key in tests
res = requests.post(f"{MUTLUCELL_URL}/smsgw-ws/sndblkex", data=xml.encode("utf-8"),
headers={"Content-Type": "text/xml; charset=UTF-8"})
result = res.text # "$12345678#1.0" or an error codeString base = System.getenv().getOrDefault("OTPMOCK_URL", "https://smsgw.mutlucell.com");
String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><smspack ka=\"" + user + "\" pwd=\"" + pwd // pwd = otpmock API key in tests
+ "\" org=\"MUTLUCELL\"><mesaj><metin>" + text + "</metin><nums>05321234567</nums></mesaj></smspack>";
HttpRequest req = HttpRequest.newBuilder(URI.create(base + "/smsgw-ws/sndblkex"))
.header("Content-Type", "text/xml; charset=UTF-8")
.POST(HttpRequest.BodyPublishers.ofString(xml))
.build();
// Response: "$12345678#1.0" or an error code such as "23". using System.Security;
var mutlucellUrl = Environment.GetEnvironmentVariable("OTPMOCK_URL") ?? "https://smsgw.mutlucell.com";
var user = Environment.GetEnvironmentVariable("MUTLUCELL_USER");
var pwd = Environment.GetEnvironmentVariable("MUTLUCELL_PASSWORD"); // otpmock API key in tests
var xml = $"<?xml version=\"1.0\" encoding=\"UTF-8\"?><smspack ka=\"{SecurityElement.Escape(user)}\" pwd=\"{SecurityElement.Escape(pwd)}\" org=\"MUTLUCELL\">"
+ $"<mesaj><metin>Doğrulama kodunuz: {code}</metin><nums>5321234567</nums></mesaj></smspack>";
var res = await new HttpClient().PostAsync(mutlucellUrl + "/smsgw-ws/sndblkex",
new StringContent(xml, Encoding.UTF8, "text/xml"));
var result = await res.Content.ReadAsStringAsync(); // "$12345678#1.0" or an error code use GuzzleHttp\Client;
$mutlucellUrl = getenv('OTPMOCK_URL') ?: 'https://smsgw.mutlucell.com';
$user = htmlspecialchars(getenv('MUTLUCELL_USER'));
$pwd = htmlspecialchars(getenv('MUTLUCELL_PASSWORD')); // otpmock API key in tests
$xml = '<?xml version="1.0" encoding="UTF-8"?>'
. "<smspack ka=\"$user\" pwd=\"$pwd\" org=\"MUTLUCELL\">"
. "<mesaj><metin>Doğrulama kodunuz: $code</metin><nums>5321234567</nums></mesaj></smspack>";
$result = (string) (new Client())->post("$mutlucellUrl/smsgw-ws/sndblkex", [
'headers' => ['Content-Type' => 'text/xml; charset=UTF-8'],
'body' => $xml,
])->getBody(); // "$12345678#1.0" or an error code require "net/http"
# Apps post the XML directly, so only the host changes.
MUTLUCELL_URL = ENV.fetch("OTPMOCK_URL", "https://smsgw.mutlucell.com")
xml = <<~XML
<?xml version="1.0" encoding="UTF-8"?>
<smspack ka="#{ENV["MUTLUCELL_USER"]}" pwd="#{ENV["MUTLUCELL_PASSWORD"]}" org="MUTLUCELL">
<mesaj><metin>Doğrulama kodunuz: #{code}</metin><nums>5321234567</nums></mesaj>
</smspack>
XML
# pwd = otpmock API key in tests
res = Net::HTTP.post(URI("#{MUTLUCELL_URL}/smsgw-ws/sndblkex"), xml, "Content-Type" => "text/xml; charset=UTF-8")
result = res.body # "$12345678#1.0" or an error code // Mutlucell has no official Go SDK; call its HTTP API with net/http, only the host changes.
base := os.Getenv("OTPMOCK_URL")
if base == "" {
base = "https://smsgw.mutlucell.com"
}
xml := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<smspack ka="%s" pwd="%s" org="MUTLUCELL"><mesaj><metin>%s</metin><nums>5321234567</nums></mesaj></smspack>`,
os.Getenv("MUTLUCELL_USER"), os.Getenv("MUTLUCELL_PASSWORD"), msg) // pwd = otpmock API key in tests
res, err := http.Post(base+"/smsgw-ws/sndblkex", "text/xml; charset=UTF-8", strings.NewReader(xml)) // "$12345678#1.0" or an error code 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/`; import os
import requests
ILETI_URL = os.environ.get("OTPMOCK_URL", "https://api.iletimerkezi.com")
res = requests.post(f"{ILETI_URL}/v1/send-sms/json", json={"request": {
"authentication": {
"key": os.environ["ILETIMERKEZI_API_KEY"], # otpmock API key in tests
"hash": os.environ["ILETIMERKEZI_API_HASH"],
},
"order": {"sender": "BASLIGIM", "sendDateTime": [], "iys": "0",
"message": {"text": f"Doğrulama kodunuz: {code}", "receipents": {"number": ["5321234567"]}}},
}})String base = System.getenv().getOrDefault("OTPMOCK_URL", "https://api.iletimerkezi.com");
// json: {"request":{"authentication":{"key":"<ILETIMERKEZI_API_KEY, otpmock API key in tests>","hash":"..."},
// "order":{"sender":"...","iys":"0","message":{"text":"...","receipents":{"number":["5051234567"]}}}}}
HttpRequest req = HttpRequest.newBuilder(URI.create(base + "/v1/send-sms/json"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build(); // İleti Merkezi has no official .NET SDK; call the JSON API with HttpClient.
var iletiUrl = Environment.GetEnvironmentVariable("OTPMOCK_URL") ?? "https://api.iletimerkezi.com";
await new HttpClient().PostAsJsonAsync(iletiUrl + "/v1/send-sms/json", new
{
request = new
{
authentication = new
{
key = Environment.GetEnvironmentVariable("ILETIMERKEZI_API_KEY"), // otpmock API key in tests
hash = Environment.GetEnvironmentVariable("ILETIMERKEZI_API_HASH"),
},
order = new
{
sender = "BASLIGIM", sendDateTime = Array.Empty<string>(), iys = "0",
message = new { text = $"Doğrulama kodunuz: {code}", receipents = new { number = new[] { "5051234567" } } },
},
},
}); use IletiMerkezi\IletiMerkeziClient;
$ileti = new IletiMerkeziClient(
getenv('ILETIMERKEZI_API_KEY'), // otpmock API key in tests
getenv('ILETIMERKEZI_API_HASH'),
getenv('ILETIMERKEZI_SENDER'),
);
if ($otpmock = getenv('OTPMOCK_URL')) {
// The SDK has no base URL option; its HTTP client keeps the URL in a private property.
$http = (fn () => $this->httpClient)->call($ileti);
(fn () => $this->baseUrl = rtrim($otpmock, '/') . '/v1/')->call($http);
} require "net/http"
require "json"
# İleti Merkezi has no official Ruby SDK; apps call the REST API, so only the host changes.
ILETI_URL = ENV.fetch("OTPMOCK_URL", "https://api.iletimerkezi.com")
res = Net::HTTP.post(
URI("#{ILETI_URL}/v1/send-sms/json"),
{ request: {
authentication: { key: ENV["ILETIMERKEZI_API_KEY"], hash: ENV["ILETIMERKEZI_API_HASH"] }, # key = otpmock API key in tests
order: { sender: ENV["ILETIMERKEZI_SENDER"], sendDateTime: [], iys: "0",
message: { text: "Doğrulama kodunuz: #{code}", receipents: { number: ["5051234567"] } } },
} }.to_json,
"Content-Type" => "application/json"
) // İleti Merkezi has no official Go SDK; call its HTTP API with net/http, only the host changes.
base := os.Getenv("OTPMOCK_URL")
if base == "" {
base = "https://api.iletimerkezi.com"
}
body, _ := json.Marshal(map[string]any{"request": map[string]any{
"authentication": map[string]string{
"key": os.Getenv("ILETIMERKEZI_API_KEY"), // otpmock API key in tests
"hash": os.Getenv("ILETIMERKEZI_API_HASH"),
},
"order": map[string]any{
"sender": os.Getenv("ILETIMERKEZI_SENDER"), "iys": "0",
"message": map[string]any{"text": msg, "receipents": map[string]any{"number": []string{"5051234567"}}},
},
}})
res, err := http.Post(base+"/v1/send-sms/json", "application/json", bytes.NewReader(body)) // HTTP 200 = success3 Providers
Each emulation follows the provider's real paths, payloads, responses and errors, and is tested end to end in Node.js, Python, Java, C#, PHP, Ruby and Go with the provider's official SDKs. Pick your language below.
twilio@vonage/server-sdk@aws-sdk/client-snstelnyx@aws-sdk/client-pinpoint-sms-voice-v2telesignsdkbandwidth-sdk@infobip-api/sdk@sinch/sdk-core@messagebird/sdkplivo@netgsm/smsHTTP APIHTTP API@iletimerkezi/iletimerkezi-nodeUsing something else? Send to the generic HTTP API from a test-only branch. All providers
4 In your tests
Give every test its own number so parallel workers never read each other's codes. Then wait for the code and type it in.
test("sign up with a phone number", async ({ page }) => {
const phone = otp.randomPhone(); // unique per test
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.config.js: on("task", { waitForCode: (phone) => otp.waitForCode(phone).then((r) => r.code) })
it("signs up with a phone number", () => {
const phone = "+1555" + Cypress._.random(1e7, 9e7);
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");
});
def test_signup(driver):
phone = "+1555" + "".join(random.choices("0123456789", k=7)) # unique per test
driver.find_element(By.NAME, "phone").send_keys(phone)
since = int(time.time() * 1000) - 5000
driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
code = wait_for_code(phone, since) # polls GET /v1/inbox/{phone}/code
driver.find_element(By.NAME, "code").send_keys(code)
it("signs up with an SMS code", async () => {
const phone = otp.randomPhone(); // unique per test
await $("~phone-input").setValue(phone);
const since = Date.now() - 5_000;
await $("~send-code-button").click();
const { code } = await otp.waitForCode(phone, { since });
await $("~code-input").setValue(code);
await expect($("~welcome-screen")).toBeDisplayed();
});
- runScript: ../scripts/new-phone.js # sets output.phone
- tapOn: { id: "phone-input" }
- inputText: ${output.phone}
- evalScript: ${output.since = Date.now() - 5000}
- tapOn: { id: "send-code-button" }
- repeat: # poll until the code is there
times: 30
while: { true: ${output.code == ''} }
commands:
- runScript: { file: ../scripts/fetch-code.js, env: { PHONE: ${output.phone} } }
- inputText: ${output.code}
// "Wait for code" request: GET {{OTPMOCK_URL}}/v1/inbox/{{phone}}/code?since={{since}}
const attempts = Number(pm.collectionVariables.get("attempts")) + 1;
pm.collectionVariables.set("attempts", String(attempts));
if (pm.response.code === 404 && attempts < 50) {
pm.execution.setNextRequest("Wait for code"); // poll again
return;
}
pm.test("code received", () => pm.response.to.have.status(200));
pm.collectionVariables.set("otp", pm.response.json().code);
# Latest code sent to a number (404 until one arrives)
curl https://api.otpmock.com/v1/inbox/%2B15550142/code \
-H "Authorization: Bearer $OTPMOCK_API_KEY"
{ "code": "482913", "messageSid": "SM6926…", "receivedAt": 1791406301420 }
The otp helper is a single dependency-free file. Get it from the docs. Using an AI coding assistant? Point it at otpmock.com/llms-full.txt and it can do the whole integration.
5 Dashboard
Manual testers watch codes land in the browser and click to copy. Engineers manage keys per environment. Everyone sees how much of the month is left.
Your Acme verification code is: 482913
Acme: 7731 is your login code. Don't share it.
Your Acme code: 905126
Use 3388 to confirm your phone number.
Illustration with sample data.
6 Spec sheet
since timestamp ignores anything left over from the previous run.7 Pricing
A message is one SMS your app sends, or one Verify verification it starts. Reading codes is always free.
App → otpmock → Test
App → otpmock → Test
App → otpmock → Test
App → otpmock → Test
Prices in USD, billed monthly. VAT or sales tax is added at checkout where your country requires it (none in most US states; usually none for EU businesses with a VAT ID). Payments are processed by Creem, our merchant of record. Cancel any time. See the refund policy.
8 Questions
No, and that's the point. Messages never leave our API: no carrier fees, no delivery delays, no phones involved. Use otpmock in test and staging environments only.
Barely. You point your provider's SDK at otpmock when an environment variable is set, usually one option such as Twilio's httpClient, Vonage's restHost or the AWS SDK's endpoint. Your production configuration stays exactly as it is.
Supported for Twilio Verify v2, Vonage Verify v2, Telnyx Verify, Infobip 2FA, Sinch Verification, Plivo Verify, Telesign Verify and Bird Verify. otpmock generates the code, your test reads it from the inbox, and the verification check approves it the same way the provider would, including wrong-code and too-many-attempts errors.
Twilio, Vonage, Telnyx, AWS SNS, AWS End User Messaging, Infobip, Sinch, Bird, Plivo, Telesign, Bandwidth, Netgsm, İleti Merkezi, Verimor and Mutlucell. For anything else, such as Telnyx or an in-house gateway, send to otpmock's generic HTTP API from a test-only branch, and email support@otpmock.com: requests decide what we build next.
You can, but please don't. Test environments should use made-up numbers like the ones randomPhone() generates. Either way, every message is deleted after 10 minutes.
Sending returns HTTP 429 until your allowance resets on the 1st of next month (UTC), or until you upgrade. Reading codes keeps working.
Any time, from your account. You keep access until the end of the period you've paid for. New subscriptions can be refunded within 7 days; see the refund policy.
Get a key in under a minute. The free plan covers a small suite for good.
Get a free API key