One-time code (Session)

Create a 6-digit code that is valid for 5 minutes. One tap opens the SMS app, and the user sends the code to 150073.

A session is a one-time verification. When your server creates a session, 2fa.mn issues a 6-digit code. As soon as the user sends that code to 150073, the session is verified and your server receives a webhook.

Use it for sign-up, login, confirming a transaction or activating a service.

Example: no SMS is sent here.

Create a code

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"]
FieldTypeDescription
phonestring, optional8-digit Mongolian phone number. +976 and 976 prefixes are accepted. If set, only an SMS sent from this number verifies the session.
metadataobject, optionalAny JSON object. Returned unchanged in the webhook, so you can link the result to your user.

Response 201 Created:

{
  "id": "ses_8f2k1c9x",
  "mode": "test",
  "code": "482913",
  "short_code": "150073",
  "sms_uri": "sms:150073?body=482913",
  "qr_svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 33 33\" shape-rendering=\"crispEdges\">…</svg>",
  "status": "pending",
  "phone": "99112233",
  "metadata": { "user_id": "u_42" },
  "expires_at": "2026-12-01T10:05:00Z",
  "created_at": "2026-12-01T10:00:00Z"
}

The qr_svg field

qr_svg is a QR code of sms_uri as a complete SVG. It is returned only while the session is pending, and is absent once the session is verified or expired.

2fa.mn generates the SVG from fixed characters only, so it is safe to inline in your HTML page.

With or without phone

With phone. If the user already entered their number, for example in a sign-up form, pass it as phone. An SMS from any other number does not verify the session. The session stays pending and keeps waiting for an SMS from the right number.

Without phone. You can show the SMS button without asking for the number first. The first number to send the code verifies the session, and you get that number from the webhook's phone field. This makes sign-up shorter.

Tip

Pass phone whenever you already know the number, for example at sign-in or payment confirmation. A session bound to a number can't be completed from another number.

After verification, show the number back so the user can confirm it is theirs, for example: "Is your number 9911••33?"

Show it to the user

Show the SMS price first, then a button linked to sms_uri. On a phone, tapping the button opens the SMS app with 150073 and the code filled in. The user then taps send in the SMS app.

<p>One SMS: 150₮</p>
<a href="sms:150073?body=482913">Send SMS</a>

SMS button or QR code

  • On a phone: show an SMS button linked to sms_uri.
  • On a computer: show qr_svg. When the user scans it with their phone's camera, the SMS app opens with the number and code (this depends on the phone and camera app). The user sends the SMS themselves. Also show the code and 150073 as text so they can send it by hand.

Either way, show the SMS price before the button or the QR code. Once verified, hide the SMS button and the QR code.

<p>One SMS: 150₮</p>
<div class="qr">
  <!-- session.qr_svg -->
</div>
<p>Scan the QR code with your phone's camera.</p>

The SVG has no width or height attribute, so size it with CSS on the element that contains it. For example:

.qr svg {
  width: 200px;
  height: 200px;
}

Always show the price

Under the terms of service, show the price of one SMS, 150₮, before the SMS button. Hide the SMS button once the number is verified.

Get a ready-made component from Build your own UI.

Status

StatusMeaning
pendingThe code was created and is waiting for the SMS.
verifiedA matching SMS arrived and the number is verified.
expired5 minutes passed without a matching SMS.

A session is used only once. If another SMS arrives for a verified or expired session, its status does not change. If the user wants to try again, create a new session.

Check the status

The webhook is the main way to get the result. If you need to show the session status in the browser, call GET /v1/sessions/{id} through your own server.

curl https://api.2fa.mn/v1/sessions/ses_8f2k1c9x \
  -H "Authorization: Bearer sk_test_..."

A verified session also includes the verified_phone, verified_at and operator fields.

{
  "id": "ses_8f2k1c9x",
  "mode": "test",
  "code": "482913",
  "short_code": "150073",
  "sms_uri": "sms:150073?body=482913",
  "status": "verified",
  "phone": "99112233",
  "metadata": { "user_id": "u_42" },
  "expires_at": "2026-12-01T10:05:00Z",
  "created_at": "2026-12-01T10:00:00Z",
  "verified_phone": "99112233",
  "verified_at": "2026-12-01T10:00:41Z",
  "operator": "unitel"
}

Never call the API from the browser

Your secret API key must stay on your server. The browser talks to your backend, and your backend talks to 2fa.mn.

Errors

CodeWhen
400 invalid_phonephone is not an 8-digit Mongolian phone number.
400 invalid_requestmetadata is not a JSON object, or is larger than 4 KB.
429 rate_limitedYou exceeded the session creation limit. See Limits.
403 client_suspendedYour account is suspended.

See Error codes for every error code.

Webhook

You receive verification.succeeded when the session is verified and session.expired when it expires. The data of session.expired contains only session_id.

{
  "id": "evt_2k9d7x",
  "type": "verification.succeeded",
  "mode": "test",
  "created_at": "2026-12-01T10:00:41Z",
  "data": {
    "session_id": "ses_8f2k1c9x",
    "phone": "99112233",
    "operator": "unitel",
    "verified_at": "2026-12-01T10:00:41Z",
    "metadata": { "user_id": "u_42" }
  }
}

See Webhooks & signatures for how to verify the webhook signature.

Good practice

  • Create the session only when the user is ready to verify, not on page load.
  • Keep metadata small and free of sensitive data. A user or order id is enough.
  • Treat the webhook as the source of truth. Polling is for UI updates only.
  • Phone numbers are always returned as 8 digits, without a country code.

What's next

Was this page helpful?