API reference

Every request and response for sessions, rules, webhooks and test mode, in one place.

Basics

  • Base URL: https://api.2fa.mn
  • Authentication: Authorization: Bearer sk_test_... or Authorization: Bearer sk_live_.... The API key decides the mode. Test API keys never reach operators and earn nothing.
  • Format: JSON request and response bodies. Send Content-Type: application/json.
  • Phone numbers: 8-digit Mongolian numbers such as 99112233. +976 and 976 prefixes are accepted on input; responses always use 8 digits.
  • Timestamps: RFC 3339, UTC.
  • Errors: see Errors.

One-time codes (Session)

Create a session

POST/v1/sessions

FieldTypeRequiredDescription
phonestringnoIf set, only an SMS from exactly this number verifies the session
metadataobjectnoAny JSON, returned on the session and in webhooks
Request
{ "phone": "99112233", "metadata": { "user_id": "u_42" } }
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"
}

phone is null when not provided. Sessions are single-use and expire after 5 minutes.

Get a session

GET/v1/sessions/{id}

Returns the session object. Once status is verified, it also includes verified_phone, verified_at and operator.

200 OK
{
  "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"
}
statusMeaning
pendingWaiting for the SMS
verifiedA matching SMS arrived
expired5 minutes passed without a match

Fixed text (Rule)

Create a rule

POST/v1/rules

FieldTypeRequiredDescription
prefixstringyes2 to 20 characters, starts with a Latin letter, letters and digits, case-insensitive. A live prefix needs at least 4 characters. 2FA, LOGIN, STOP, HELP, TEST and INFO are reserved
namestringnoA label for your reference

Prefixes are shared by every client on 150073, so no live prefix may be a prefix of another client's. An overlapping live prefix returns 409 prefix_taken. A client can have up to 5 live rules; beyond that you get 409 rule_limit. A message matches on its first word: HIHI matches HIHI 123 but not HIHI123.

Request
{ "prefix": "HIHI", "name": "Activate account" }
201 Created
{
  "id": "rul_3m9q2w",
  "mode": "test",
  "prefix": "HIHI",
  "name": "Activate account",
  "created_at": "2026-12-01T10:00:00Z"
}

List rules

GET/v1/rules

200 OK
{
  "data": [
    { "id": "rul_3m9q2w", "mode": "test", "prefix": "HIHI", "name": "Activate account", "created_at": "2026-12-01T10:00:00Z" }
  ]
}

Delete a rule

DELETE/v1/rules/{id}

Returns 204 No Content.

Webhook

There is one webhook endpoint per mode.

Set the endpoint

PUT/v1/webhook

Request
{ "url": "https://example.com/webhooks/2fa" }
200 OK
{ "url": "https://example.com/webhooks/2fa", "secret": "whsec_..." }

Get the endpoint

GET/v1/webhook

Returns the endpoint configured for the API key's mode.

Rotate the secret

POST/v1/webhook/rotate-secret

Returns the endpoint with a new secret. Update your server with the new value.

200 OK
{ "url": "https://example.com/webhooks/2fa", "secret": "whsec_..." }

List deliveries

GET/v1/webhook/deliveries

200 OK
{
  "data": [
    {
      "event_id": "evt_9a8b7c",
      "type": "verification.succeeded",
      "status": "delivered",
      "attempts": 1,
      "last_status_code": 200,
      "created_at": "2026-12-01T10:00:42Z",
      "delivered_at": "2026-12-01T10:00:42Z"
    }
  ]
}

status is pending, delivered or failed.

Widget token

Redeem a token

POST/v1/verifications

Redeem the token from the widget once, on your server, with a secret API key. The token travels in the request body, so it stays out of URLs and logs.

Request
{ "token": "eyJhbGciOiJFUzI1NiIs..." }
200 OK
{ "session_id": "ses_8f3k2m", "phone": "99112233", "verified_at": "2026-12-01T10:02:14Z", "mode": "test", "state": "q9Xh2LwP0aT7bV4e" }

GET /v1/verifications/{token} still works, but prefer POST. Errors: 400 invalid_token, 404 not_found, 409 token_used, 410 token_expired.

Test

Simulate an SMS

POST/v1/test/mo

Test API keys only. Live API keys get 403 test_only. Limited to 30 requests per minute per client (429 rate_limited). Test webhooks are sent once, without retries.

Request
{ "from": "99112233", "text": "482913" }
202 Accepted
{ "id": "mo_4h6j8k" }

Webhook events

Sent as POST to your endpoint, signed with X-2FA-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>. See Webhooks & signatures.

Envelope
{
  "id": "evt_9a8b7c",
  "type": "verification.succeeded",
  "mode": "live",
  "created_at": "2026-12-01T10:00:42Z",
  "data": { }
}

verification.succeeded

{ "session_id": "ses_8f2k1c9x", "phone": "99112233", "operator": "unitel", "verified_at": "2026-12-01T10:00:41Z", "metadata": { "user_id": "u_42" } }

rule.matched

{ "rule_id": "rul_3m9q2w", "prefix": "HIHI", "phone": "99112233", "text": "HIHI 12345", "operator": "unitel", "received_at": "2026-12-01T10:00:01Z" }

session.expired

{ "session_id": "ses_8f2k1c9x" }

What's next

Was this page helpful?