3D Secure
Control and customize 3DS2 authentication flows — integration modes, standalone auth, browser data, result codes, and testing.
3D Secure 2 (3DS2) is the network protocol that carries Strong Customer Authentication. When a payment requires SCA, VINR drives a 3DS2 exchange with the issuer that collects device data, optionally shows a challenge, and returns a cryptographic proof of authentication. This page covers the three integration modes and how to supply or read authentication data. For the regulatory context — which payments need SCA and which exemptions apply — see Strong Customer Authentication.
Integration modesAsk
VINR's hosted checkout owns the full 3DS flow. You create a payment, redirect the customer to checkoutUrl, and VINR returns them to your returnUrl when done. The challenge UI, ACS redirect, and result handling are all managed for you.
import { Vinr } from '@vinr/sdk';
const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });
const payment = await vinr.payments.create({
amount: 7900,
currency: 'EUR',
description: 'Order #8821',
returnUrl: 'https://yoursite.com/orders/8821/complete',
});
// Send the customer here. VINR runs 3DS and redirects back.
console.log(payment.checkoutUrl);This is the recommended mode for browser-based checkout. No additional SDK is required, and soft declines (issuer overrides an exemption and demands a challenge) are handled transparently.
Use redirect mode when your UI needs the ACS URL directly — for example, to embed the challenge in an iframe or to control the redirect yourself. Create the payment, then inspect authentication.redirectUrl before sending the customer.
const payment = await vinr.payments.create({
amount: 7900,
currency: 'EUR',
description: 'Order #8821',
returnUrl: 'https://yoursite.com/orders/8821/complete',
authentication: {
mode: 'redirect',
},
});
if (payment.authentication?.status === 'required') {
const acsUrl = payment.authentication.redirectUrl;
// Redirect the customer to acsUrl, or load it in an iframe.
}VINR posts the authentication result to your returnUrl with a paymentId query parameter. Retrieve the payment server-side to confirm the final status before fulfilling the order.
Data-only mode sends device and browser signals to the issuer for risk scoring without presenting a challenge to the customer. The issuer uses the data to decide whether to approve the payment frictionlessly. No customer interaction occurs.
const payment = await vinr.payments.create({
amount: 1500,
currency: 'EUR',
description: 'Top-up',
returnUrl: 'https://yoursite.com/wallet/complete',
authentication: {
mode: 'data_only',
browserInfo: {
userAgent: req.headers['user-agent'],
colorDepth: 24,
screenWidth: 1440,
screenHeight: 900,
timeZoneOffset: -120,
javaEnabled: false,
language: 'en-GB',
},
},
});Use data-only for low-value payments, trusted returning customers, or flows where a challenge would be disproportionate. The issuer may still decline if risk scoring fails; there is no frictionless guarantee. Liability shift does not apply to data-only results — see Reading the result.
Standalone authenticationAsk
Authenticate a card without immediately charging it. This is useful for verifying a card before saving it to a customer's wallet, or for pre-authenticating a marketplace seller's payout method. The returned authenticationId can be attached to a later payments.create call.
const auth = await vinr.authentication.create({
amount: 0,
currency: 'EUR',
paymentMethod: 'pm_4Rk9...',
returnUrl: 'https://yoursite.com/wallet/verify-complete',
deviceChannel: 'browser',
challengeIndicator: 'challenge_requested',
browserInfo: {
userAgent: req.headers['user-agent'],
colorDepth: 24,
screenWidth: 1440,
screenHeight: 900,
timeZoneOffset: -120,
javaEnabled: false,
language: 'en-GB',
},
});
// auth.authenticationId → "aut_7Kx2p..."
// auth.status → "pending" → "authenticated" | "not_authenticated"
// auth.redirectUrl → present if a challenge is required
// Later: attach to a payment with no re-authentication needed.
const payment = await vinr.payments.create({
amount: 9900,
currency: 'EUR',
customer: 'cust_8Qd2...',
paymentMethod: 'pm_4Rk9...',
authenticationId: auth.authenticationId,
confirm: true,
});Standalone authentication is intended for saving a payment method or pre-authorizing a marketplace participant. For subscription setup, pass setupFutureUsage: 'recurring' on the initial payment instead — that authenticates and stores the mandate in a single step.
Passing 3DS dataAsk
Supply these fields on authentication (inside payments.create or authentication.create) to improve the issuer's risk decision and maximize frictionless results.
Prop
Type
Reading the resultAsk
After authentication completes, inspect authentication.status on the payment or authentication object. The status determines who bears liability if the payment is later disputed as unauthorized.
| Status | Meaning | Liability shift? |
|---|---|---|
authenticated | Customer completed a 3DS challenge or the flow resolved frictionlessly with a fully authenticated result. | Yes — issuer bears liability. |
attempted | Issuer participated in 3DS but did not fully authenticate; an attempt proof (ECI 06 / 01) was returned. | Partial — varies by network and region. |
not_authenticated | Authentication failed. Customer failed or abandoned the challenge, or the issuer rejected it. | No — merchant bears liability. |
informational | Data-only / passive flow completed. Issuer received signals but no authentication was performed. | No — merchant bears liability. |
rejected | The 3DS request was rejected by the card scheme before reaching the issuer (e.g. invalid card data). | No — payment should be declined. |
Never capture or fulfil an order if authentication.status is not_authenticated or rejected. Doing so shifts fraud liability to you and increases dispute risk.
Verify the outcome from a webhook rather than relying on the browser redirect alone:
const event = vinr.webhooks.verify(rawBody, req.headers['x-vinr-signature']);
switch (event.type) {
case 'payment.completed':
await fulfillOrder(event.data.metadata.orderId);
break;
case 'payment.authentication_failed':
await notifyAuthFailed(event.data.id);
break;
}TestingAsk
The sandbox drives authentication outcomes from the card number. Use any future expiry, any 3-digit CVC, and any postal code.
| Card number | 3DS outcome | authentication.status |
|---|---|---|
4242 4242 4242 4242 | Frictionless — no challenge | authenticated |
4000 0000 0000 3220 | Challenge required | authenticated (after approval) or not_authenticated (after failure) |
4000 0000 0000 9995 | Data-only / informational | informational |
To simulate a specific ECI value, pass test.eciOverride in the authentication object. This field is ignored outside the sandbox.
const payment = await vinr.payments.create({
amount: 4900,
currency: 'EUR',
returnUrl: 'https://yoursite.com/complete',
authentication: {
test: {
eciOverride: '06',
},
},
});
// authentication.eci → "06" (attempted authentication)
// authentication.status → "attempted"For the challenge card (4000 0000 0000 3220), approve or fail the challenge from Dashboard → Sandbox → Authentication to control the final status without browser interaction.
Next stepsAsk
Strong Customer Authentication
Regulatory context, exemptions, and how VINR applies SCA.
Recurring payments
Off-session charges, mandates, and authentication on renewals.
Testing your integration
Sandbox cards, ECI simulation, and scripting challenge outcomes.
Last updated on