Route your app's SMS to a mock inbox in the test environment. In Robot Framework, generate a unique phone number, store the time, trigger the SMS through the UI, then call a keyword that reads the inbox with RequestsLibrary inside Wait Until Keyword Succeeds. Type the returned code with SeleniumLibrary or Browser.
Step 1: route SMS to the mock
The application under test sends the SMS. 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. Robot only reads the inbox.
Step 2: the inbox API
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: libraries
robotframework>=7.0
robotframework-requests>=0.9.7
robotframework-seleniumlibrary # or robotframework-browserStep 4: an otpmock resource file
Put the inbox keywords in one resource so every suite can import them. GET On Session with expected_status=200 fails on the 404 that means "not yet", which is exactly what Wait Until Keyword Succeeds needs to retry.
*** Settings ***
Library RequestsLibrary
Library String
*** Keywords ***
Open Otpmock Session
&{headers}= Create Dictionary Authorization=Bearer %{OTPMOCK_API_KEY}
Create Session otpmock %{OTPMOCK_URL=https://api.otpmock.com} headers=${headers} verify=${True}
Random Phone
${digits}= Generate Random String 7 [NUMBERS]
RETURN +1555${digits}
Now Minus Skew
${since}= Evaluate int(time.time() * 1000) - 5000 modules=time
RETURN ${since}
Latest OTP Should Exist
[Arguments] ${phone} ${since}
${encoded}= Evaluate urllib.parse.quote($phone, safe='') modules=urllib.parse
&{params}= Create Dictionary since=${since}
${resp}= GET On Session otpmock /v1/inbox/${encoded}/code params=${params} expected_status=200
RETURN ${resp.json()}[code]
Wait For OTP
[Arguments] ${phone} ${since} ${timeout}=15s
${code}= Wait Until Keyword Succeeds ${timeout} 300ms Latest OTP Should Exist ${phone} ${since}
RETURN ${code}Wait Until Keyword Succeeds returns the inner keyword's return value, so Wait For OTP hands the code straight back to the test.
Step 5: the test with SeleniumLibrary
*** Settings ***
Library SeleniumLibrary
Resource ../resources/otpmock.resource
Suite Setup Open Otpmock Session
Test Teardown Close Browser
*** Test Cases ***
Phone Signup With SMS Code
${phone}= Random Phone
Open Browser https://staging.example.com/signup headlesschrome
Input Text name:phone ${phone}
${since}= Now Minus Skew
Click Button css:button[type=submit]
${code}= Wait For OTP ${phone} ${since}
Input Text name:code ${code}
Click Button css:button[type=submit]
Wait Until Page Contains WelcomeWith the Browser library (Playwright)
Only the UI keywords change; the otpmock resource stays the same.
*** Settings ***
Library Browser
Resource ../resources/otpmock.resource
Suite Setup Open Otpmock Session
*** Test Cases ***
Phone Signup With SMS Code
${phone}= Random Phone
New Page https://staging.example.com/signup
Fill Text input[name=phone] ${phone}
${since}= Now Minus Skew
Click button[type=submit]
${code}= Wait For OTP ${phone} ${since}
Fill Text input[name=code] ${code}
Click button[type=submit]
Get Text body contains WelcomeRunning in CI
Set OTPMOCK_API_KEY as a secret and run robot tests/. If the endpoint uses a private CA, point requests at it: either export REQUESTS_CA_BUNDLE, or pass the path as verify=%{REQUESTS_CA_BUNDLE} in Create Session. Do not use verify=${False} outside local experiments. With pabot each test still uses its own number, so parallel runs never read each other's codes.
Common pitfalls
- Forgetting
expected_status=200. The default already fails on 4xx, butexpected_status=any(or a copied keyword that sets it) would turn the 404 into a "success" with no code. Keep it at 200. Sleepinstead of polling. A fixed sleep is slow when the SMS is fast and flaky when it is slow.- Unencoded
+. Quote the number before putting it in the path. - Taking
sinceafter the click. Take it before the action that sends the SMS.
FAQ
Do I need a custom Python library?
No. RequestsLibrary and the built-in Wait Until Keyword Succeeds cover the whole flow.
Does it work for mobile tests with AppiumLibrary?
Yes. Reading the code is the same HTTP call; only the keywords that type it change.
The free plan includes 100 messages a month. No card required.
Get a free API key