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.

Example. No SMS is sent and nothing reaches a server.

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.code
body := 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.

server.js
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_ and sk_live_ API keys and add your real sites to the widget's list. Always redeem the token on your own server and check state.
  • 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 per id.

What's next

Was this page helpful?