3D Secure

Control and customize 3DS2 authentication flows — integration modes, standalone auth, browser data, result codes, and testing.

View as MarkdownInstall skills

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.

StatusMeaningLiability shift?
authenticatedCustomer completed a 3DS challenge or the flow resolved frictionlessly with a fully authenticated result.Yes — issuer bears liability.
attemptedIssuer participated in 3DS but did not fully authenticate; an attempt proof (ECI 06 / 01) was returned.Partial — varies by network and region.
not_authenticatedAuthentication failed. Customer failed or abandoned the challenge, or the issuer rejected it.No — merchant bears liability.
informationalData-only / passive flow completed. Issuer received signals but no authentication was performed.No — merchant bears liability.
rejectedThe 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 number3DS outcomeauthentication.status
4242 4242 4242 4242Frictionless — no challengeauthenticated
4000 0000 0000 3220Challenge requiredauthenticated (after approval) or not_authenticated (after failure)
4000 0000 0000 9995Data-only / informationalinformational

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

Was this page helpful?
Edit on GitHub

Last updated on

On this page