Guides / Maestro

How to test SMS OTP verification in Maestro

Updated October 8, 2026
Short answer

Point your backend's SMS provider at a mock inbox in the test environment. In the Maestro flow, a small runScript generates a unique fake number, the flow types it and taps "send code", then a second script calls the inbox with Maestro's built-in http.get inside a repeat loop until a code arrives. The flow types it with inputText: ${output.code}.

Step 1: route SMS to the mock

The app does not send the SMS; your backend does. In the test or staging backend, point the SMS provider's SDK at otpmock with one option, documented on each provider page. The app build under test only needs to talk to that backend.

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"}

Step 3: two small scripts

Maestro runs JavaScript (GraalJS by default) in a sandbox without Node modules, but it provides http.get/http.post, a global json() parser and an output object whose fields later steps read as ${output.name}. Variables passed in a runScript env block are available in the script as globals.

scripts/new-phone.js
// Unique fake number per run so parallel flows never share an inbox.
var digits = '';
for (var i = 0; i < 7; i++) digits += Math.floor(Math.random() * 10);
output.phone = '+1555' + digits;
output.code = '';
scripts/fetch-code.js
// One attempt: sets output.code when a code has arrived, otherwise leaves it empty.
var url = OTPMOCK_URL + '/v1/inbox/' + encodeURIComponent(PHONE) + '/code?since=' + SINCE;
var response = http.get(url, {
  headers: { 'Authorization': 'Bearer ' + OTPMOCK_API_KEY }
});

if (response.status === 200) {
  output.code = json(response.body).code;
} else if (response.status === 404) {
  // Not there yet. Maestro JS has no sleep, so wait briefly before the next attempt.
  var until = Date.now() + 500;
  while (Date.now() < until) {}
} else {
  throw new Error('otpmock ' + response.status + ': ' + response.body);
}

Step 4: the flow

flows/signup-otp.yaml
appId: ${APP_ID}
env:
  OTPMOCK_URL: https://api.otpmock.com
---
- launchApp:
    clearState: true
- runScript: ../scripts/new-phone.js
- tapOn:
    id: "phone-input"
- inputText: ${output.phone}
# Take "since" just before the SMS is triggered, minus 5 s for clock skew.
- evalScript: ${output.since = Date.now() - 5000}
- tapOn:
    id: "send-code-button"

# Poll up to 30 times (about 15 s) until the script sets output.code.
- repeat:
    times: 30
    while:
      true: ${output.code == ''}
    commands:
      - runScript:
          file: ../scripts/fetch-code.js
          env:
            OTPMOCK_URL: ${OTPMOCK_URL}
            OTPMOCK_API_KEY: ${OTPMOCK_API_KEY}
            PHONE: ${output.phone}
            SINCE: ${output.since}
- assertTrue: ${output.code != ''}

- tapOn:
    id: "code-input"
- inputText: ${output.code}
- tapOn:
    id: "verify-button"
- assertVisible:
    id: "welcome-screen"

The repeat command stops as soon as the while condition becomes false, and times caps it if the code never arrives. The assertTrue afterwards turns a timeout into a clear failure instead of typing an empty string. Avoid the retry command for this: it allows at most 3 retries and is meant for flaky UI steps, not for waiting on data.

Running it with the API key

Keep the key out of the YAML and pass it on the command line with -e (or --env):

shell
maestro test \
  -e APP_ID=com.example.app \
  -e OTPMOCK_API_KEY="$OTPMOCK_API_KEY" \
  flows/signup-otp.yaml

In CI, read the variable from your secret store. On Maestro Cloud, pass it the same way with -e to the cloud upload command. The HTTP call is made by the Maestro runner, so the device itself needs no network access to otpmock.

Why not wait for the SMS on the device?

No SMS reaches the emulator, simulator or phone, so iOS code autofill and Android SMS Retriever prompts never appear. The flow types the code, which keeps it deterministic and identical on both platforms.

Common pitfalls

FAQ

Can I use the otpmock TypeScript helper in Maestro?

No. Maestro scripts run in a sandbox without Node modules or fetch. The two short scripts above replace it using Maestro's own http.get.

Does the busy wait slow the device?

No. The script runs in Maestro's JavaScript engine, not inside your app, and each wait lasts half a second. Maestro has no sleep command for scripts, which is why the loop waits this way.

Can several flows run in parallel?

Yes. Each flow generates its own number, so codes never mix between flows or devices.

Try it on your own flows

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

Get a free API key

Related guides