Wocha Docs

SMS MFA API

Account API endpoints for SMS MFA enrolment, verification, and step-up challenges

SMS MFA delivers one-time codes to a user's verified phone number. Enrolment follows the same POST → PUT pattern as TOTP. Step-up challenges during OAuth consent use internal endpoints called by the auth app.


Authentication

APITokenBase URL
Account APIUser access tokenhttps://TENANT.api.wocha.ai/v1/account
Internal APIService secret (x-internal-service-secret)https://TENANT.api.wocha.ai/v1/internal

Account API endpoints require a user OAuth access token. Internal step-up endpoints are called by the auth app only.


Prerequisites

  1. SMS enabled and mfa_enabled: true in tenant SMS settings
  2. User has a verified phone number on their identity (PUT /v1/account/phone)
  3. sms included in tenant MFA policy allowed_factors

SMS MFA enrolment flow

sequenceDiagram
    participant App
    participant API as Account API
    participant GW as SMS Gateway
    participant Kratos
    participant User
 
    App->>API: POST /mfa/sms
    API->>GW: sendOtp(verification)
    GW->>User: SMS with 6-digit code
    API-->>App: { status: pending, phone_number_masked }
    User->>App: Enters code
    App->>API: PUT /mfa/sms { code }
    API->>GW: verifyOtp(code)
    API->>Kratos: activate code credential (via: sms)
    API-->>App: { enabled: true, methods: ["sms"] }

Start SMS MFA enrolment

POST /v1/account/mfa/sms
Authorization: Bearer {user_access_token}

Response 201 Created:

{
  "data": {
    "status": "pending",
    "phone_number_masked": "+44 ••••• 3456"
  }
}

Sends an OTP to the user's verified phone number. Pending enrolment expires after 15 minutes.

Errors:

StatusCodeWhen
422validation_errorNo verified phone number on identity
409conflictSMS MFA already enrolled
503service_unavailableSMS MFA not enabled for tenant
429rate_limitedRate limit exceeded

Verify and activate SMS MFA

PUT /v1/account/mfa/sms
Authorization: Bearer {user_access_token}
Content-Type: application/json
 
{ "code": "123456" }

Response 200 OK:

{
  "data": {
    "enabled": true,
    "methods": ["sms"],
    "backup_codes_remaining": 0
  }
}

On success, the SMS code credential is activated in Kratos with via: sms and the pending enrolment is cleared.

Errors:

StatusCodeWhen
422validation_errorInvalid code, expired enrolment, or max attempts
404not_foundNo pending SMS MFA enrolment — call POST first
409conflictSMS MFA already enrolled

Remove SMS MFA

DELETE /v1/account/mfa/sms
Authorization: Bearer {user_access_token}

Response 200 OK:

{
  "data": {
    "enabled": false,
    "methods": [],
    "backup_codes_remaining": 0
  }
}

Removes the SMS code credential from the identity. The verified phone number trait is retained — remove it separately via DELETE /v1/account/phone if needed.


Step-up challenge flow

During OAuth consent, when SMS is the preferred MFA factor and the session is at AAL1, the auth app calls internal endpoints:

sequenceDiagram
    participant Auth as Auth app
    participant API as Internal API
    participant GW as SMS Gateway
    participant User
 
    Auth->>API: POST /internal/mfa/sms/challenge
    API->>GW: sendOtp(mfa_challenge)
    GW->>User: SMS with step-up code
    API-->>Auth: { phone_number_masked }
    Auth->>User: Redirect to /sms-verify
    User->>Auth: Enters code
    Auth->>API: POST /internal/mfa/sms/verify { code }
    API->>GW: verifyOtp(code)
    API-->>Auth: { verified: true }
    Auth->>Auth: Mark session AAL2, resume consent

The auth app marks the Kratos session as AAL2-equivalent in Valkey after successful verification, allowing consent to proceed without native Kratos code MFA.


SDK usage

React hooks

import { usePhone, useMfa } from "@wocha/react";
 
function SecuritySettings() {
  const { addPhone, verifyPhone } = usePhone();
  const { enrollSms, verifySms, removeSms, data: mfaStatus } = useMfa();
 
  const setupSmsMfa = async () => {
    // Ensure phone is verified first
    await addPhone("+447911123456");
    await verifyPhone("123456");
 
    // Enrol SMS MFA
    const { phone_number_masked } = await enrollSms();
    const status = await verifySms("654321");
    console.log(status.methods); // ["sms"]
  };
}

REST API

const accountApiUrl = "https://acme.api.wocha.ai/v1/account";
const token = await getAccessToken();
 
// Start enrolment
await fetch(`${accountApiUrl}/mfa/sms`, {
  method: "POST",
  headers: { Authorization: `Bearer ${token}` },
});
 
// Verify and activate
const res = await fetch(`${accountApiUrl}/mfa/sms`, {
  method: "PUT",
  headers: {
    Authorization: `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ code: "123456" }),
});
const { data } = await res.json();
// data.enabled === true, data.methods includes "sms"

cURL

# Start SMS MFA enrolment
curl -X POST "https://acme.api.wocha.ai/v1/account/mfa/sms" \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
 
# Verify code
curl -X PUT "https://acme.api.wocha.ai/v1/account/mfa/sms" \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{"code": "123456"}'
 
# Remove SMS MFA
curl -X DELETE "https://acme.api.wocha.ai/v1/account/mfa/sms" \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."

Factor prioritisation

When multiple MFA factors are enrolled, Wocha challenges the strongest factor by default:

WebAuthn > TOTP > SMS > Email

Override with preferred_factor: "sms" in the tenant MFA policy. SMS step-up uses the Wocha gateway verify page (/sms-verify) rather than native Kratos AAL2.


Security notes

SMS OTP is classified as a restricted authenticator under NIST SP 800-63B section 5.1.3.3. Wocha supports it for broad device compatibility but recommends TOTP or WebAuthn for high-security applications. See the SMS authentication guide for full security documentation.


On this page