Guides / Postman

How to test SMS OTP flows in Postman and Newman

Updated October 8, 2026
Short answer

Route your app's SMS to a mock inbox in the test environment. In the collection, a pre-request script picks a unique phone number and stores the current time. The request that triggers the SMS uses that number, the next request reads the inbox and retries itself with pm.execution.setNextRequest until the code arrives, and the verify request sends it back to your API. Newman runs the same collection in CI.

Step 1: route SMS to the mock

Your API sends the SMS, not Postman. In the test or staging environment, point your provider's SDK at otpmock with one option. Each provider's option is on its provider page. Postman then only needs to call your API and the otpmock inbox.

Step 2: the inbox API

HTTP
GET https://api.otpmock.com/v1/inbox/{phone}/code?since={unix_ms}
Authorization: Bearer {{OTPMOCK_API_KEY}}

200 {"code":"482913","messageSid":"SM…","body":"Your code is 482913","receivedAt":1791417652342}
404 {"error":"no_code_yet"}

The phone number goes in the path, so URL-encode it: +15551234567 becomes %2B15551234567.

Step 3: collection variables

Keep the otpmock URL and key as collection variables (or in an environment) and set the collection's Authorization to Bearer Token with {{OTPMOCK_API_KEY}}. Requests to your own API can override auth per request. The per-run values (phone, since, attempts) are written by scripts with pm.collectionVariables.set, so they never leak between collections.

Step 4: trigger the SMS

On the request that starts verification (for example POST {{baseUrl}}/auth/phone/start with body {"phone":"{{phone}}"}), add a pre-request script:

Start verification · Pre-request
const phone = '+1555' + Array.from({ length: 7 }, () => Math.floor(Math.random() * 10)).join('');
pm.collectionVariables.set('phone', phone);
pm.collectionVariables.set('phoneEncoded', encodeURIComponent(phone));
pm.collectionVariables.set('since', String(Date.now() - 5000)); // clock skew margin
pm.collectionVariables.set('attempts', '0');

Step 5: poll with setNextRequest

Add a request named Wait for code: GET {{OTPMOCK_URL}}/v1/inbox/{{phoneEncoded}}/code?since={{since}}. Its post-response script reruns the same request while the inbox answers 404:

Wait for code · Post-response
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'); // run this request again
  return;
}
pm.test('code received', () => pm.response.to.have.status(200));
pm.collectionVariables.set('otp', pm.response.json().code);

Add a short pause between attempts in its pre-request script. The sandbox waits for pending timers before sending the request:

Wait for code · Pre-request
if (Number(pm.collectionVariables.get('attempts')) > 0) {
  setTimeout(() => {}, 300);
}

The next request, POST {{baseUrl}}/auth/phone/verify with {"phone":"{{phone}}","code":"{{otp}}"}, finishes the flow. setNextRequest only takes effect in the Collection Runner and Newman, not when you click Send on a single request. Older collections use postman.setNextRequest; it still works, but pm.execution.setNextRequest is the current name.

Alternative: poll inside one script with pm.sendRequest

If you prefer a single request, poll from the verify request's pre-request script. pm.sendRequest returns a promise when called without a callback, and scripts support await:

Verify code · Pre-request
const base = pm.collectionVariables.get('OTPMOCK_URL');
const url = `${base}/v1/inbox/${pm.collectionVariables.get('phoneEncoded')}/code?since=${pm.collectionVariables.get('since')}`;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

for (let i = 0; i < 50; i++) {
  const res = await pm.sendRequest({
    url,
    method: 'GET',
    header: { Authorization: 'Bearer ' + pm.collectionVariables.get('OTPMOCK_API_KEY') },
  });
  if (res.code === 200) { pm.collectionVariables.set('otp', res.json().code); break; }
  if (res.code !== 404) throw new Error('otpmock ' + res.code + ': ' + res.text());
  await sleep(300);
}
if (!pm.collectionVariables.get('otp')) throw new Error('no code arrived');

Clear otp in the trigger request's pre-request script so a value from an earlier run is never reused.

Running it in CI with Newman

Export the collection and pass secrets on the command line, so they never sit in the JSON file:

.github/workflows/api-tests.yml (step)
- name: OTP flow
  run: |
    npx newman run tests/otp.postman_collection.json \
      --env-var "OTPMOCK_URL=https://api.otpmock.com" \
      --env-var "OTPMOCK_API_KEY=${{ secrets.OTPMOCK_API_KEY }}" \
      --env-var "baseUrl=https://staging.example.com"

Environment variables take precedence over collection variables with the same name, so the defaults in the collection are overridden. Newman exits non-zero when any pm.test fails. If your staging API uses a private CA, export NODE_EXTRA_CA_CERTS=/path/to/ca.pem before running Newman rather than turning off SSL checks with --insecure.

Common pitfalls

FAQ

Does this work in the Postman desktop app?

Yes. Run the collection with the Collection Runner so setNextRequest is honoured. The pm.sendRequest variant also works when you click Send.

Can I test without a real backend?

Yes. Replace the trigger request with POST /v1/messages/send on otpmock, which records an SMS as if your app had sent it.

How many messages does a run use?

One per SMS your API sends. Polling the inbox is not counted as a message.

Try it on your own collection

The free plan includes 100 messages a month. No card required.

Get a free API key

Related guides