Quick guide for implementing SIM-based phone number verification (no SMS needed).
- What is TS43?
- Quick Setup
- How to Use
- Configuration
- Frontend Integration
- Troubleshooting
- Technical Details
TS43 enables SIM-based phone verification without SMS codes:
✅ No SMS - Uses SIM card directly
✅ Seamless - User just confirms on their device
✅ Secure - Device-backed credentials
✅ Fast - No waiting for SMS delivery
Perfect for: Login flows, phone verification, KYC processes
In config/default.json:
{
"auth_servers": [
{
"id": "stage",
"url": "https://api.stage.ipification.com/auth"
}
]
}Add SIM flows to your clients:
{
"clients": [
{
"user_flow": "pvn_sim",
"title": "SIM Verification",
"scope": "openid ip:phone_verify"
}
]
}The app handles TS43 flow automatically. Users will see "SIM" buttons in the UI.
- User clicks "SIM" button
- Browser requests credential from device
- User confirms on device
- App shows verified phone number
No SMS, no typing!
// Frontend automatically calls:
1. POST /ts43/auth → get digital_request
2. navigator.credentials.get(digital_request) → get vp_token
3. POST /ts43/token → get user info
// You don't need to implement these - they're already in login.jsSee CONFIG_DEFAULT_JSON_SAMPLE.md for full config guide.
Minimum required:
auth_servers: At least one serverclient_idandclient_secret: Your OAuth credentialsrealm: Usually"ipification"
button#pvn_sim SIM VerificationThe app already handles TS43 flow. The relevant code is in:
public/js/login.js- Seestart_pvn_sim()andstart_login_sim()functions
You don't need to write frontend code - it's already implemented!
Cause: The server_id doesn't exist in your auth_servers config.
Solution: Check config/default.json and ensure server_id matches configured servers.
Cause: auth_servers array is empty or missing.
Solution: Add at least one server. See Quick Setup.
Cause: Client not configured or device/browser compatibility.
Solution:
- Check
clientsarray includespvn_simorlogin_sim - Test on Android with Chrome/Edge (best compatibility)
- Ensure mobile data is enabled
Solution:
- Verify OAuth credentials are correct
- Check auth server is accessible
- Confirm carrier supports TS43
- Try with different SIM card
IMPORTANT: The client app calls only /ts43/auth and /ts43/token. All calls to IPification Service are done by the backend.
- Client -> Backend:
POST /ts43/auth - Backend -> IPification:
ciba/auth - Backend -> IPification:
ts43/dcql - Backend -> Client:
auth_req_id+digital_request - Client -> Credential Manager: request credential
- Credential Manager -> Client:
vp_token - Client -> Backend:
POST /ts43/token - Backend -> IPification:
ts43/callback - Backend -> IPification:
openid-connect/token - Backend -> IPification:
openid-connect/userinfo - Backend -> Client: user info response
client_idandclient_secret(configured inconfig/default.json)realm(default:ipification)auth_serversconfiguration with at least one server
- Frontend calls
POST /ts43/authon your backend withclient_id,server_id(optional), and optionallogin_hint,carrier_hint,scope,operation - Backend resolves auth server URL from
server_id(defaults to first server if not provided) - Backend calls IPification CIBA auth:
POST /ext/ciba/auth→ getsauth_req_id - Backend calls IPification DCQL:
POST /ext/bc/ts43/dcqlwithoperation+nonce→ getsdcqlResponse.data - Backend returns to frontend:
auth_req_id,nonce, anddigital_request(built fromdcqlResponse.data) - Frontend calls Credential Manager with
digital_request→ receivesvp_token - Frontend calls
POST /ts43/tokenwithvp_token,auth_req_id,client_id,nonce,server_id(optional) - Backend calls IPification callback:
POST /ext/bc/ts43/callback - Backend exchanges token:
POST /openid-connect/token→ getsaccess_token - Backend fetches user info:
GET /openid-connect/userinfo - Backend returns user info JSON to frontend
Goal: Start the CIBA flow and receive a digital_request for the Credential Manager.
Client request body
{
"client_id": "your_client_id",
"server_id": "stage",
"login_hint": "+1234567890",
"carrier_hint": "51010",
"scope": "openid ip:phone_verify",
"operation": "VerifyPhoneNumber"
}Backend response body
{
"auth_req_id": "<auth_req_id>",
"nonce": "<uuid>",
"digital_request": {
"protocol": "openid4vp-v1-unsigned",
"data": {
"response_type": "vp_token",
"response_mode": "dc_api",
"nonce": "<uuid>",
"dcql_query": {
"credentials": [<dcqlResponse.data>]
}
}
}
}How the response is used
auth_req_id: used later in/ts43/tokento exchange for an access tokennonce: pass back to/ts43/tokenfor session correlationdigital_request: sent to the Credential Manager to obtainvp_token
How digital_request is built
After the backend calls DCQL (/ext/bc/ts43/dcql), it embeds dcqlResponse.data into the DC API request:
{
"protocol": "openid4vp-v1-unsigned",
"data": {
"response_type": "vp_token",
"response_mode": "dc_api",
"nonce": "<ts43_nonce>",
"dcql_query": {
"credentials": ["<dcqlResponse.data>"]
}
}
}Goal: Use digital_request to retrieve vp_token in the browser.
Client request (browser)
const { auth_req_id, digital_request, nonce } = body;
const credentialResponse = await navigator.credentials.get({
digital: {
requests: [digital_request],
},
});Extract vp_token from the response
const credentialData = credentialResponse.token || credentialResponse.data;
const { vp_token } = credentialData || {};
const dataToken = {
vp_token: vp_token["ipification.com"][0],
auth_req_id,
client_id: data.client_id,
nonce,
};Goal: Send vp_token + auth_req_id to complete CIBA and fetch user info.
Client request body
{
"vp_token": "<vp_token>",
"auth_req_id": "<auth_req_id>",
"client_id": "your_client_id",
"nonce": "<nonce_from_step_1>"
}Important: The nonce value must be the same nonce returned from /ts43/auth in Step 1. This is used for session correlation and security validation.
Backend response body
{
"sub": "<subject>",
"phone_number": "+1234567890",
"phone_number_verified": true
}How the response is used
- The client uses the user info JSON for login/session creation
- The backend also stores the response in the session for
/user/info
POST /ts43/auth
This is the first client call. Backend initiates CIBA and DCQL with IPification Service.
Content-Type: application/json
{
"client_id": "your_client_id",
"server_id": "stage",
"login_hint": "+1234567890",
"carrier_hint": "carrierX",
"scope": "openid ip:phone_verify",
"operation": "VerifyPhoneNumber"
}Request Body Fields:
| Field | Type | Required | Description |
|---|---|---|---|
client_id |
string | Yes | Your OAuth2 client ID |
server_id |
string | No | Auth server ID (e.g., "stage", "live"). Defaults to first server |
login_hint |
string | No | Phone number in E.164 format (e.g., +1234567890) |
carrier_hint |
number | No | Carrier MCC+MNC code (e.g., 51010 for Viettel Vietnam) |
operation |
string | No | Operation type: "VerifyPhoneNumber" or "GetPhoneNumber" |
scope |
string | No | OAuth scopes (default: uses client's scope from configuration) |
| Parameter | Type | Required | Description |
|---|---|---|---|
client_id |
string | Yes | OAuth client id |
login_hint |
string | No | E.164 phone for VerifyPhoneNumber |
carrier_hint |
string | No | Carrier hint (MCCMNC or vendor string) |
scope |
string | No | Example: openid ip:phone_verify |
operation |
string | No | VerifyPhoneNumber or GetPhoneNumber |
{
"auth_req_id": "<auth_req_id>",
"nonce": "<uuid>",
"digital_request": {
"protocol": "openid4vp-v1-unsigned",
"data": {
"response_type": "vp_token",
"response_mode": "dc_api",
"nonce": "<uuid>",
"dcql_query": {
"credentials": [<dcqlResponse.data>]
}
}
}
}| Field | Type | Description |
|---|---|---|
auth_req_id |
string | Auth request id from CIBA |
nonce |
string | UUID for the DCQL request |
digital_request |
object | OpenID4VP DC API request |
- URL:
${AUTH_SERVER_URL}/realms/ipification/protocol/openid-connect/ext/ciba/auth - Method:
POST - Content-Type:
application/x-www-form-urlencoded
Form body:
client_idclient_secretscopelogin_hint(optional)carrier_hint(optional)
Example:
curl -X POST "$AUTH_SERVER_URL/realms/ipification/protocol/openid-connect/ext/ciba/auth" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "client_id=$CLIENT_ID" \
--data-urlencode "client_secret=$CLIENT_SECRET" \
--data-urlencode "scope=openid" \
--data-urlencode "login_hint=+1234567890" \
--data-urlencode "carrier_hint=carrierX"- URL:
${AUTH_SERVER_URL}/realms/ipification/protocol/openid-connect/ext/bc/ts43/dcql - Method:
POST - Content-Type:
application/json - Authorization:
Bearer ${auth_req_id}
JSON body:
{
"operation": "GetPhoneNumber",
"nonce": "<uuid>"
}Example:
curl -X POST "$AUTH_SERVER_URL/realms/ipification/protocol/openid-connect/ext/bc/ts43/dcql" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $AUTH_REQ_ID" \
-d '{"operation":"GetPhoneNumber","nonce":"<uuid>"}'POST /ts43/token
Client sends the VP token and auth request id. Backend validates and returns user info.
Content-Type: application/json
{
"vp_token": "<vp_token>",
"auth_req_id": "<auth_req_id>",
"client_id": "your_client_id",
"nonce": "<nonce_from_step_1>",
"server_id": "stage"
}Request Body Fields:
| Field | Type | Required | Description |
|---|---|---|---|
vp_token |
string | Yes | VP token received from Credential Manager |
auth_req_id |
string | Yes | Auth request ID from /ts43/auth response |
client_id |
string | Yes | Your OAuth2 client ID |
nonce |
string | Yes | Nonce from /ts43/auth response |
server_id |
string | No | Auth server ID. Defaults to first server if not provided |
| Parameter | Type | Required | Description |
|---|---|---|---|
vp_token |
string | Yes | VP token from Credential Manager |
auth_req_id |
string | Yes | Auth request id from /ts43/auth |
client_id |
string | Yes | OAuth client id |
nonce |
string | Yes | Nonce returned from /ts43/auth |
{
"sub": "<subject>",
"phone_number": "+1234567890",
"phone_number_verified": true
}| Field | Type | Description |
|---|---|---|
sub |
string | Subject identifier |
phone_number |
string | Verified phone number |
phone_number_verified |
boolean | Verification result |
{
"error": "<message>",
"status": 500,
"data": {}
}data is included when the upstream service returns a body.
- URL:
${AUTH_SERVER_URL}/realms/ipification/protocol/openid-connect/ext/bc/ts43/callback - Method:
POST - Content-Type:
application/json - Authorization:
Bearer ${auth_req_id}
JSON body:
{
"vp_token": "<vp_token>"
}Expected response (typical):
{
"status": "ok"
}Some deployments may return an empty body or vendor-specific payload. A 2xx response is sufficient.
- URL:
${AUTH_SERVER_URL}/realms/ipification/protocol/openid-connect/token - Method:
POST - Content-Type:
application/x-www-form-urlencoded
Form body:
client_idclient_secretgrant_type=urn:openid:params:grant-type:cibaauth_req_id
Success response (typical):
{
"access_token": "<access_token>",
"expires_in": 300,
"refresh_expires_in": 0,
"token_type": "Bearer",
"scope": "openid"
}Error response (typical):
{
"error": "authorization_pending",
"error_description": "The authorization request is still pending."
}- URL:
${AUTH_SERVER_URL}/realms/ipification/protocol/openid-connect/userinfo - Method:
GET - Authorization:
Bearer ${access_token}
Success response (typical):
{
"sub": "<subject>",
"phone_number": "+1234567890",
"phone_number_verified": true
}Cause: The server_id provided doesn't exist in auth_servers configuration.
Solution: Check config/default.json and ensure the server_id matches one of the configured server IDs (e.g., stage, live).
Cause: The server_id field is missing from the request body (only applies if backend has strict validation).
Solution: Include server_id in your request body or configure the backend to use the default server.
Cause: The auth_servers array in configuration is empty or undefined.
Solution: Add at least one auth server to config/default.json. See Prerequisites for proper configuration.
- API Documentation: Complete API endpoint reference
- Architecture Documentation: System design and architecture
- Sample Config Reference: Configuration guide
- Quick Start Guide: Getting started guide