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_...orAuthorization: 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.+976and976prefixes 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
| Field | Type | Required | Description |
|---|---|---|---|
phone | string | no | If set, only an SMS from exactly this number verifies the session |
metadata | object | no | Any JSON, returned on the session and in webhooks |
{ "phone": "99112233", "metadata": { "user_id": "u_42" } }{
"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.
{
"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"
}status | Meaning |
|---|---|
pending | Waiting for the SMS |
verified | A matching SMS arrived |
expired | 5 minutes passed without a match |
Fixed text (Rule)
Create a rule
POST/v1/rules
| Field | Type | Required | Description |
|---|---|---|---|
prefix | string | yes | 2 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 |
name | string | no | A 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.
{ "prefix": "HIHI", "name": "Activate account" }{
"id": "rul_3m9q2w",
"mode": "test",
"prefix": "HIHI",
"name": "Activate account",
"created_at": "2026-12-01T10:00:00Z"
}List rules
GET/v1/rules
{
"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
{ "url": "https://example.com/webhooks/2fa" }{ "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.
{ "url": "https://example.com/webhooks/2fa", "secret": "whsec_..." }List deliveries
GET/v1/webhook/deliveries
{
"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.
{ "token": "eyJhbGciOiJFUzI1NiIs..." }{ "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.
{ "from": "99112233", "text": "482913" }{ "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.
{
"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?