Skip to main content

Token Payment

Introduction

Token payment enables a seamless experience for repeat purchases. Customers can save their payment method once and reuse it for future transactions, reducing friction and increasing conversion rates.

User Consent

Always ask customers for explicit consent before saving their card. This builds trust and ensures compliance.

1. Save Card for Future Use

During checkout, offer customers the option to save their card for future purchases.

<div className="flex items-center gap-2 mb-4">
<input type="checkbox" id="save-card" name="save-card" className="rounded border-gray-300" />
<label htmlFor="save-card" className="text-sm text-gray-700">Save card for future purchases</label>
</div>

When submitting the payment, include the consent in the SDK call:

const saveCard = document.getElementById('save-card').checked;
const result = await secureForm.submit({ saveToken: saveCard });
// Send result.vault_form_token to your server to create the payment

To save a card during payment, your server calls the Payment API with the save_token flag:

curl -X POST 'https://api.purse-sandbox.com/payment/v2/payments' \
--header 'Content-Type: application/json' \
--header "x-api-key: ${API_KEY}" \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--data-raw '{
"entity_id": "${ENTITY_ID}",
"amount": 100,
"currency": "EUR",
"order": { },
"split": [
{
"vault_form_token": "${VAULT_FORM_TOKEN}",
"save_token": true
}
],
"browser": { }
}'
API Endpoint

2. Pay with a Saved Card

For subsequent purchases, customers can select a saved card. Only CVV is required for extra security.

Your server retrieves the saved tokens for the customer:

curl -X GET 'https://api.purse-sandbox.com/wallet/v3/merchants/${VAULT_CLIENT_NAME}/customers/${CUSTOMER_REFERENCE}/tokens' \
--header "Authorization: Bearer ${ACCESS_TOKEN}"
merchant path segment = vault client_name

The {merchant_id} path segment is the vault client_name configured for your entity (e.g. QA_CORP), not your entity_id UUID. Using the entity id returns no tokens.

The response wraps the tokens in an envelope — the array is under tokens:

{
"tokens": [
{
"id": "c1fffec1-…",
"status": "ACTIVE",
"expiration_date": "2034-12-31T23:59:59.999999Z",
"description": { "brand_name": "VISA", "display_token": "401200******3001", "holder_name": "JO MA" },
"scope": { "partner": "ingenico", "method": "creditcard" }
}
],
"total_elements": 1,
"shared_wallet_user": false
}

Tokens may be INACTIVE or belong to non-card methods (PayPal, gift cards), so filter to the tokens you can charge — typically status === 'ACTIVE' and scope.method === 'creditcard'.

Display saved cards for selection (client-side, using data returned by your server):

function displaySavedCards(tokens) {
tokens.forEach(token => {
const button = document.createElement('button');
button.textContent = token.description.display_token || token.id;
button.onclick = () => {
startPaymentFlow(token);
};
document.getElementById('saved-cards-list').appendChild(button);
});
}

Initialize SecureFields for CVV input (client-side) with the token brand:

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

let secureForm;
async function initCvvOnlyForm(tokenBrand) {
const sf = await loadSecureFields('sandbox');
secureForm = await sf.initSecureFields({
tenantId: '${VAULT_TENANT_ID}',
config: {
brands: [tokenBrand],
fields: {
cvv: {
target: 'cvv-only-placeholder',
placeholder: 'ex: 123',
ariaLabel: 'CVV',
iframeTitle: 'CVV',
}
},
styles: {
color: '#181818'
}
}
});
await secureForm.render();
}

HTML placeholder for CVV field:

<div>
<label id="cvv-only-label" class="block text-sm font-medium text-gray-700 mb-1">CVV</label>
<div id="cvv-only-placeholder" aria-labelledby="cvv-only-label" class="w-full rounded-lg border border-gray-300 bg-white px-2 py-2"></div>
</div>

Submit the CVV (client-side), then send the resulting vault_form_token to your server to create the payment:

async function startPaymentFlow(token) {
await initCvvOnlyForm( token.description.brand_name );
const result = await secureForm.submit(); // leave empty
// Send result.vault_form_token and selectedWalletTokenId to your server
await fetch('/your-server/pay-with-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
vaultFormToken: result.vault_form_token,
walletTokenId: token.id,
}),
});
}

Your server then calls the Payment API:

curl -X POST 'https://api.purse-sandbox.com/payment/v2/payments' \
--header 'Content-Type: application/json' \
--header "x-api-key: ${API_KEY}" \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--data-raw '{
"entity_id": "${ENTITY_ID}",
"amount": 100,
"currency": "EUR",
"order": {
"reference": "order-456",
"net_amount": 100,
"tax_amount": 0
},
"customer": {
"reference": "${CUSTOMER_REFERENCE}"
},
"split": [
{
"amount": 100,
"partner": "${TOKEN_PARTNER}",
"method": "creditcard",
"wallet_token": "${WALLET_TOKEN_ID}",
"vault_form_token": "${VAULT_FORM_TOKEN}",
"three_ds_authentication_options": {
"challenge_indicator": "NO_CHALLENGE_REQUESTED"
}
}
],
"browser": {
"user_agent": "Mozilla/5.0 …",
"accept_header": "text/html",
"color_depth": 32,
"java_enabled": false,
"javascript_enabled": true,
"locale": "fr-FR",
"screen_height": 1080,
"screen_width": 1920,
"utc_time_zone": 60
}
}'
customer.reference is required for token payments

customer.reference must match the customer the token belongs to — it is how the server resolves the wallet_token to its stored vault token. If it is missing, the PSP rejects the payment with "mandatory payment data are not present … vault_vendor_token_id". order.reference is also required. wallet_token is the token's id (the UUID from the list response).

Required vs optional fields

Which split fields are mandatory can depend on the PSP. The create-payment OpenAPI schema is the source of truth: at minimum amount, currency, entity_id, order at the top level and amount, partner, method per split item. browser is required for 3DS.

API Endpoint
  • Endpoint: GET /wallet/v3/merchants/{merchant_id}/customers/{customer_reference}/tokenslist tokens ({merchant_id} = vault client_name)
  • Endpoint: POST /payment/v2/paymentscreate payment

3. Delete a Saved Card

Customers may want to remove a saved card for privacy or security reasons. Your server calls the Wallet API to delete the token:

curl -X DELETE 'https://api.purse-sandbox.com/wallet/v3/merchants/${VAULT_CLIENT_NAME}/customers/${CUSTOMER_REFERENCE}/tokens/${TOKEN_ID}' \
--header "Authorization: Bearer ${ACCESS_TOKEN}"
API Endpoint
  • Endpoint: DELETE /wallet/v3/merchants/{merchant_id}/customers/{customer_reference}/tokens/{id}
  • API Reference

Further Information

For more details about tokenization and payment tokens, see the Token Payment Glossary.

Next Steps

Full Working Example

The flow below illustrates a complete token payment integration: the client-side handles the UI and SecureFields, while all API calls are made server-side.

Client-side — Display saved cards and collect CVV:

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

let secureForm;

async function initCvvOnlyForm(tokenBrand) {
const sf = await loadSecureFields('sandbox');
secureForm = await sf.initSecureFields({
tenantId: '${VAULT_TENANT_ID}',
config: {
brands: [tokenBrand],
fields: {
cvv: {
target: 'cvv-only-placeholder',
placeholder: 'ex: 123',
ariaLabel: 'CVV',
iframeTitle: 'CVV',
}
},
styles: { color: '#181818' }
}
});
await secureForm.render();
}

async function displaySavedCards() {
// Fetch saved tokens from your server (your server calls the Wallet API)
const response = await fetch('/your-server/tokens');
// The wallet API wraps tokens in an envelope; keep only chargeable cards.
const { tokens } = await response.json();

tokens
.filter(token => token.status === 'ACTIVE' && token.scope?.method === 'creditcard')
.forEach(token => {
const button = document.createElement('button');
button.textContent = token.description.display_token || token.id;
button.onclick = async () => {
await initCvvOnlyForm(token.description.brand_name);
const result = await secureForm.submit();
// Send vault_form_token and token id to your server
await fetch('/your-server/pay-with-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
vaultFormToken: result.vault_form_token,
walletTokenId: token.id,
}),
});
};
document.getElementById('saved-cards-list').appendChild(button);
});
}

async function deleteToken(tokenId) {
await fetch(`/your-server/tokens/${tokenId}`, { method: 'DELETE' });
}

Server-side — Wallet and Payment API calls:

# List saved tokens
curl -X GET 'https://api.purse-sandbox.com/wallet/v3/merchants/${VAULT_CLIENT_NAME}/customers/${CUSTOMER_REFERENCE}/tokens' \
--header "Authorization: Bearer ${ACCESS_TOKEN}"

# Create payment with saved token
curl -X POST 'https://api.purse-sandbox.com/payment/v2/payments' \
--header 'Content-Type: application/json' \
--header "x-api-key: ${API_KEY}" \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--data-raw '{
"entity_id": "${ENTITY_ID}",
"amount": 100,
"currency": "EUR",
"order": {
"reference": "order-456",
"net_amount": 100,
"tax_amount": 0
},
"customer": {
"reference": "${CUSTOMER_REFERENCE}"
},
"split": [
{
"amount": 100,
"partner": "${TOKEN_PARTNER}",
"method": "creditcard",
"wallet_token": "${WALLET_TOKEN_ID}",
"vault_form_token": "${VAULT_FORM_TOKEN}",
"three_ds_authentication_options": {
"challenge_indicator": "NO_CHALLENGE_REQUESTED"
}
}
],
"browser": {
"user_agent": "Mozilla/5.0 …",
"accept_header": "text/html",
"color_depth": 32,
"java_enabled": false,
"javascript_enabled": true,
"locale": "fr-FR",
"screen_height": 1080,
"screen_width": 1920,
"utc_time_zone": 60
}
}'

# Delete a token
curl -X DELETE 'https://api.purse-sandbox.com/wallet/v3/merchants/${VAULT_CLIENT_NAME}/customers/${CUSTOMER_REFERENCE}/tokens/${TOKEN_ID}' \
--header "Authorization: Bearer ${ACCESS_TOKEN}"

HTML placeholders:

<div id="saved-cards-list"></div>
<div>
<label id="cvv-only-label" class="block text-sm font-medium text-gray-700 mb-1">CVV</label>
<div id="cvv-only-placeholder" aria-labelledby="cvv-only-label" class="w-full rounded-lg border border-gray-300 bg-white px-2 py-2"></div>
</div>