mxlabs-number-verify
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.
Install
pip install mxlabs-number-verify
Quick start
One 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 SMS
Advanced — full control
verify_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)
Why "open the URL over cellular"?
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.
API
MXNumberVerify(api_key, backend_url=..., timeout=30.0, transport=None)
| 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 |
Methods
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"}.
Status values
verify / poll_verify return a status. Poll until it is terminal:
status |
Meaning | What to do |
|---|---|---|
pending |
The silent check is still running, waiting on the operator. Not terminal. | Keep polling verify (poll_verify does this for you). |
verified |
The number is confirmed. method shows how (number_verification or sms_otp); phone echoes it back. |
Grant access. |
not_verified |
The operator checked and the number is not active on this device. | Do not grant access; offer the SMS fallback (fallback: "sms_otp"). |
nv_unavailable |
The silent check could not run (e.g. Wi-Fi, timeout, or sandbox). reason says why. |
Fall back to an SMS one-time code (sms_send → sms_verify). |
SMS backstop statuses: sms_send → sms_sent (with masked, expires_in);
sms_verify → verified / invalid (wrong code) / expired (code timed out).
Errors
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"
Try it in your browser
Live interactive demo (sandbox, no signup): https://api.mxlab.sg/
Sandbox — try it in 10 minutes, no real phone
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}
API contract
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.
License
MIT © MX Labs
Metadata
Release files for mxlabs-number-verify 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| mxlabs_number_verify-0.1.2.tar.gz | 11.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mxlabs_number_verify-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 20.0 kB
Release files / mxlabs_number_verify-0.1.2.tar.gz
| Download URL | mxlabs_number_verify-0.1.2.tar.gz |
|---|---|
| Size | 11.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
971fd89344a5b3f62b7c0d372e347c7b243ee0858accfffe16998d0fdc94e5ee
|
|
BLAKE2b-256 checksum How to use checksums |
309a683efd493741f18761b65dffdc666bf80104fe73629c2c10902f617a7161
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.12
|
Release files / mxlabs_number_verify-0.1.2-py3-none-any.whl
| Download URL | mxlabs_number_verify-0.1.2-py3-none-any.whl |
|---|---|
| Size | 8.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
03df711c74e73ee9c896bd48bbfcdf7ba235f3ba46367ccd26b054c2586c4d6d
|
|
BLAKE2b-256 checksum How to use checksums |
8c9334b5347c6b085de8a03f2ac08c8cc0301e43f86f5b628d1f1fb88e154bc6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.12
|