Three form-encoded POSTs cover the whole loop: send a one-time passcode by SMS, fall back to a voice call, verify what the user typed. The code itself is generated, encrypted and expired by Signalmash Verifier, so your application never creates or stores a passcode. Base URL https://api.signalmash.com.
Every request carries a token in the Authorization header. Tokens are created in the portal under API → Tokens and can be scoped per function, so the service that sends codes does not need the permissions of the one that buys numbers. Give the OTP integration its own token and rotate it on its own schedule.
Authorization: YOUR_API_TOKEN
Verifier creates a unique, encrypted passcode tied to one user session, delivers it, and keeps it valid only for the window you configure on the account (anywhere from 30 seconds to several minutes). Your side of the exchange is three steps: call POST /sendotpbysms when the user needs a code, collect what they type, then call POST /verifyotp with the code and the session identifier from the send. A 200 on the verify call is the only thing that means the user passed.
POST /sendotpbysms with an application/x-www-form-urlencoded body of three required fields. FROM is your Signalmash number (local, toll-free or short code), TO is the recipient, and TemplateKey selects the message template the code is inserted into, so the wording, brand name and expiry text live on the account rather than in your code. A 200 means the code was sent; keep the session identifier returned with it, because the verify call needs it.
curl -X POST https://api.signalmash.com/sendotpbysms \ -H "Authorization: YOUR_API_TOKEN" \ -d "FROM=15035550100" \ -d "TO=15035550199" \ -d "TemplateKey=login_code"
import requests
resp = requests.post(
"https://api.signalmash.com/sendotpbysms",
headers={"Authorization": "YOUR_API_TOKEN"},
data={"FROM": "15035550100", "TO": "15035550199", "TemplateKey": "login_code"},
timeout=10,
)
resp.raise_for_status() # 200 = OTP sent
session_id = resp.json().get("SessionId") # store against the login attemptconst res = await fetch("https://api.signalmash.com/sendotpbysms", {
method: "POST",
headers: {
Authorization: process.env.SIGNALMASH_TOKEN,
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({ FROM: "15035550100", TO: "15035550199", TemplateKey: "login_code" }),
});
if (!res.ok) throw new Error(`OTP send failed: ${res.status}`);
const { SessionId } = await res.json(); // keep for /verifyotpSome users are on a landline, some carriers are slow, some people simply prefer a call. POST /sendotpbycall reads the code out over the phone. It takes the same FROM and TO plus a boolean Template field. Offer it as a "call me instead" link after the first SMS attempt rather than as the default, so the SMS path stays the cheap one.
curl -X POST https://api.signalmash.com/sendotpbycall \ -H "Authorization: YOUR_API_TOKEN" \ -d "FROM=15035550100" \ -d "TO=15035550199" \ -d "Template=true"
POST /verifyotp with OTP (what the user typed) and SessionId (from the send). 200 means verified. Anything else, including an expired code, is a failure: an expired passcode is automatically invalid, so the right response is to offer a fresh send, not to retry the same one.
curl -X POST https://api.signalmash.com/verifyotp \ -H "Authorization: YOUR_API_TOKEN" \ -d "OTP=482913" \ -d "SessionId=SESSION_FROM_SEND"
resp = requests.post(
"https://api.signalmash.com/verifyotp",
headers={"Authorization": "YOUR_API_TOKEN"},
data={"OTP": user_input, "SessionId": session_id},
timeout=10,
)
verified = resp.status_code == 200200 from /verifyotp means the person holds the phone.SMS one-time passcodes reach every phone with no app install, which is why they remain the default second factor for consumer logins and transaction confirmations. Time-based codes from an authenticator app are stronger against SIM-swap attacks but need enrolment; our explainer on time-based one-time passwords covers the trade-off. For fintech and banking flows where the sender's identity matters as much as the code, RCS verified messaging shows a verified brand mark next to the passcode. The wider view is in 2FA and OTP as a service.
OTP messages are billed like any other SMS or voice minute on the account, per message or per call, with carrier pass-through fees that every US provider charges identically. Current rates are on the pricing page. If you also send notifications or marketing, the same account and token serve the SMS API.
Tell us your expected verification volume and which countries you need to reach, and we will set up the sending number, the template and the registration so the first call you make is the send.