# 2fa.mn > 2fa.mn verifies that a user owns a Mongolian phone number. The user sends one SMS from their own phone to the short code 150073 (the operator charges the user 150₮, VAT included), and the client's server receives a signed webhook in under a second. Clients pay nothing per verification and earn on every verification. This is MO (user-sent) verification: it proves SIM ownership and makes SMS-pumping fraud impossible. Products: SESSION (one-time 6-digit code, valid 5 minutes, single-use, one-tap `sms:150073?body=CODE` link) and RULE (a fixed text prefix such as "HIHI" that triggers a `rule.matched` webhook). Operator coverage: all six operators live (Unitel, Mobicom, Skytel, G-Mobile, ONDO, Lime), plus Toki (on Unitel's network) and KAOS (on Mobicom's network). ## Docs Get started: - [Quickstart in 5 minutes](https://2fa.mn/en/docs/quickstart): widget button with a test key, redeem the token and check state on your server - [Widget](https://2fa.mn/en/docs/widget): button plus modal (number step, then price, code, SMS button, QR), phone and state options, events, redeem call, security, JWKS - [Introduction](https://2fa.mn/en/docs): how verification by a user-sent SMS works - [Operators](https://2fa.mn/en/docs/operators): live operators, number prefixes (reference only; ported numbers work) Products: - [One-time code (Session)](https://2fa.mn/en/docs/session): one-time codes, phone-bound sessions, statuses, errors - [Fixed text (Rule)](https://2fa.mn/en/docs/rule): text prefixes that trigger actions Integrate: - [Webhooks & signatures](https://2fa.mn/en/docs/webhooks): events, signature verification in Node, Go, PHP, Python, retries, delivery log - [API keys](https://2fa.mn/en/docs/keys): test and live keys, rotation, IP allowlist - [Test mode](https://2fa.mn/en/docs/test-mode): simulate an SMS with POST /v1/test/mo, Dashboard playground - [Build your own UI](https://2fa.mn/en/docs/price-ui): required price notice and SMS button (HTML+JS and React) when not using the widget Earnings: - [Earnings and payouts](https://2fa.mn/en/docs/payouts): statuses, payout timing, 10,000₮ minimum, tax, bank account, acceptable use Reference: - [Error codes](https://2fa.mn/en/docs/errors): error format and every code the API returns - [Limits](https://2fa.mn/en/docs/limits): session, phone, request and webhook limits - [FAQ](https://2fa.mn/en/docs/faq): pricing, operators, ported numbers, testing - [API reference](https://2fa.mn/en/docs/api-reference): every endpoint - [AI coding assistants](https://2fa.mn/en/docs/ai): llms.txt and a ready prompt Mongolian docs live at the same paths without `/en` (for example https://2fa.mn/docs/session). ## Widget (drop-in) The widget draws a "Continue with phone" button; a click opens a centered modal (iframe from verify.2fa.mn, blurred backdrop). The user enters their 8-digit number, then sees the code, the 150₮ price and an SMS button (phone) or QR (desktop), and sends the code from that number. The page cannot alter it. Status arrives by server push (SSE) inside the widget. Why the SMS: the operator delivers it with the sender's number, so sending from that phone proves the user holds the SIM; an SMS from another number is refused and the widget asks the user to send it from the number they entered. Create a publishable key (`pk_test_` / `pk_live_`) and list allowed sites in Dashboard > Widget (`localhost` / `127.0.0.1` on test keys only), then: ```html
``` `TwoFA.button(target, { key, lang?, phone?, state?, accent?, radius?, size? })` returns a handle with `.on('open' | 'close' | 'verified' | 'expired' | 'error', fn)`, `.open()`, `.close()`, `.destroy()`. `verified` gives `{ token }`. `phone` passes a known 8-digit number: the number step is skipped and other senders are refused (bad value: invalid_phone). `state` (1..128 chars `[A-Za-z0-9._~-]`, bad value: invalid_request) is a random value your server stores in the user's session; it is bound into the token and returned by redeem, and you must compare it with the session value (stops a token from one browser being used in someone else's sign-in). `accent` is '#rrggbb' only, `radius` px 0..24 (default 12), `size` 'full' | 'compact'; `lang` defaults to 'mn'. `error` gives the code as a string. `TwoFA.mount(target, { key, lang, phone, state })` renders the widget inline instead (also emits `ready`; no open/close or look overrides). Redeem the token once on the server with the secret key: ```bash curl https://api.2fa.mn/v1/verifications -H "Authorization: Bearer $TWOFA_KEY" \ -H "Content-Type: application/json" -d "{\"token\": \"$TOKEN\"}" ``` The token is an ES256 JWT, single-use, valid 10 minutes by default. Never trust it in the browser. Offline check (signature alone is not enough: test tokens are signed with a separate test key and the JWKS lists both): JWKS at https://api.2fa.mn/.well-known/jwks.json, verify the ES256 signature, `iss` exactly `https://api.2fa.mn` (test tokens: `https://api.2fa.mn/test`), `aud` exactly your client id (test tokens: `