---
description: >-
  Handle 3D Secure (3DS) authentication for card payments. Two integration
  options: URL redirect or JavaScript postMessage.
---

# Handling 3D Secure

3D Secure (3DS) adds a layer of cardholder authentication to reduce fraud. When 3DS is triggered, the cardholder may need to verify their identity with their issuing bank before the payment is authorized.

## 3DS Modes

| Mode | Cardholder Interaction | Description |
|---|---|---|
| **Data-only** | None | Transaction data is sent to the issuer for risk assessment; no cardholder action needed |
| **Challenge** | Required | Cardholder verifies via OTP, biometrics, or bank app |

> **Important:** In the US market, banks are not mandated to enforce step-up authentication. The issuing bank decides whether to issue a challenge, even if you request one during tokenization. 3DS in the AFT (Account Funding Transaction) market is for fraud control only — there is no liability shift.

## Enabling 3DS

3DS can be enabled in two ways:

1. **Automatic** (backend configuration) — All payments require 3DS. Configured by the Inyo team during onboarding.
2. **Manual** (per-transaction) — You control when to request 3DS by setting `threeDSData` during [card tokenization](../../../../tokenizing-cards.md).

## Integration Options

When the payment API returns `status: "CHALLENGE"`, the cardholder must complete authentication. You have **two ways** to handle the challenge result:

| Option | How it works | Best for |
|---|---|---|
| **URL Redirect** | Browser redirects to `redirectAcsUrl`; after authentication, the 3DS provider redirects back to your `successUrl` or `failUrl` | Full-page checkout flows, server-rendered apps |
| **PostMessage** | Open `redirectAcsUrl` in an iframe; the 3DS provider sends a `postMessage` event to the parent window with the result | Single-page apps (SPAs), embedded/modal checkout experiences |

---

## Option 1: URL Redirect

The traditional approach. The cardholder's browser navigates to the bank's 3DS page, and after authentication, is redirected back to your site.

### Tokenizer Configuration

```javascript
const tokenizer = new InyoTokenizer({
  targetId: '#payment-form',
  publicKey: 'YOUR_PUBLIC_KEY',
  threeDSData: {
    enable: true,
    successUrl: 'https://yoursite.com/3ds/success',
    failUrl: 'https://yoursite.com/3ds/fail'
  },
  successCallback: handleSuccess,
  errorCallback: handleError
});
```

### Flow

```
1. Frontend tokenizes card → receives token
2. Backend calls POST /v2/payment with token
3. API returns status: "CHALLENGE" + redirectAcsUrl
4. Frontend redirects browser to redirectAcsUrl
5. Cardholder completes bank authentication
6. Bank redirects to successUrl (approved) or failUrl (rejected)
7. Your server-side handler receives a POST with payment data
8. Backend confirms payment status via GET /payments/{id}
```

### Step 4 — Redirect to Challenge

```javascript
const response = await createPayment(paymentData);

if (response.data.status === 'CHALLENGE' && response.data.redirectAcsUrl) {
  // Full-page redirect to bank's 3DS page
  window.location.href = response.data.redirectAcsUrl;
}
```

### Step 7a — Success URL Handler

The 3DS provider sends a POST to your `successUrl` with payment data:

```json
{
  "paymentId": "dce568c6-98ec-456c-bb33-4a6809c4fff8",
  "externalPaymentId": "order-12345",
  "amount": "99.99",
  "approved": "true"
}
```

**Server-side handler example (Node.js / Express):**

```javascript
// POST /3ds/success
app.post('/3ds/success', async (req, res) => {
  const { paymentId, externalPaymentId } = req.body;

  // CRITICAL: Always verify via API — don't trust the redirect payload alone
  const payment = await fetch(
    `https://sandbox-gw.simpleps.com/payments/${externalPaymentId}`,
    { headers: { 'Authorization': `Bearer ${accessToken}` } }
  ).then(r => r.json());

  if (payment.status === 'AUTHORIZED' || payment.status === 'CAPTURED') {
    // Payment confirmed — show success page
    res.redirect(`/order/${externalPaymentId}/confirmed`);
  } else {
    // Unexpected state — show error
    res.redirect(`/order/${externalPaymentId}/error`);
  }
});
```

### Step 7b — Failure URL Handler

The 3DS provider sends a POST to your `failUrl`:

```json
{
  "paymentId": "dce568c6-98ec-456c-bb33-4a6809c4fff8",
  "externalId": "order-12345",
  "amount": 99.99,
  "approved": false,
  "status": "Rejected by ACS",
  "responseMsg": "Rejected by ACS",
  "responseCode": "99",
  "signatureVerification": "N",
  "acsChallengeRequired": true,
  "acsParStatus": "N"
}
```

```javascript
// POST /3ds/fail
app.post('/3ds/fail', (req, res) => {
  // Authentication failed — redirect user back to payment page
  res.redirect('/checkout?error=3ds_failed');
});
```

> **Note:** When authentication fails, the payment was never authorized. Do not attempt to capture or void.

---

## Option 2: JavaScript PostMessage

For single-page apps and embedded checkout experiences, you can open the 3DS challenge in an **iframe** and receive the result via the browser's `postMessage` API — no full-page redirect needed.

### PostMessage URL Whitelisting (Required)

The URL of the page that will receive the `postMessage` events must be registered with Inyo. This is a security measure to ensure that only your intended page can listen to 3DS authentication results.

- **Before going live**, provide Inyo with the exact origin URL (e.g., `https://checkout.yoursite.com`) of the page that listens for `postMessage` events.
- **Any change to the URL** must be communicated to Inyo so the whitelist can be updated.
- 3DS results will not be delivered via `postMessage` to non-whitelisted origins.

> This is in addition to the tokenizer domain whitelisting described in [Tokenizing Cards](../../../../tokenizing-cards.md). Both the tokenizer page URL and the PostMessage listener page URL must be registered.

### Tokenizer Configuration

Set `enablePostMessage: true` in `threeDSData`:

```javascript
const tokenizer = new InyoTokenizer({
  targetId: '#payment-form',
  publicKey: 'YOUR_PUBLIC_KEY',
  threeDSData: {
    enable: true,
    enablePostMessage: true
  },
  successCallback: handleSuccess,
  errorCallback: handleError
});
```

> **Key difference:** When using `enablePostMessage: true`, you do **not** need to provide `successUrl` or `failUrl`. The result is delivered via `postMessage` instead of a redirect.

### Flow

```
1. Frontend tokenizes card → receives token
2. Backend calls POST /v2/payment with token
3. API returns status: "CHALLENGE" + redirectAcsUrl
4. Frontend opens redirectAcsUrl in an iframe (or modal)
5. Cardholder completes bank authentication inside the iframe
6. 3DS provider sends a postMessage to the parent window
7. Frontend listens for the message and handles the result
8. Backend confirms payment status via GET /payments/{id}
```

### Step 4 — Open Challenge in Iframe

```html
<!-- Hidden iframe for 3DS challenge -->
<div id="threeds-container" style="display: none;">
  <iframe id="threeds-iframe" width="100%" height="500"
          sandbox="allow-forms allow-scripts allow-same-origin allow-popups">
  </iframe>
</div>
```

```javascript
const response = await createPayment(paymentData);

if (response.data.status === 'CHALLENGE' && response.data.redirectAcsUrl) {
  // Show the iframe container
  document.getElementById('threeds-container').style.display = 'block';
  // Load the 3DS challenge page
  document.getElementById('threeds-iframe').src = response.data.redirectAcsUrl;
}
```

### Step 7 — Listen for PostMessage

```javascript
window.addEventListener('message', async (event) => {
  // Validate the origin for security
  if (!event.origin.includes('simpleps.com')) return;

  const data = event.data;

  // Hide the iframe
  document.getElementById('threeds-container').style.display = 'none';

  if (data.approved === true || data.approved === 'true') {
    // 3DS succeeded — verify payment status from your backend
    const paymentStatus = await verifyPaymentOnBackend(data.externalPaymentId);

    if (paymentStatus === 'AUTHORIZED' || paymentStatus === 'CAPTURED') {
      showSuccessMessage();
    } else {
      showErrorMessage('Payment could not be confirmed.');
    }
  } else {
    // 3DS failed — let user retry
    showErrorMessage('Card verification failed. Please try again or use a different card.');
  }
});
```

### PostMessage Payload — Success

```json
{
  "paymentId": "dce568c6-98ec-456c-bb33-4a6809c4fff8",
  "externalPaymentId": "order-12345",
  "amount": 99.99,
  "approved": true
}
```

### PostMessage Payload — Failure

```json
{
  "paymentId": "dce568c6-98ec-456c-bb33-4a6809c4fff8",
  "externalPaymentId": "order-12345",
  "amount": 99.99,
  "approved": false,
  "status": "Rejected by ACS",
  "responseCode": "99"
}
```

### Complete SPA Example

```javascript
// Full 3DS handling for a single-page app
class ThreeDSHandler {
  constructor() {
    this.iframe = document.getElementById('threeds-iframe');
    this.container = document.getElementById('threeds-container');
    this.pendingResolve = null;

    window.addEventListener('message', (event) => {
      if (!event.origin.includes('simpleps.com')) return;
      this.handleResult(event.data);
    });
  }

  // Returns a promise that resolves when 3DS completes
  startChallenge(redirectAcsUrl) {
    return new Promise((resolve) => {
      this.pendingResolve = resolve;
      this.container.style.display = 'block';
      this.iframe.src = redirectAcsUrl;
    });
  }

  handleResult(data) {
    this.container.style.display = 'none';
    this.iframe.src = '';
    if (this.pendingResolve) {
      this.pendingResolve({
        success: data.approved === true || data.approved === 'true',
        paymentId: data.paymentId,
        externalPaymentId: data.externalPaymentId
      });
      this.pendingResolve = null;
    }
  }
}

// Usage in your checkout flow:
const threeds = new ThreeDSHandler();

async function processPayment(paymentData) {
  const response = await createPayment(paymentData);

  if (response.data.status === 'CHALLENGE') {
    const result = await threeds.startChallenge(response.data.redirectAcsUrl);

    if (result.success) {
      // Verify on backend, then show confirmation
      const verified = await verifyPayment(result.externalPaymentId);
      showConfirmation(verified);
    } else {
      showRetryPrompt();
    }
  } else if (response.data.status === 'AUTHORIZED') {
    showConfirmation(response.data);
  } else {
    showDeclineMessage(response.data.message);
  }
}
```

---

## Choosing Between Redirect and PostMessage

| Consideration | URL Redirect | PostMessage |
|---|---|---|
| **User experience** | Full page navigation; user leaves your checkout | Seamless; challenge appears in iframe/modal |
| **Implementation complexity** | Simpler — just set URLs | More code — manage iframe + event listener |
| **Server requirements** | Need server-side POST handlers for success/fail URLs | No server handlers needed for 3DS result |
| **SPA compatibility** | Requires workarounds (state loss on redirect) | Native fit for SPAs |
| **Security** | Result delivered server-to-server (POST to your URL) | Result delivered client-side (validate origin!) |
| **Mobile webview** | Works everywhere | Some webviews restrict iframe postMessage |

**Recommendation:**
- Use **URL Redirect** if you have a traditional server-rendered checkout or need maximum compatibility.
- Use **PostMessage** if you're building an SPA or want an embedded checkout experience without page navigation.

Regardless of which option you choose, **always verify the payment status via the API** (`GET /payments/{externalPaymentId}`) before fulfilling the order.

---

## Testing 3DS

Use the 3DS-enabled test cards from the [Test Data](../../../../test-data/cards.md) page:

| Card Number | Network | 3DS Type |
|---|---|---|
| `5413330033003303` | Mastercard | Challenge |
| `5169527513596963` | Mastercard | Challenge |
| `4983305199046950` | Visa | Challenge |
| `5454545454545454` | Mastercard | Data-only |
| `4975303994654672` | Visa | Data-only |
| `6011926557021045` | Discover | Data-only |

## Implementation Checklist

- [ ] Register your tokenizer page URL with Inyo (required for CORS)
- [ ] Choose your integration method: URL Redirect or PostMessage
- [ ] **If Redirect:** Configure `successUrl` and `failUrl` in tokenizer; implement server-side POST handlers
- [ ] **If PostMessage:** Register your PostMessage listener page URL with Inyo; set `enablePostMessage: true` in tokenizer; implement `message` event listener with origin validation
- [ ] Detect `status: "CHALLENGE"` in payment responses
- [ ] Handle the 3DS result (success and failure)
- [ ] **Always** verify payment status via GET API after 3DS completes
- [ ] Test with 3DS test cards in sandbox
- [ ] Handle edge cases: user closes iframe/tab, timeout, network errors
- [ ] Notify Inyo of any URL changes before deploying to new domains
