Token Fields (HPF) with 3DS
When you capture the card with Token Fields (HPF), your server never receives the PAN, expiry or CVV. You only hold the paymentToken returned by the SDK. This page explains how to run the 3D Secure v2 flow in that case.
Short version
You use the same standalone 3DS v2 flow as for raw cards. The only difference is that, everywhere the 3DS guide asks for cardNumber / cardExpireMonth / cardExpireYear, you send the paymentToken instead. The gateway resolves the token to the underlying card internally before contacting the 3DS provider, so no card data leaves the iframe.
The flow
- Initialise —
POST /3ds/v2/initializationwith thepaymentToken(instead of card fields) plus the browser data. - Handle the response — frictionless (Flow A) or challenge required (Flow B), exactly as documented in Flows.
- Sale —
POST /salewith the samepaymentTokenplus the resulting 3DS values (eci,cavv,dsTransactionId,version). See Add 3DS fields in Sale API.
// 1. Initialise 3DS using the Token Fields token (no card data)
const init = await initialize({ paymentToken, /* browser + order fields */ });
if (init.responseCode !== '3DS_1000') return handleError(init);
// 2a. Flow A — frictionless: eci + cavv already present
if (init.threeDSProviderResponse.eci && init.threeDSProviderResponse.cavv)
return sale({ paymentToken, ...mapThreeDsVals(init) });
// 2b. Flow B — challenge required: hand control to the provider, then read the result
if (init.threeDSProviderResponse.status === 'ACS_REQUIRED')
redirectToChallengePage(init.threeDSProviderResponse.redirectUrl);
Initialisation request with a token
Send the Initialisation API request as usual, but omit the raw card fields and provide paymentToken:
{
"netvalveMidId": "{{netvalveMidId}}",
"amount": 43.10,
"currency": "USD",
"cardHolderName": "John Doe",
"paymentToken": "e9d74bfb-12d6-422c-a230-fdcc351afc52",
"merchantRedirectUrl": "https://example.com/redirect",
"customerIp": "203.0.113.1",
"customerEmail": "docs@netvalve.com",
"customerPhone": "+12025551234",
"userAgent": "{{USER_AGENT}}",
"browserHeader": "{{BROWSER_HEADER}}",
"browserJavaEnabled": true,
"browserLanguage": "en-US",
"browserColorDepth": 24,
"browserScreenHeight": 864,
"browserScreenWidth": 1536,
"browserTimeZone": 300
}
What changes versus the raw-card request
| Field | Raw card | Token Fields |
|---|---|---|
paymentToken | not used | Required (replaces the card fields) |
cardNumber | Required | Omit |
cardExpireMonth | Required | Omit |
cardExpireYear | Required | Omit |
cardHolderName | Required | Still required |
currency | Required | Required |
amount, netvalveMidId/midId/siteId, merchantRedirectUrl, browser fields, customerEmail/customerPhone (for Visa) | as documented | as documented |
cardHolderName is still validated on the initialisation request (you receive GTW_2021 if it is missing), even when paying with a paymentToken. Collect the cardholder name alongside the Token Fields form so you can supply it here.
The paymentToken must still be valid and unused at the time of the initialisation call. A token is only retired after a successful sale, so the same token is used for both the initialisation and the sale. Do not request a fresh token between the two calls.
Challenge flow (Flow B) and finalisation
If the initialisation response indicates a challenge is required, handle it exactly as in the standard flow described in Flows and ACS Challenge:
- Initialisation response:
responseCode = "3DS_1000",threeDSProviderResponse.status = "ACS_REQUIRED"(equivalentlychallengeRequired: true), with the challenge location inthreeDSProviderResponse.redirectUrl. - After the cardholder completes the challenge, they return to the
merchantRedirectUrlyou supplied, carrying atransID. - Call the 3DS Result API (
POST /3ds/resultwith{ netvalveMidId, transID }) to retrieveeci,cavv,threeDs2TransactionIdandthreeDsVersion. - Call
/salewith thepaymentTokenand the 3DS values mapped into the request.