Thin Python client for silent phone-number verification — CAMARA Number Verification delivered through the MX Labs Open Gateway — with an automatic SMS OTP fallback.
Your app never touches telco keys, JWT signing, or the OIDC/CIBA dance. You call two methods; the hosted backend brokers every operator's auth and gives you back a yes/no.
┌──────────────┐ start / verify / sms_* ┌────────────────────────┐ CAMARA ┌────────────┐
│ your server │ ────────────────────────► │ MX Labs Open Gateway │ ─────────► │ operator │
│ (this SDK) │ ◄──────────────────────── │ (keys stay here) │ │ network │
└──────────────┘ JSON └────────────────────────┘ └────────────┘
- Zero dependencies. Standard library only. Python 3.8+.
- Silent when it can be, SMS when it can't. One flow, graceful fallback.
- Open-core. This client is MIT. Point it at the MX Labs hosted backend (recommended) or your own backend running against your own Bridge Alliance credentials — same wire contract.
pip install mxlabs-number-verifyOne call. The SDK handles the session and polling; you just open the auth URL on the user's device (that cellular fetch is what silently proves the number).
import os
from mxlabs_number_verify import MXNumberVerify
nv = MXNumberVerify(api_key=os.environ["MXLABS_API_KEY"])
result = nv.verify_number(
"+60123456789",
on_auth_url=lambda url, _sid: redirect_user_to_url(url), # device opens over cellular
)
if result["status"] == "verified":
grant_access(result["phone"])
else:
nv.sms_send(result["session_id"]) # fallback: OTP by SMSverify_number is sugar over four primitives; use them directly to drive the
session, redirect, and fallback yourself:
started = nv.start("+60123456789")
# have the device open started["auth_url"] over cellular, then:
result = nv.poll_verify(started["session_id"])
if result["status"] != "verified":
sent = nv.sms_send(started["session_id"])
code = input(f"Enter the code sent to {sent['masked']}: ")
nv.sms_verify(started["session_id"], code)Silent Number Verification works by having the device's mobile network
confirm the SIM's number — no SMS, no code, no user action. That only fires when
the authorize URL is fetched over the cellular data path (not Wi-Fi). If the
device is on Wi-Fi or the operator can't verify, the call returns
nv_unavailable / not_verified with fallback: "sms_otp" — switch to the SMS
flow. The SDK surfaces this for you; you never guess.
| arg | notes |
|---|---|
api_key |
required — issued by MX Labs |
backend_url |
hosted backend (default https://api.mxlab.sg/sdk.php), or your own |
timeout |
per-request network timeout in seconds |
transport |
optional callable(url, headers, body, timeout) -> (status, dict) for tests / a custom HTTP stack |
ping()→{"status": "ok", "partner": ...}.start(phone)→{"session_id", "auth_url", "callback_url", ...}.phonemay be E.164 (+60...) or local digits.verify(session_id, code=None)→ one check;statusispending,verified,not_verified, ornv_unavailable.poll_verify(session_id, interval=2.0, timeout=30.0)→ pollsverifyuntil a terminal status or timeout. On timeout returns{"status": "nv_unavailable", "reason": "timeout", "fallback": "sms_otp"}.sms_send(session_id)→{"status": "sms_sent", "masked", "expires_in"}.sms_verify(session_id, code)→{"status": "verified" | "invalid" | "expired"}.
Transport failures, non-2xx HTTP, and {"status": "error"} bodies raise
MXNumberVerifyError with a machine-readable .code (e.g. invalid_api_key,
invalid_phone, session_not_found_or_expired), plus .http_status and
.response. Non-error verification outcomes (not_verified, nv_unavailable,
invalid, expired) are returned, not raised — they're normal results.
from mxlabs_number_verify import MXNumberVerifyError
try:
nv.start("nope")
except MXNumberVerifyError as e:
print(e.code) # "invalid_phone"Live interactive demo (sandbox, no signup): https://api.mxlab.sg/
Ask MX Labs for a sandbox key (sbx_…). Sandbox keys are fully simulated:
they never touch a real operator or send a real SMS, and only accept these
magic test numbers:
| Number | Simulates |
|---|---|
+10000000001 |
silent verification succeeds |
+10000000002 |
silent NV unavailable → SMS OTP fallback |
+10000000003 |
NV reachable but number doesn't match |
In sandbox the SMS OTP is always 000000. Everything else is identical to
production — same methods, same responses (with an extra "sandbox": true). Swap
in a live key for real numbers; no code changes.
nv = MXNumberVerify(api_key="sbx_...")
started = nv.start("+10000000001")
nv.verify(started["session_id"]) # -> {"status": "verified", "sandbox": True}The raw HTTP contract is published as an OpenAPI 3.1 spec: mxlabs/number-verify-openapi. Use it to generate a client in any other language.
MIT © MX Labs