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.
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": { }
}'
- Endpoint:
/payment/v2/payments - Method:
POST - API Reference
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}"
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 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).
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.
- Endpoint:
GET /wallet/v3/merchants/{merchant_id}/customers/{customer_reference}/tokens— list tokens ({merchant_id}= vaultclient_name) - Endpoint:
POST /payment/v2/payments— create 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}"
- 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
- Customization: Adapt the flow to your brand
- Create a payment: 3DS and payment creation details
- SDK Reference: Full API documentation
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>