Skip to main content

Advanced Flow

Introduction

The Advanced Flow gives you full control over how payment data is collected and processed. It separates the secure collection of sensitive card information (handled by the SecureFields SDK) from the actual payment creation (handled by the Payment API).

This flow is recommended if you want to:

  • Keep a low-level integration with maximum flexibility.
  • Control when and how payments are created.
  • Handle business rules before sending a payment request.

Below is the nominal sequence for a successful payment using the Advanced Flow:

Advanced Flow sequence

Flow Description

  1. Checkout initialization — The customer opens checkout; the Merchant Frontend initializes the SecureFields SDK and receives secure-fields configuration.
  2. Card data collection — The customer enters card details in secure fields. On submit, the SecureFields SDK returns a vault_form_token (no PAN/CVV touch your systems) which is sent to the Merchant Backend.
  3. Payment creation — The Merchant Backend calls the Payment API with the token, order data, 3DS preferences, and shopper redirection URL. The API forwards to the PSP, creating a pending payment and returning a 3DS redirect URL.
  4. 3DS authentication — The customer is redirected to the PSP’s 3DS page. After successful authentication, the PSP redirects back to your shopper_redirection_url with context (purse-redirection-data).
  5. Final update — The Payment API sends the final payment status to the Merchant Backend via webhook.

Before you start

  • Ask your Purse Integration Team for a Vault Tenant ID and your Payment API Key.

Collect card details

In this section, we will show you how to collect card details using the SecureFields SDK.

Beta Notice

The SecureFields SDK is currently in beta. Documentation may evolve. Contact us at [email protected] with questions or feedback.

1. Install the SDK

<!-- Sandbox -->
<script src="https://cdn.purse-sandbox.com/secure-fields/latest/purse.js"></script>

<!-- Production -->
<script src="https://cdn.purse-secure.com/secure-fields/stable/purse.js"></script>

The CDN script exports a global PurseSecureFields object.

You can get the ESM version from the CDN. Just replace purse.js with purse.esm.js.

2. Initialize SecureFields

import { loadSecureFields } from '@purse-eu/web-sdk';

async function init() {
const sf = await loadSecureFields('sandbox');

const secureForm = await sf.initSecureFields({
tenantId: '[your vault tenant_id]',
config: {
brands: ['VISA', 'MASTERCARD', 'CARTE_BANCAIRE', 'AMERICAN_EXPRESS'],
fields: {
cardNumber: {
target: 'pan-placeholder',
placeholder: 'ex: 1234 5678 9012 3456',
ariaLabel: 'Numéro de carte',
iframeTitle: 'Numéro de carte'
},
cvv: {
target: 'cvv-placeholder',
placeholder: 'ex: 123',
ariaLabel: 'CVV',
iframeTitle: 'CVV'
}
},
styles: {
color: "#181818"
}
}
}
);
}

init();

3. Render the fields

await secureForm.render();

4. Optionally update configuration

secureForm.setConfig({
fields: {
cardNumber: {
placeholder: 'ex :new placeholder',
},
},
styles: {
input: {
color: 'blue',
},
},
});

5. Listen for events

secureForm.on('ready', () => {
console.log('Secure fields are ready');
});

secureForm.on('change', (event) => {
console.log('Field changed', event);
});

6. Submit and receive vault_form_token

try {
const result = await secureForm.submit({
expiryMonth: 12,
expiryYear: 25,
cardHolderName: 'Joe Dawn',
selectedNetwork: 'VISA',
saveToken: false
});
console.log('vault_form_token received:', result.vault_form_token);
} catch (err) {
console.error('Submit failed', err.code, err.reason);
}

Create Payment

After successfully receiving the vault_form_token, you must pass this token to create a payment along with other required customer and order data when calling the payment creation endpoint.

To create a payment, call the /create_payment endpoint:

await fetch('https://api.purse-sandbox.com/payment/v2/payments', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'xxx'
},
body: JSON.stringify({
amount: 100,
currency: 'EUR',
// ...other order data
split: [{
vault_form_token: "123",
card: {
holder_name: "John Doe",
selected_network: "VISA",
birth_date: "12/12/12"
}
}]
})
});

For 3DS payments, see 3DS authentication.

Complete Integration Example

This example shows a complete implementation flow including initialization, rendering, event handling, form submission, payment processing, and error handling.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Purse Secure Fields — Minimal Example</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<!-- Include Purse Secure Fields via CDN -->
<script src="https://cdn.purse-sandbox.com/secure-fields/latest/purse.js"></script>
<style>
/* Compact containers for the iframed fields */
#pan-placeholder, #cvv-placeholder {
height: 32px;
min-height: 0;
}
#pan-placeholder iframe, #cvv-placeholder iframe {
height: 100% !important;
display: block;
}
</style>
</head>
<body class="min-h-screen bg-gray-50 flex items-center justify-center p-6">
<div class="w-full max-w-md">
<div class="bg-white shadow rounded-xl p-6 border border-gray-100">
<h1 class="text-lg font-semibold text-gray-900 mb-1">Purse Secure Fields</h1>
<p class="text-sm text-gray-500 mb-6">Copy/paste example for documentation.</p>

<form id="payment-form" class="space-y-4">
<div id="payment-error" class="hidden text-sm text-red-600 bg-red-50 border border-red-200 rounded-md p-3"></div>

<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 items-end">
<div class="sm:col-span-2">
<label id="pan-label" class="block text-sm font-medium text-gray-700 mb-1">Card Number</label>
<div id="pan-placeholder" aria-labelledby="pan-label" class="w-full rounded-lg border border-gray-300 bg-white px-2 py-0 shadow-sm focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500"></div>
</div>
<div>
<label for="brand-select" class="block text-sm font-medium text-gray-700 mb-1">Brand</label>
<select id="brand-select" class="w-full h-8 rounded-lg border border-gray-300 bg-white px-2 py-1 shadow-sm focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500">
<option value="VISA">VISA</option>
<option value="MASTERCARD">MASTERCARD</option>
<option value="CARTE_BANCAIRE">CARTE_BANCAIRE</option>
<option value="AMERICAN_EXPRESS">AMERICAN_EXPRESS</option>
</select>
</div>
</div>

<div>
<label id="cvv-label" class="block text-sm font-medium text-gray-700 mb-1">CVV</label>
<div id="cvv-placeholder" aria-labelledby="cvv-label" class="w-full rounded-lg border border-gray-300 bg-white px-2 py-0 shadow-sm focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500"></div>
</div>

<div>
<label for="cardholder-name" class="block text-sm font-medium text-gray-700 mb-1">Holder Name</label>
<input type="text" id="cardholder-name" class="w-full rounded-lg border border-gray-300 bg-white px-2 py-1 shadow-sm focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500" placeholder="John Doe" />
</div>

<div class="grid grid-cols-2 gap-3">
<div>
<label for="expiry-month" class="block text-sm font-medium text-gray-700 mb-1">Expiry Month</label>
<input type="text" id="expiry-month" class="w-full rounded-lg border border-gray-300 bg-white px-2 py-1 shadow-sm focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500" placeholder="MM" />
</div>
<div>
<label for="expiry-year" class="block text-sm font-medium text-gray-700 mb-1">Expiry Year</label>
<input type="text" id="expiry-year" class="w-full rounded-lg border border-gray-300 bg-white px-2 py-1 shadow-sm focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500" placeholder="YY" />
</div>
</div>

<div class="flex items-center gap-3">
<button id="submit-button" type="submit" class="inline-flex items-center justify-center rounded-lg bg-indigo-600 px-4 py-2 text-white text-sm font-medium shadow hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-60">
Pay
</button>
<div id="loading-indicator" style="display: none;" class="text-sm text-gray-600">Processing...</div>
</div>

<div>
<label for="submit-result" class="block text-sm font-medium text-gray-700 mb-1">Submit result</label>
<pre id="submit-result" class="whitespace-pre-wrap text-xs bg-gray-50 border border-gray-200 rounded-lg p-3 text-gray-800"></pre>
</div>
</form>
</div>
</div>

<script>
(async function (purse) {
// 1) Initialize SecureFields
const supportedBrands = ['VISA', 'MASTERCARD', 'CARTE_BANCAIRE', 'AMERICAN_EXPRESS'];
const secureForm = await purse.initSecureFields({
tenantId: '[your vault tenant_id]',
config: {
brands: supportedBrands,
fields: {
cardNumber: {
target: 'pan-placeholder',
placeholder: 'ex: 1234 5678 9012 3456',
ariaLabel: 'Card number',
iframeTitle: 'Card number',
},
cvv: {
target: 'cvv-placeholder',
placeholder: 'ex: 123',
ariaLabel: 'CVV',
iframeTitle: 'CVV',
},
},
styles: {
input: {
fontSize: '14px',
lineHeight: '20px',
},
},
},
});

// 2) Render
await secureForm.render();

// 3) Events
secureForm.on('ready', function () {
const errorBox = document.getElementById('payment-error');
if (errorBox) {
errorBox.textContent = '';
errorBox.classList.add('hidden');
}
});

secureForm.on('brandDetected', function ({ brands }) {
const brandSelect = document.getElementById('brand-select');
if (!brandSelect || !brands || !brands.length) return;
const detected = String(brands[0]);
const has = Array.from(brandSelect.options).some(function (opt) { return opt.value === detected; });
if (has) brandSelect.value = detected;
});

// 4) Submit
document.getElementById('payment-form').addEventListener('submit', async function (e) {
e.preventDefault();
const btn = document.getElementById('submit-button');
const spinner = document.getElementById('loading-indicator');
const pre = document.getElementById('submit-result');
if (pre) pre.textContent = '';
btn.disabled = true;
spinner.style.display = 'block';
try {
const expMonth = document.getElementById('expiry-month').value;
const expYear = document.getElementById('expiry-year').value;
const holder = document.getElementById('cardholder-name').value;
const selectedNetwork = document.getElementById('brand-select').value;
const result = await secureForm.submit({
expiryMonth: expMonth,
expiryYear: expYear,
cardHolderName: holder,
selectedNetwork: selectedNetwork,
saveToken: false,
});

// In real life you would send the vault_form_token to the payment creation endpoint.
if (pre) pre.textContent = JSON.stringify(result, null, 2);
} catch (err) {
const anyErr = err || {};
const box = document.getElementById('payment-error');
if (box) {
box.textContent = 'Payment failed: ' + (anyErr.reason || 'Unknown error');
box.classList.remove('hidden');
}
if (pre) pre.textContent = JSON.stringify({ error: true, code: anyErr.code, reason: anyErr.reason || String(anyErr) }, null, 2);
} finally {
btn.disabled = false;
spinner.style.display = 'none';
}
});
})(window.PurseSecureFields);
</script>
</body>
</html>

Next Steps