Overview
Additional Information
Overview
Additional Information
Overview
Additional Information
Overview
Additional Information
Overview
Additional Information
Number Verification 2
Number Verification 2 is Vodafone’s next-generation authentication mechanism, designed to deliver a seamless, secure, and low-friction user experience.
It leverages operating system-level integration with the operator’s entitlement server and token-based API authentication to verify a user's mobile number, without requiring manual input or SMS delivery.
This API follows the CAMARA standard specification.
JWT Bearer Flow
To verify a mobile number using Number Verification 2, the implementing service must generate a JWT assertion that includes a TS.43-compliant temporary token. This JWT is used in a bearer token request to Vodafone's authentication server.
Flow Summary
The mobile device obtains a TS.43 token via SIM-based authentication.
The app sends this token to its backend.
The backend constructs an encoded and signed JWT with the TS.43 temporary token in the sub claim.
The JWT is sent as part of a bearer token request to Vodafone.
Vodafone validates the JWT and issues an opaque access token.
The access token is used to call the
/number-verification/v2endpoint to retrieve or verify the mobile number.
For more details on the use of JWT Bearer as a means of requesting an OAuth access token, see RFC 7523 JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants.
JWT Header Structure
The following claims should be present in the JWT header:
Claim | Description |
|---|---|
| the key ID is a hint indicating which key was used to secure the JWT. |
| the media type of this complete JWT. |
| the algorithm used to sign the JWT. |
Example of JWT header:
{
"kid": "dmp-prod-kid",
"typ": "JWT",
"alg": "RS256"
}
JWT Payload Structure
The JWT must be signed and include the following claims:
Claim | Description |
|---|---|
sub | Subject: must contain the operator token retrieved from the TS.43 process and start with the string |
aud | Audience: OAuth endpoint URL (incl. the path parameter). |
scope | Scope value for the API endpoint being called. Supported individual scopes are: "dpv:FraudPreventionAndDetection number-verification:device-phone-number:read" "dpv:FraudPreventionAndDetection number-verification:verify" For requesting both services use: "dpv:FraudPreventionAndDetection number-verification:device-phone-number:read number-verification:verify" |
iss | Issuer: identifies the entity issuing the JWT. The Vodafone provided client ID must be used in this claim. |
iat | Issued at: UNIX timestamp of when the toke was issued. |
jti | JWT ID: unique identifier for the token. |
exp | Expiration time: the exact moment the JWT becomes invalid |
Example JWT Payload
Example sandbox:
{
"sub": "operatortoken:sandbox_operator_token",
"aud": "https://api-sandbox.vf-dmp.engineering.vodafone.com/oauth2/v2/token/{plmnid}",
"scope": "dpv:FraudPreventionAndDetection number-verification:device-phone-number:read",
"iss": "JFmbgGigniCOKRPblruIvlMvIrBWSBj2807w20T62ROS5nmy",
"iat": 1504804131,
"exp": 1504807731,
"jti": "2a1991e4-eb3a-4eb5-9913-52e97b0c8907"
}
Example production:
{
"sub": "operatortoken:prod_operator_token",
"aud": "https://api.vf-dmp.engineering.vodafone.com/oauth2/v2/token/{plmnid}",
"scope": "dpv:FraudPreventionAndDetection number-verification:device-phone-number:read number-verification:verify",
"iss": "JFmbgGigniCOKRPblruIvlMvIrBWSBj2807w20T62ROS5nmy",
"iat": 1504804131,
"exp": 1504807731,
"jti": "2a1991e4-eb3a-4eb5-9913-52e97b0c8907"
}
Create JSON Request
Place the encoded and signed JWT into the assertion field within the JSON request body defined in the Number Verification 2.0 API specification.
Example JSON request body:
{
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": "eyJraWQiOiJkbXAtcHJvZC1raWQi..."
}
Channel Partner Integration
Integration Architecture Overview
diagram showing how the integration works
Key Integration Points
Aggregator/CP ↔ Google: Certificate registration (one-time setup, see below).
Aggregator Backend ↔ Mobile App: DCQL generation & SD-JWT decryption.
Aggregator Backend ↔ Vodafone: OAuth token exchange & phone number verify service API.
Partner Onboarding: Prerequisites & Setup
For access to the service, please contact the Vodafone Network API sales team to complete commercial onboarding.
To be technically onboarded to the service with Vodafone, you are required to provide the following information:
The IP address range for allowlisting. This is required as part of our secure connectivity.
Provide Vodafone with the required data for Google Registration (see certificate generation steps below):
SAN (Subject Alternative Name). This is a value that defines your Android app domain used in the TLS handshake with Google's infrastructure. For example:
aggregator.yourcompany.com.Encryption Fingerprint. This is used to encrypt the request and response from the developers application using your service. For example:
c8df5c3ac7d8f1abefa8d47ae2133eb7fe3c84b71e22709b3726549000e98c9b.
Share JWKS public endpoint. Typicaly
https://{your-domain}/.well-known/jwks.json. Used to verify the token's cryptographic signature.
Certificate Generation
Supported Algorithms
ES256 (recommended)
Elliptic Curve P-256 (prime256v1) with SHA-256.
Smaller keys (256-bit), faster signing.
Standard algorithm in Android Credential Manager documentation.
RS256 (alternative)
RSA 3072-bit with SHA-256.
Larger keys, broader theoretical compatibility.
Not documented in Android examples (verify carrier support before use).
Certificate Chain Structure
Root CA Certificate
├─ Common Name: aggregator.yourcompany.com
├─ Key Usage: keyCertSign, cRLSign
├─ Basic Constraints: CA:TRUE
├─ Validity: 10 years
└─ Purpose: Signs leaf certificatesManual Generation
Step 1: Generate Root CA
# Generate EC P-256 private key
openssl ecparam -name prime256v1 -genkey -noout -out ca.keyStep 2: Extract Fingerprint (Provide SAN, fingerprint to Vodafone for Google/CP registration)
# Example output:
# SAN: aggregator.yourcompany.com
# FINGERPRINT:c8df5c3ac7d8f1abefa8d47ae2133eb7fe3c84b71e22709b3726549000e98c9bStep 3: Generate Leaf Certificate
# Generate leaf private key
openssl ecparam -name prime256v1 -genkey -noout -out leaf.keyStep 4: Validate Certificate Chain
# Verify root is self-signed && leaf is signed by root
openssl verify -CAfile ca.cert ca.cert
openssl verify -CAfile ca.cert leaf.certSecurity Best Practices
Private Key Storage: Store
ca.keyandleaf.keyin Hardware Security Module (HSM) or AWS KMS/Azure Key Vault.Never Commit: add
*.keyto.gitignore.Access Control: Restrict key access to signing service only,
Rotation: Rotate leaf certificates annually, root CA every 5-10 years.
Backup: Maintain encrypted backups of root CA key in separate secure location.
Phone Number Verify: Integration Flow
Digital Credential Query Language (DCQL) Request Generation
What is DCQL?
The DCQL is a JSON-encoded query language defined as part of OpenID4VP and used to express which credentials a verifier would like to request from a holder.
This request originates from the app backend and the aggregator is responsible for returning the DCQL credential object within the OpenID4VP wrapper, which has a structure similar to the following:
{
"requests": [
{
"protocol": "openid4vp-v1-unsigned",
"data": {
"response_type": "vp_token",
"response_mode": "dc_api",
"nonce": "<UNIQUE_UUID>",
"dcql_query": {
"credentials": [
{
"id": "<YOUR_AGGREGATOR_NAME_AS_PER_YOUR_CERT_GENERATION>",
"format": "dc-authorization+sd-jwt",
"claims": [],
"meta": {
"vct_values": [
"number-verification/device-phone-number/ts43"
],
"credential_authorization_jwt": "<SIGNED_CREDENTIAL_AUTH_JWT>"
}
}
]
}
}
}
]
}Credential Authorisation JWT Structure - Deep Dive
The aggregator backend must generate an ephemeral Elliptic Curve (EC) key pair for response encryption, then build a signed KWT using the Leaf Certificate.
Header:
{
"typ": "oauth-authz-req+jwt",
"alg": "ES256", // might be RS256 for you!
"x5c": [
"<base64_leaf_cert>",
"<base64_root_cert>"
]
}Payload:
{
"iss": "<YOUR_AGGREGATOR_NAME_AS_PER_YOUR_CERT_GENERATION>",
"nonce": "<UNIQUE_UUID>",
"state": "optional-state-value",
"encrypted_response_enc_values_supported": ["A128GCM"],
"jwks": {
"keys": [{
"kty": "EC",
"crv": "P-256",
"x": "<ephemeral_public_key_x>",
"y": "<ephemeral_public_key_y>",
"alg": "ECDH-ES",
"use": "enc",
"kid": "1"
}]
},
"consent_data": "<base64_encoded_consent_json>" // see consent data config. below.
}Reference Implementation
Kotlin
fun createCredentialAuthorizationJWT(nonce: String, jwks: Map<String, Any>, leafKey: ECPrivateKey, leafCert: String, rootCert: String): String {
val header = JWSHeader.Builder(JWSAlgorithm.ES256)
.type(JOSEObjectType("oauth-authz-req+jwt"))
.x509CertChain(listOf(Base64.from(leafCert), Base64.from(rootCert)))
.build()
val claims = JWTClaimsSet.Builder()
.issuer(YOUR_AGGREGATOR_NAME_AS_PER_YOUR_CERT_GENERATION)
.claim("nonce", nonce)
.claim("jwks", jwks)
.claim("consent_data", createConsentData())
.build()
val signedJWT = SignedJWT(header, claims)
signedJWT.sign(ECDSASigner(leafKey))
return signedJWT.serialize()
}Python
def create_credential_authorization_jwt(nonce, jwks, leaf_private_key, leaf_cert, root_cert):
"""Generates the signed authorization structure entirely on local infrastructure."""
consent_data_b64 = create_consent_data()
payload = {
"iss":YOUR_AGGREGATOR_NAME_AS_PER_YOUR_CERT_GENERATION,
"nonce": nonce,
"state": "optional-state-value",
"encrypted_response_enc_values_supported": ["A128GCM"],
"jwks": jwks,
"consent_data": consent_data_b64,
}
headers = {
"typ": "oauth-authz-req+jwt",
"alg": "ES256",
"x5c": [leaf_cert, root_cert],
}
return jwt.encode(payload, leaf_private_key, algorithm="ES256", headers=headers)Consent Data Configuration
Aggregators must dynamically inject user-facing legal declaration text into the JWT payload. The mobile OS utilises this to compile the authorisation interface (device consent pop-up via the credential manager interaction).
Construct a JSON object containing the required policy strings, then base64 encode the entire object and embed it with the jwt payload. Example:
{
"consent_text": "Allow your carrier to share your phone number with the app. The app may use your phone number to manage your account and provide services subject to their ",
"policy_link": "https://policies.google.com/terms?hl=en-US",
"policy_text": "privacy policy."
}Invoke Android Credential Manager
On receipt of the DCQL Request, the app initiates the credential manager (CM) interaction.
User receives the consent screen pop-up to agree and continue.
On acceptance, the CM completes the SIM based silent-authentication with the carrier network via TS43.
Note: the SD-JWT (Selective response received from the CM can only be decrypted by the aggregator.
Token Exchange: SD-JWT Decryption Pattern
The app receives an encrypted SD-JWT in the format: issuer_jwt~key_binding_jwt.
Response:
{
"protocol": "openid4vp-v1-unsigned",
"data": {
"vp_token": {
"YOUR_AGGREGATOR_NAME_AS_PER_YOUR_CERT_GENERATION": [
"eyJhbGciOiJFUzI1NiIsInR5cCI....signature"
]
}
}
}Decrypted payload:
{
"claims": {
"android_carrier_hint": 28,
"app_info": "5f2391... ...402|com.google.android.gms",
"carrier_hint": "23415", // <--| extract these for carrier integration
"country_code": "gb",
"subscription_hint": 3,
"vct": "number-verification/device-phone-number/ts43"
},
"decrypted_payload": {
"temp_token": "4RcVH1ZoysULGM3BxTRyQzqzhetf5ALaUcu+Nf6WyvA="
},
"issuer": "com.google.android.gms",
"nonce_verified": "e3f7c4b2-a19c-4319-a9eb-6a7f6ad737ba",
"success": true,
"temp_token": "4RcVH1ZoysULGM3BxTRyQzqzhetf5ALaUcu+Nf6WyvA=" // <--| extract these for carrier integration
}Suggested Decryption Steps:
Parse & Split: Separate de SD-JWT string by the
~delimiter into theissuer_jwtandkey_binding_jwtcomponents.Verify Issuer Signature: Extract the X.509 certificate from the
issuer_jwtheader to verify its signature usingcom.nimbusds.jose.crypto.ECDSAVerifier, confirming the credential originated from a trusted source (Android Telephony)Verify Key Binding Signature: Parse the confirmation claim (
cnf.jwk) from the verified issuer payload and use this device public key to verify thekey_binding_jwtsignature, protecting against token injection or swapping.Validate Nonce: Assert that the
nonceclaim inside the verified key-binding payload matches the unque session tracking value from the original DCQL request to prevent replay attacks.Decrypt JWE Payload: Load the cahced session ephemeral private key to decrypt the
encrypted_credentialblock inside the key-binding payload viacom.nimbusds.jose.crypto.ECDHDecrypter.Extract Target Tokens: Parse the decrypted cleartext payload to extract the short-lived
temp_tokenand thecarrier_hintneeded to downstream token exchange routing.
The temp_token is a short-lived credential (5 minutes expiry), one time use required for Vodafone OAuth exchange.
Reference Implementation
Kotlin
fun decryptAndVerify(sdJwtString: String, expectedNonce: String, ephemeralPrivateKey: ECPrivateKey): Map<String, String> {
val parts = sdJwtString.split("~")
val issuerJwtStr = parts[0]
val keyBindingJwtStr = parts.last().ifEmpty { parts[parts.size - 2] }Python
def decrypt_and_verify_sd_jwt(sd_jwt_string, expected_nonce, ephemeral_private_key):
parts = sd_jwt_string.split("~")
issuer_jwt, key_binding_jwt = parts[0], parts[-1] or parts[-2]
unverified_header = jwt.get_unverified_header(issuer_jwt)
issuer_x5c = f"-----BEGIN CERTIFICATE-----\n{unverified_header['x5c'][0]}\n-----END CERTIFICATE-----"
issuer_pub_key = jwk.JWK.from_pem(issuer_x5c.encode()).export_to_pem()
issuer_payload = jwt.decode(issuer_jwt, issuer_pub_key, algorithms=["ES256"])Vodafone Service API Integration
Host: https://api.vf-dmp.engineering.vodafone.com
Vodafone OAuth: POST /token/{plmn_id} └─ Body: JWT assertion (signed RS256) └─ Response: access_token
Vodafone Resource-1: GET /number-verification/v2/device-phone-number └─ Header: Bearer access_token └─ Response: devicePhoneNumber (string) ~ pattern: ^+[1-9][0-9]{4,14}$
Vodafone Resource-2: POST /number-verification/v2/verify └─ Header: Bearer access_token, Body: phoneNumber └─ Response: devicePhoneNumberVerified (boolean)
What is PLMN_ID?
PLMN ID (Public Land Mobile Network) identifies the mobile network operator.
Format: MCCMNC (5-6 digits)
MCC: Mobile Country Code (3 digits)
MNC: Mobile Network Code (2-3 digits)
Where to Extract it?
From SD-JWT (Recommended)
See Token Exchange: SD-JWT Decryption Pattern for a more detailed view.
# After splitting SD-JWT: issuer~keybinding
kb_payload = jwt.decode(key_binding_jwt, options={"verify_signature": False})
plmn_id = kb_payload["carrier_hint"] # "23415"From Android TelephonyManager (Fallback)
val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManagerval plmn_id = telephonyManager.simOperator // Returns "23415"Where to Use It
REQUIRED: OAuth Token Endpoint
# PLMN_ID is a mandatory, and MUST be in URL pathoauth_url = f"https://api.vf-dmp.engineering.vodafone.com/oauth2/v2/token/{plmn_id}"Example URLs:
Vodafone UK:
.../oauth2/v2/token/23415Vodafone DE:
.../oauth2/v2/token/26202
OAuth Token Exchange
JWT Assertion Structure
Header:
{
"alg": "RS256",
"kid": "AGGREGATOR-KEY-ID" //Key ID as per your JWKS endpoint (https://{your-domain}/.well-known/jwks.json).
}Payload:
{
"iss": "YOUR_VODAFONE_CLIENT_ID",
"sub": "operatortoken:{extracted_temp_token_from_sd_jwt}",
"aud": "https://api.vf-dmp.engineering.vodafone.com/oauth2/v2/token/{plmn_id}",
"exp": 1736000000,
"iat": 1735999700,
"jti": "550e8400-e29b-41d4-a716-446655440000",
"scope": "dpv:FraudPreventionAndDetection number-verification:device-phone-number:read"
}Sample cURL Command
curl -X POST 'https://api.vf-dmp.engineering.vodafone.com/oauth2/v2/token/23415' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'x-correlator: ce2adc2b-781a-4396-8468-be40a8ccd9be' \
--data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer' \
--data-urlencode 'assertion=eyJraWQiOiJBR0dSRUdBVE9SLUtFWS1JRCIsImFsZyI6IlJTMjU2In0.eyJpc3MiOiJZT...{signature}' \
--data-urlencode 'resource=https://api.vf-dmp.engineering.vodafone.com/number-verification/v2/device-phone-number'Additional Resources
Android Credential Manager Docs: https://developer.android.com/identity/digital-credentials/phone-number-verification
CAMARA Number Verification API Spec: https://developer.vodafone.com/api-catalogue/number-verification-2-camara
OpenID4VP Specification: https://openid.net/specs/openid-4-verifiable-presentations-1_0.html
SD-JWT Specification: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-selective-disclosure-jwt
Questions or Issues?
Submit technical support tickets or contact your Vodafone integration team.