Number Verification — operator token flow

Number Verification adds SIM-based authentication using a GSMA TS.43 operator token. Unlike the network-based flow in Number Verification, this path does not require the device to be on a cellular data connection at the time of verification. Wi-Fi and other connections are fine, as long as the device can obtain an operator token from the SIM.

Number Verification is served at the /number-verification/verify endpoint.

This page covers the verify number use case: confirm that the phone number the user entered matches the number on the device. The flow uses OpenID4VP on your backend, the Android Credential Manager on the device, and the Number Verification verify API with the credential field.

When to use this flowheader link

Operator token flow (this page)Network-based flow
ConnectionWi-Fi, cellular, or otherCellular data on the device (not Wi-Fi or hotspot)
User stepsOS consent dialog via Credential ManagerOpen authorization URL on the device
PlatformAndroid with up-to-date Google Play ServicesAny browser or app that can open the auth URL
Best forNative Android apps where users may be off the home networkWeb or simpler integrations on cellular

If the device cannot produce an operator token (unsupported OS, missing Play Services, user declined consent), fall back to the network-based flow.

Overviewheader link

Your backend drives the flow. The mobile app is a pass-through for the Credential Manager step; it never sees decryption keys or the operator token in cleartext.

Number Verify call flow

  1. The app asks its backend to start verification for a phone number.
  2. The backend builds an incomplete OpenID4VP request and sends it to Network as Code. NaC signs the request and returns a completed DCQL payload.
  3. The app passes that payload to the Android Credential Manager. The user consents in an OS dialog.
  4. The app forwards the encrypted SD-JWT response to its backend.
  5. The backend calls Number Verification /verify with the phone number and the SD-JWT in the credential field. Network as Code decrypts the credential, exchanges the operator token with the operator, and returns the result.

Certificate management, aggregator registration, and operator-token exchange happen inside Network as Code. Your integration only touches the steps above.

Prerequisitesheader link

  • An active Network as Code subscription and API key.
  • An Android app targeting devices with current Google Play Services.
  • A backend able to make server-to-server HTTPS calls to Network as Code (the OpenID4VP and verify steps are never called directly from the mobile app).
  • A SIM from an operator that supports the TS.43 operator token flow in the target market. If the operator does not support it, use Number Verification instead.

Step 1 — Build an incomplete OpenID4VP requestheader link

On your backend, construct an OpenID4VP request object that describes the credential you need. Leave credential_authorization_jwt out of the DCQL credential meta object — Network as Code fills that in.

Generate a fresh, cryptographically random nonce for each verification attempt. Store the nonce server-side; you will need it to correlate the Credential Manager response later.

Example incomplete request:

{
  "response_type": "vp_token",
  "response_mode": "dc_api.jwt",
  "nonce": "1137c18a-1d52-4952-90b5-81f6a36f63e4",
  "state": "your-server-side-session-id",
  "dcql_query": {
    "credentials": [
      {
        "id": "aggregator-nac",
        "format": "dc-authorization+sd-jwt",
        "meta": {
          "vct_values": [
            "number-verification/verify/ts43"
          ]
        },
        "claims": [
          {
            "path": ["subscription_hint"],
            "values": [1]
          },
          {
            "path": ["phone_number_hint"],
            "values": ["+358401234567"]
          }
        ]
      }
    ]
  }
}
FieldDescription
nonceSingle-use random value for this verification attempt.
stateOptional. Echoed back by Network as Code; use it to correlate this verification attempt with a server-side session.
vct_valuesMust be number-verification/verify/ts43. The Network as Code operator token flow currently supports the verify use case only.
phone_number_hintOptional. The E.164 number the user entered, to guide SIM selection in multi-SIM setups.
subscription_hintOptional. Numeric SIM slot index (for example 1 for the first SIM) in multi-SIM devices.

Treat the completed DCQL JSON you receive from Network as Code as an opaque blob when handing it to the client. Do not modify it after NaC returns it.

Step 2 — Complete the request with Network as Codeheader link

Send the incomplete OpenID4VP object to Network as Code. NaC adds the signed credential_authorization_jwt (including encryption keys and consent text for the OS dialog) and returns the completed request.

curl --request POST \
  --url 'https://network-as-code.p-eu.rapidapi.com/oauth2/auth/openid4vp' \
  --header 'Content-Type: application/json' \
  --header 'X-RapidAPI-Host: network-as-code.nokia.rapidapi.com' \
  --header 'X-RapidAPI-Key: YOUR-API-KEY' \
  --data '{
    "response_type": "vp_token",
    "response_mode": "dc_api.jwt",
    "nonce": "1137c18a-1d52-4952-90b5-81f6a36f63e4",
    "state": "your-server-side-session-id",
    "dcql_query": {
      "credentials": [
        {
          "id": "aggregator-nac",
          "format": "dc-authorization+sd-jwt",
          "meta": {
            "vct_values": ["number-verification/verify/ts43"]
          },
          "claims": [
            {
              "path": ["subscription_hint"],
              "values": [1]
            },
            {
              "path": ["phone_number_hint"],
              "values": ["+358401234567"]
            }
          ]
        }
      ]
    }
  }'

This is the same body constructed in Step 1, sent as-is to Network as Code.

A successful response is the same OpenID4VP structure with credential_authorization_jwt populated inside dcql_query.credentials[].meta. Return this JSON to your mobile app.

Step 3 — Obtain the credential on the deviceheader link

Pass the completed OpenID4VP JSON from your backend to the Android app. The app invokes the Android Credential Manager to present the OS consent dialog and retrieve an encrypted verifiable presentation (SD-JWT).

The SD-JWT is encrypted end-to-end between the device and Network as Code. Your app must not attempt to decrypt or parse it — forward the response to your backend as-is.

For platform integration details, required dependencies, and request formatting for Credential Manager, see the upstream guide: Android phone number verification with digital credentials.

At a high level, the Credential Manager response has this shape:

{
  "protocol": "openid4vp-unsigned",
  "data": {
    "vp_token": {
      "aggregator-nac": ["eyJ...~eyJ..."]
    }
  }
}

Extract the SD-JWT string from data.vp_token (the key matches the id you used in the DCQL credential). Send that string to your backend together with the nonce from step 2.

Step 4 — Verify the phone numberheader link

Call the Number Verification verify endpoint with:

  • The phone number to check, as either phoneNumber or hashedPhoneNumber.
  • The SD-JWT from the Credential Manager in the required credential field.
  • Optionally, an x-correlator header for request tracing.

Authenticate the request with your Network as Code API key. Number Verification requires a 3-legged authorization with the number-verification:verify scope. Network as Code performs that authorization on your behalf by exchanging the TS.43 operator token supplied in credential with the operator, so your backend does not need to run a separate authorization code flow for this path.

FieldTypeRequiredDescription
phoneNumberstringone of*E.164 phone number to verify, prefixed with +.
hashedPhoneNumberstringone of*SHA-256 hash (hex-encoded) of the E.164 phone number, including the + prefix.
credentialstringYesSD-JWT from the Android Credential Manager containing the TS.43 operator token.

* Exactly one of phoneNumber or hashedPhoneNumber must be present. Sending both, or neither, returns a 400 INVALID_ARGUMENT error.

curl --request POST \
  --url 'https://network-as-code.p-eu.rapidapi.com/passthrough/camara/number-verification/number-verification/verify' \
  --header 'Content-Type: application/json' \
  --header 'X-RapidAPI-Host: network-as-code.nokia.rapidapi.com' \
  --header 'X-RapidAPI-Key: YOUR-API-KEY' \
  --header 'x-correlator: b4333c46-49c0-4f62-80d7-f0ef930f1c46' \
  --data '{
    "phoneNumber": "+358401234567",
    "credential": "eyJ...~eyJ..."
  }'

Alternatively, verify with a hashed phone number:

curl --request POST \
  --url 'https://network-as-code.p-eu.rapidapi.com/passthrough/camara/number-verification/number-verification/verify' \
  --header 'Content-Type: application/json' \
  --header 'X-RapidAPI-Host: network-as-code.nokia.rapidapi.com' \
  --header 'X-RapidAPI-Key: YOUR-API-KEY' \
  --data '{
    "hashedPhoneNumber": "a32f67ab4e4312618b09cd23ed8ce41b13e095fe52b73b2e8da8ef49830e50db",
    "credential": "eyJ...~eyJ..."
  }'

Verify responseheader link

A successful response:

{
  "devicePhoneNumberVerified": true
}
FieldTypeDescription
devicePhoneNumberVerifiedbooleantrue if the supplied number matches the SIM on the device; false otherwise.

Error responsesheader link

HTTP statusCodeMeaning
400INVALID_ARGUMENTMalformed request, for example both phoneNumber and hashedPhoneNumber supplied, or an invalid E.164 value.
401UNAUTHENTICATEDMissing, invalid, or expired credentials.
403PERMISSION_DENIEDInsufficient permission or scope for Number Verification.
403NUMBER_VERIFICATION.USER_NOT_AUTHENTICATED_BY_MOBILE_NETWORKThe user was not authenticated via the mobile network.
404NOT_FOUNDThe requested resource does not exist.
422UNPROCESSABLE_CONTENTThe request was well-formed but could not be processed, for example an invalid or undecryptable credential.
5xxINTERNAL, NOT_AVAILABLE, TIMEOUTServer-side or upstream operator error. Retry with backoff or fall back to Number Verification.

This table lists the most common errors. POST /oauth2/auth/openid4vp (step 2) returns 422 with a detail array of validation errors if the incomplete OpenID4VP request is malformed.

See Number Verification API HTTP responses for full response shapes.

Error handlingheader link

SituationWhat to do
User declines the OS consent dialogTreat as verification cancelled. Offer Number Verification if appropriate.
nonce reused or expiredStart a new flow from step 1 with a fresh nonce and a new OpenID4VP request.
Invalid or undecryptable credentialNetwork as Code returns an error. Do not retry with the same SD-JWT; restart from step 1.
Device lacks Credential Manager supportFall back to Number Verification.

Security notesheader link

  • Never embed signing keys, decryption keys, or aggregator certificates in the mobile app. Only your backend talks to POST /oauth2/auth/openid4vp.
  • The SD-JWT in credential is encrypted for Network as Code. The app is a transport layer only.
  • Generate a new nonce and OpenID4VP request for every verification attempt. Do not cache or reuse completed DCQL payloads across sessions.
  • Bind the verification session on your backend (for example with a server-side session ID) so a captured SD-JWT cannot be replayed against a different user context.

Last updated September 18, 2026

Number Verification API HTTP responses

This section provides example responses returned when using the Number Verification API.

200

When you get a successful response with the verified number data.

  • Verify (POST .../verify): returns devicePhoneNumberVerified.
  • Phone number share (GET .../device-phone-number): returns devicePhoneNumber.
{ "devicePhoneNumberVerified": true }
{ "type": "object", "description": "Number verification result", "required": ["devicePhoneNumberVerified"], "properties": { "devicePhoneNumberVerified": { "type": "boolean", "description": "Number verification. True, if it matches" } } }
{ "devicePhoneNumber": "+346661113334" }
{ "type": "object", "description": "Number verification share result", "required": ["devicePhoneNumber"], "properties": { "devicePhoneNumber": { "type": "string", "description": "The device phone number associated to the access token in E.164 format (starting with country code). Optionally prefixed with '+'." } } }

400

Invalid argument in the request:

{ "status": 400, "code": "INVALID_ARGUMENT", "message": "Client specified an invalid argument, request body or query param" }
{ "type": "object", "required": ["status", "code", "message"], "properties": { "status": { "type": "integer", "description": "HTTP response status code" }, "code": { "type": "string", "description": "Code given to this error" }, "message": { "type": "string", "description": "Detailed error description" } } }

401

Authentication issue with the request:

{ "status": 401, "code": "UNAUTHENTICATED", "message": "Request not authenticated due to missing, invalid, or expired credentials" }
{ "type": "object", "required": ["status", "code", "message"], "properties": { "status": { "type": "integer", "description": "HTTP response status code" }, "code": { "type": "string", "description": "Code given to this error" }, "message": { "type": "string", "description": "Detailed error description" } } }

403

Insufficient permission, or the device could not be authenticated by the mobile network. Any of these bodies may be returned:

{ "status": 403, "code": "PERMISSION_DENIED", "message": "Client does not have sufficient permissions to perform this action" } { "status": 403, "code": "NUMBER_VERIFICATION.USER_NOT_AUTHENTICATED_BY_MOBILE_NETWORK", "message": "Client must authenticate via the mobile network to use this service" } { "status": 403, "code": "INVALID_TOKEN_CONTEXT", "message": "Phone number cannot be deducted from access token context" }
{ "type": "object", "required": ["status", "code", "message"], "properties": { "status": { "type": "integer", "description": "HTTP response status code" }, "code": { "type": "string", "description": "Code given to this error" }, "message": { "type": "string", "description": "Detailed error description" } } }

500

Internal server error. Contact the Network as Code support team if it persists:

{ "status": 500, "code": "INTERNAL", "message": "Server error" }
{ "type": "object", "required": ["status", "code", "message"], "properties": { "status": { "type": "integer", "description": "HTTP response status code" }, "code": { "type": "string", "description": "Code given to this error" }, "message": { "type": "string", "description": "Detailed error description" } } }

503

Service unavailable:

{ "status": 503, "code": "UNAVAILABLE", "message": "Service unavailable" }
{ "type": "object", "required": ["status", "code", "message"], "properties": { "status": { "type": "integer", "description": "HTTP response status code" }, "code": { "type": "string", "description": "Code given to this error" }, "message": { "type": "string", "description": "Detailed error description" } } }

504

Request timeout. If it happens repeatedly, consider reducing request complexity:

{ "status": 504, "code": "TIMEOUT", "message": "Request timeout exceeded. Try later." }
{ "type": "object", "required": ["status", "code", "message"], "properties": { "status": { "type": "integer", "description": "HTTP response status code" }, "code": { "type": "string", "description": "Code given to this error" }, "message": { "type": "string", "description": "Detailed error description" } } }

Back to Number Verification