Silent phone-number verification (CAMARA Number Verification) with SMS OTP fallback — thin client for the MX Labs Open Gateway.
Project description
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
import os
from mxlabs_number_verify import MXNumberVerify
nv = MXNumberVerify(api_key=os.environ["MXLABS_API_KEY"])
# 1. Start a session for the number the user claims to own.
started = nv.start("+60123456789")
# 2. Have the USER'S DEVICE open started["auth_url"] over its CELLULAR data
# connection. That mobile-network path is what silently proves the number.
# 3. Poll for the result.
result = nv.poll_verify(started["session_id"])
if result["status"] == "verified":
grant_access(result["phone"]) # silently verified — no OTP, no friction
else:
# 4. Fallback: send an OTP by SMS.
sent = nv.sms_send(started["session_id"])
code = input(f"Enter the code sent to {sent['masked']}: ")
if nv.sms_verify(started["session_id"], code)["status"] == "verified":
grant_access(result.get("phone"))
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"}.
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mxlabs_number_verify-0.1.0.tar.gz.
File metadata
- Download URL: mxlabs_number_verify-0.1.0.tar.gz
- Upload date:
- Size: 7.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
61b98c9e2baf646dfa4b4dbd9423292bbec27dfd7d243d8c67a87184a9da6e60
|
|
| MD5 |
ad6f1f243a989b9a2041be61f5b3b079
|
|
| BLAKE2b-256 |
6277950479cf468b331bede3e223c2d219a9d225ba7fd67ec1c595ecd4ef4677
|
File details
Details for the file mxlabs_number_verify-0.1.0-py3-none-any.whl.
File metadata
- Download URL: mxlabs_number_verify-0.1.0-py3-none-any.whl
- Upload date:
- Size: 8.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f9913f2bc37bcf334c4e0f8c35cfd4add309dabefc971dac93a0319542630e55
|
|
| MD5 |
53f260f57ed2dcf953a95f5e27a10a5a
|
|
| BLAKE2b-256 |
5d4ae99059712646766d0a2788d18c20bf767cd50d34dd912ad00fded5c5d2fe
|