Thin Node 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 functions; 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. Uses the built-in
fetch(Node 18+). - 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 at your own backend running against your own Bridge Alliance credentials — same wire contract.
npm 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).
const { MXNumberVerify } = require('mxlabs-number-verify');
const nv = new MXNumberVerify({ apiKey: process.env.MXLABS_API_KEY });
const result = await nv.verifyNumber('+60123456789', {
onAuthUrl: (url) => redirectUserToUrl(url), // device opens it over cellular
});
if (result.status === 'verified') grantAccess(result.phone);
else await nv.smsSend(result.sessionId); // fallback: OTP by SMSverifyNumber is sugar over four primitives; use them directly when you want to
drive the session, redirect, and fallback yourself:
const { sessionId, authUrl } = await nv.start('+60123456789');
// have the device open authUrl over cellular, then:
const result = await nv.pollVerify(sessionId);
if (result.status !== 'verified') {
const { masked } = await nv.smsSend(sessionId);
const code = await askUser(`Enter the code sent to ${masked}`);
await nv.smsVerify(sessionId, 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.
| option | type | default | notes |
|---|---|---|---|
apiKey |
string | — | required — issued by MX Labs |
backendUrl |
string | https://api.mxlab.sg/sdk.php |
hosted backend, or your own |
timeoutMs |
number | 30000 |
per-request network timeout |
fetch |
function | global fetch |
override for tests / older runtimes |
All methods return Promises.
ping()→{ status: 'ok', partner }. Health/key check.start(phone)→{ sessionId, authUrl, callbackUrl }.phonemay be E.164 (+60...) or local digits.verify(sessionId, { code? })→ one check.statusispending,verified,not_verified, ornv_unavailable. Passcodeonly if your client captured the auth code from the redirect itself.pollVerify(sessionId, { intervalMs = 2000, timeoutMs = 30000 })→ pollsverifyuntil a terminal status or timeout. On timeout resolves to{ status: 'nv_unavailable', reason: 'timeout', fallback: 'sms_otp' }.smsSend(sessionId)→{ status: 'sms_sent', masked, expires_in }. The OTP never leaves the backend.smsVerify(sessionId, code)→{ status: 'verified' | 'invalid' | 'expired' }.
Transport failures, non-2xx HTTP, and {status:"error"} bodies throw
MXNumberVerifyError with a machine-readable .code (e.g. invalid_api_key,
invalid_phone, session_not_found_or_expired), plus .httpStatus and
.response. Non-error verification outcomes (not_verified, nv_unavailable,
invalid, expired) are returned, not thrown — they're normal results.
const { MXNumberVerifyError } = require('mxlabs-number-verify');
try {
await nv.start('nope');
} catch (e) {
if (e instanceof MXNumberVerifyError) console.error(e.code); // "invalid_phone"
}See examples/basic.js:
MXLABS_API_KEY=xxx node examples/basic.js +60123456789Types ship with the package (src/index.d.ts) — no
@types install needed.
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 they only accept these
magic test numbers — so you can walk the whole flow from your laptop:
| 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 in the API is
identical to production — same functions, same responses (with an extra
"sandbox": true). When you're ready for real numbers, swap in a live key; no
code changes.
const nv = new MXNumberVerify({ apiKey: 'sbx_...' });
const { sessionId } = await nv.start('+10000000001');
const r = await nv.verify(sessionId); // → { status: 'verified', sandbox: true }Request one from MX Labs. The hosted backend brokers Bridge Alliance's Open Gateway across its operator networks, so a single key works across every supported country.
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