Quickstart
Run your first verification with a test key, without sending a real SMS: get your API keys, create a code, receive the result.
Pick one of two paths.
- Widget: add about 10 lines to your page. The widget itself shows the number field, the 150₮ price, the code, the SMS button and the QR code. No webhook code needed.
- Your own UI: you build the design and flow yourself. Your server creates a session, shows the price UI and receives the result by webhook.
The user clicks a button, enters their phone number and sends the code from that phone by SMS. Once they verify, your server gets the number with one call.
Sign in
Sign in to Example with your phone number.
Get your API keys and add your site
Sign in to the Dashboard, open Widget and create a publishable key starting with pk_test_. Add the address where the widget will run, for example http://localhost:3000. Under API keys, create an sk_test_ secret API key for your server.
export TWOFA_KEY="sk_test_..."Test keys never reach an operator and earn nothing.
Paste the snippet
<div id="twofa"></div>
<script src="https://verify.2fa.mn/embed/v1/embed.js"></script>
<script>
TwoFA.button('#twofa', { key: 'pk_test_...', state: '<random value kept in your session>' })
.on('verified', ({ token }) => fetch('/login', { method: 'POST', body: token }));
</script>Your server creates state fresh for each sign-in attempt and keeps it in the session. The widget itself shows the price first and hides the button once verified.
Simulate the user's SMS
Click the button to open the modal and enter a number, for example 99112233. In test mode, simulate the user's SMS with POST /v1/test/mo: the number you entered goes in from, the code shown in the modal goes in text.
curl -X POST https://api.2fa.mn/v1/test/mo \
-H "Authorization: Bearer $TWOFA_KEY" \
-H "Content-Type: application/json" \
-d '{ "from": "99112233", "text": "482913" }'The modal shows the verified state and closes, and the verified event delivers a token.
Redeem the token on your server and check state
curl https://api.2fa.mn/v1/verifications/$TOKEN \
-H "Authorization: Bearer $TWOFA_KEY"{
"session_id": "ses_8f3k2m",
"phone": "99112233",
"verified_at": "2026-12-01T10:02:14Z",
"mode": "test",
"state": "q9Xh2LwP0aT7bV4e"
}Sign the user in only if state matches the value in your session. A token works once. See Widget for Node.js, Python and PHP examples and the error codes.
Your server creates the session and shows the price UI in your own design. You receive the result by signed webhook.
Create a secret API key
Sign in to the Dashboard and, under API keys, create a test key starting with sk_test_.
export TWOFA_KEY="sk_test_..."Keep the secret API key on your server only. Never put it in a browser, a mobile app or git.
Register your webhook URL
2fa.mn sends the result to your server by webhook. Register your URL and store the secret in the response somewhere safe.
curl -X PUT https://api.2fa.mn/v1/webhook \
-H "Authorization: Bearer $TWOFA_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.mn/webhooks/2fa" }'{ "url": "https://example.mn/webhooks/2fa", "secret": "whsec_..." }Developing locally? Give your server a public address with a tunnel such as ngrok or cloudflared.
Create a code
When a user verifies their number, your server creates a session.
curl -X POST https://api.2fa.mn/v1/sessions \
-H "Authorization: Bearer $TWOFA_KEY" \
-H "Content-Type: application/json" \
-d '{ "phone": "99112233", "metadata": { "user_id": "u_42" } }'const res = await fetch('https://api.2fa.mn/v1/sessions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.TWOFA_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ phone: '99112233', metadata: { user_id: 'u_42' } }),
});
const session = await res.json(); // session.sms_uri, session.codebody := strings.NewReader(`{"phone": "99112233", "metadata": {"user_id": "u_42"}}`)
req, _ := http.NewRequest("POST", "https://api.2fa.mn/v1/sessions", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("TWOFA_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)<?php
$ch = curl_init('https://api.2fa.mn/v1/sessions');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('TWOFA_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['phone' => '99112233', 'metadata' => ['user_id' => 'u_42']]),
]);
$session = json_decode(curl_exec($ch), true);import os, requests
res = requests.post(
"https://api.2fa.mn/v1/sessions",
headers={"Authorization": f"Bearer {os.environ['TWOFA_KEY']}"},
json={"phone": "99112233", "metadata": {"user_id": "u_42"}},
)
session = res.json() # session["sms_uri"], session["code"]{
"id": "ses_8f3k2m",
"mode": "test",
"code": "482913",
"short_code": "150073",
"sms_uri": "sms:150073?body=482913",
"status": "pending",
"phone": "99112233",
"expires_at": "2026-12-01T10:05:00Z"
}With phone set, only an SMS from that number verifies this session.
Show the price and the SMS button
Show the price before the button, and link the button to sms_uri. The terms of service require this.
<p>Price: 150₮</p>
<a href="sms:150073?body=482913">Send SMS</a>Copy ready-made HTML, QR and React code from Build your own UI.
Simulate the user's SMS
curl -X POST https://api.2fa.mn/v1/test/mo \
-H "Authorization: Bearer $TWOFA_KEY" \
-H "Content-Type: application/json" \
-d '{ "from": "99112233", "text": "482913" }'Receive the webhook and check the signature
A verification.succeeded event arrives at your URL. Always check the X-2FA-Signature header before you act on it.
import express from "express";
import crypto from "node:crypto";
const app = express();
const SECRET = process.env.TWOFA_WEBHOOK_SECRET;
app.post("/webhooks/2fa", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-2FA-Signature") ?? "";
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return res.sendStatus(400);
const expected = crypto
.createHmac("sha256", SECRET)
.update(`${t}.${req.body}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 ?? "");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(400);
const event = JSON.parse(req.body);
if (event.type === "verification.succeeded") {
// event.data.phone is verified. Use event.id to skip duplicates.
}
res.sendStatus(200);
});
app.listen(3000);See Webhooks & signatures for other languages.
Going live
- Widget: create
pk_live_andsk_live_API keys and add your real sites to the widget's list. Always redeem the token on your own server and checkstate. - Your own UI: create an
sk_live_API key and register a separate live webhook URL. Show the price before the button and hide the button once verified. Process webhooks once perid.
What's next
Was this page helpful?