Accept credit and debit cards with automatic 3DS authentication, PCI-compliant tokenisation, and built-in fraud prevention.
Card payments are automatically included in the drop-in and appear as the default expanded payment method. The drop-in handles all card input fields, validation, tokenisation, and 3D Secure authentication automatically.
- New card entry: Secure card number, expiry, and CVV input
- Card-on-file: Stored cards for returning customers (requires
onGetShopper) - 3D Secure: Automatic 3DS authentication when required
- Address verification: Optional AVS for fraud prevention
- Validation: Automatic validation of card number (Luhn), expiry date, and CVV
When a customer pays with a card:
- The customer enters their card details (number, expiry, CVV, name). Real-time validation provides instant feedback.
- The customer clicks submit.
- The card is securely tokenised (never touches your server).
- The system evaluates whether 3DS is required.
- If needed, 3DS authentication occurs.
- The transaction is processed.
- Your
onSuccesscallback fires.
Drop-in handles tokenisation, 3DS, and all card field configuration automatically.
Card payments inherit all settings from methodConfig.global. There are no card-specific configuration options - the card object remains empty.
The following properties are available in methodConfig.global and apply to card payments:
| Property | Description |
|---|---|
acceptedCardNetworksstring[] | Which card brands to accept (Visa, Mastercard, etc.). Falls back to session.allowedFundingTypes.cardSchemes |
allowedCardFundingSourcestring[] | Which funding types to accept (Credit, Debit, Prepaid) |
allowedIssuerCountryCodesstring[] | Restrict cards to specific issuer countries (ISO country codes) |
onGetConsent(paymentMethod) => boolean | Controls whether to show consent checkbox for saving cards |
onCancel(paymentMethod, data) => void | Called when user cancels card payment (e.g., closes 3DS window) |
Card payments use an empty card object in methodConfig:
methodConfig: {
global: {
// Card settings go here
},
card: {
// Empty - all configuration is in global
}
}This example shows a full card configuration using global settings:
import PaymentMethod from '@pxpio/web-components-sdk/src/components/checkoutDropInComponents/types/PaymentMethod';
methodConfig: {
global: {
// Card settings
acceptedCardNetworks: ['Visa', 'Mastercard', 'American Express'],
allowedCardFundingSource: ['CREDIT', 'DEBIT'],
allowedIssuerCountryCodes: ['US', 'GB', 'CA'],
// Show consent checkbox for card to enable card-on-file
onGetConsent: (paymentMethod: PaymentMethod) => {
return paymentMethod === PaymentMethod.Card || paymentMethod === PaymentMethod.Paypal;
},
// Handle 3DS window cancellation
onCancel: (paymentMethod: PaymentMethod, data: any) => {
if (paymentMethod === PaymentMethod.Card) {
console.log('Card payment cancelled', data);
// Track analytics
trackEvent('card_cancelled', {
timestamp: Date.now()
});
}
}
},
card: {
// Empty - inherits from global
}
}Choose the appropriate intent for your use case:
import IntentType from '@pxpio/web-components-sdk/src/basePxpCheckout/types/IntentType';
// Physical goods (authorise now, capture later)
transactionData: {
intent: { card: IntentType.Authorisation } // Capture when shipped
}
// Digital goods (immediate capture)
transactionData: {
intent: { card: IntentType.Purchase }
}
// Card verification (no charge)
transactionData: {
amount: 0.00,
intent: { card: IntentType.Verification }
}Card payments work through the standard Drop-in implementation:
import CheckoutDropIn from '@pxpio/web-components-sdk/src/checkoutDropIn/CheckoutDropIn';
import IntentType from '@pxpio/web-components-sdk/src/basePxpCheckout/types/IntentType';
import { BaseSubmitResult } from '@pxpio/web-components-sdk/src/checkoutDropIn/types/BaseSubmitResult';
import BaseSdkException from '@pxpio/web-components-sdk/src/types/sdkExceptions/BaseSdkException';
// Get session from backend
const response = await fetch('/api/create-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const result = await response.json();
if (!result.success || !result.data) {
console.error('Failed to create session:', result.error);
throw new Error('Session creation failed');
}
const sessionData = result.data;
// Initialise Drop-in
const checkoutDropIn = CheckoutDropIn.initialize({
environment: 'test',
session: sessionData,
ownerId: 'MERCHANT-1',
ownerType: 'MerchantGroup',
transactionData: {
currency: 'USD',
amount: 99.99,
entryType: 'Ecom',
intent: {
card: IntentType.Authorisation,
paypal: IntentType.Authorisation
},
merchantTransactionId: crypto.randomUUID(),
merchantTransactionDate: () => new Date().toISOString()
},
onGetShopper: () => Promise.resolve({ id: 'shopper-123' }),
onSuccess: async (result: BaseSubmitResult) => {
// CRITICAL: Verify on backend
await verifyPaymentOnBackend(result);
},
onError: (error: BaseSdkException) => {
console.error('Card payment failed:', error);
alert(`Payment failed: ${error.message}`);
}
});
// Mount the drop-in
checkoutDropIn.create('checkout-drop-in-container');Create a session with card payments enabled:
// BACKEND: Create session with card enabled
const sessionRequest = {
merchant: "MERCHANT-1",
site: "SITE-1",
sessionTimeout: 120,
merchantTransactionId: crypto.randomUUID(),
transactionMethod: {
intent: {
card: "Authorisation"
}
},
amounts: {
currencyCode: "USD",
transactionValue: 99.99
},
allowedFundingTypes: {
card: true // Enable card payments
},
allowTransaction: true,
serviceType: "CheckoutDropIn"
};When a card payment succeeds, your onSuccess callback receives the standard result:
onSuccess: (result: BaseSubmitResult) => {
console.log('Payment Details:');
console.log('- System Transaction ID:', result.systemTransactionId);
console.log('- Merchant Transaction ID:', result.merchantTransactionId);
console.log('- Payment Method:', result.paymentMethod); // "Card"
// Note: authenticationId is NOT included in the callback for card payments
// Amount, currency, and other transaction details must be retrieved from backend
}Unlike Google Pay and Apple Pay, card payments don't include authenticationId in the onSuccess callback, even when 3DS authentication was used. You must retrieve all transaction details (including authentication data, amount, currency, etc.) from your backend by querying the PXP API with the systemTransactionId.
Handle card-specific errors:
import BaseSdkException from '@pxpio/web-components-sdk/src/types/sdkExceptions/BaseSdkException';
onError: (error: BaseSdkException) => {
// Check SDK error codes
if (error.code === 'SDK1114') {
alert('Card verification failed.');
} else if (error.code === 'SDK1116') {
// Card payment failed - check message for specifics
if (error.message.toLowerCase().includes('declined')) {
alert('Card declined. Try a different card.');
} else if (error.message.toLowerCase().includes('insufficient funds')) {
alert('Insufficient funds.');
} else if (error.message.toLowerCase().includes('expired')) {
alert('Card expired.');
} else if (error.message.toLowerCase().includes('cvv') ||
error.message.toLowerCase().includes('cvc') ||
error.message.toLowerCase().includes('security code')) {
alert('Invalid security code.');
} else if (error.message.toLowerCase().includes('card number')) {
alert('Invalid card number.');
} else if (error.message.toLowerCase().includes('expiry') ||
error.message.toLowerCase().includes('expiration')) {
alert('Invalid expiry date.');
} else {
alert('Card payment failed. Please check your details.');
}
} else if (error.code === 'SDK0500') {
alert('Connection error. Check your internet.');
} else if (error.message.toLowerCase().includes('session') ||
error.message.toLowerCase().includes('expired')) {
alert('Session expired. Refresh the page.');
} else if (error.message.toLowerCase().includes('timeout')) {
alert('Verification timed out.');
} else {
alert(`Payment failed: ${error.message}`);
}
}Always verify card payments on your backend to ensure payment success before fulfilling orders:
import { BaseSubmitResult } from '@pxpio/web-components-sdk/src/checkoutDropIn/types/BaseSubmitResult';
// FRONTEND: Send to backend
onSuccess: async (result: BaseSubmitResult) => {
const verified = await fetch('/api/verify-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
systemTransactionId: result.systemTransactionId,
merchantTransactionId: result.merchantTransactionId
})
}).then(r => r.json());
if (verified.success) {
window.location.href = `/success?orderId=${verified.orderId}`;
} else {
alert('Payment verification failed');
}
}Use the following backend code to verify card transactions via the PXP API:
// BACKEND: Complete verification
app.post('/api/verify-payment', async (req, res) => {
const { systemTransactionId, merchantTransactionId } = req.body;
// 1. Verify transaction
const txnPath = `api/v1/transactions/${systemTransactionId}`;
const { authHeader: txnAuthHeader, requestId: txnRequestId } = createAuthHeader(
txnPath,
'',
process.env.PXP_TOKEN_ID,
process.env.PXP_TOKEN_VALUE
);
const transaction = await fetch(
`https://api-services.pxp.io/${txnPath}`,
{
headers: {
'X-Client-Id': process.env.PXP_CLIENT_ID,
'X-Request-Id': txnRequestId,
'Authorization': txnAuthHeader
}
}
).then(r => r.json());
if (transaction.state !== 'Authorised' && transaction.state !== 'Captured') {
return res.json({ success: false, error: 'Transaction not authorised' });
}
// Verify amount matches expected amount from your order records
const order = await getOrderByMerchantTransactionId(merchantTransactionId);
const txnAmount = transaction.amounts?.transactionValue || transaction.amount || 0;
if (Math.abs(txnAmount - order.amount) > 0.01) {
return res.json({ success: false, error: 'Amount mismatch' });
}
// 2. Verify 3DS if used
if (transaction.authentication?.authenticationId) {
const authPath = `api/v1/threedsecure/integrated/authentications/${transaction.authentication.authenticationId}`;
const { authHeader: auth3dsHeader, requestId: auth3dsRequestId } = createAuthHeader(
authPath,
'',
process.env.PXP_TOKEN_ID,
process.env.PXP_TOKEN_VALUE
);
const auth = await fetch(
`https://api-services.pxp.io/${authPath}`,
{
headers: {
'X-Client-Id': process.env.PXP_CLIENT_ID,
'X-Request-Id': auth3dsRequestId,
'Authorization': auth3dsHeader
}
}
).then(r => r.json());
if (auth.transactionStatus !== 'Y' && auth.transactionStatus !== 'A') {
return res.json({ success: false, error: '3DS verification failed' });
}
}
// 3. Check idempotency
if (await checkOrderFulfilled(merchantTransactionId)) {
return res.json({ success: false, error: 'Already processed' });
}
// 4. Fulfill order
const orderId = await fulfillOrder(merchantTransactionId, transaction);
return res.json({ success: true, orderId });
});Your backend should verify that:
- The transaction exists and its state is
AuthorisedorCaptured. - The amount and currency match the expected values .
- The transaction is 3DS authenticated (if
authenticationIdpresent in transaction response). - CVV/AVS were checked (for non-3DS).
- The transaction wasn't previously processed .
- There are no replay attacks (
authenticationIdnot reused).
Drop-in automatically handles 3DS authentication when required. The payment flow works as follows:
- The customer enters their card details and clicks submit.
- The card is tokenised securely.
- The system evaluates whether 3DS is required:
- If 3DS is needed: Device fingerprinting → Authentication (frictionless or challenge).
- If 3DS isn't needed: Direct authorisation with CVV/AVS checks.
- The transaction is processed
- Your
onSuccesscallback fires.
Drop-in automatically uses 3DS authentication when:
- The amount exceeds the risk thresholds configured in the Unity Portal.
- Strong Customer Authentication (SCA) is required.
- High-risk indicators are present.
- The card issuer requires it.
To check if 3DS was used, query the transaction from your backend using the PXP API. The response includes 3DS authentication data when used.
You can't directly control whether Drop-in uses 3DS in your code. Configure rules in the Unity Portal under Merchant setup > Merchant groups > Services > Card service.
Drop-in supports recurring payments for subscription-based services and merchant-initiated transactions (MITs). Configure the recurring object in your transaction data to set up the initial payment, then use the stored gatewayTokenId for subsequent charges.
PXP doesn't provide an automatic payment scheduler. You must implement your own scheduling system to initiate subsequent recurring charges via the Transactions API.
The recurring payment flow involves two phases:
Phase 1: Initial setup (via Drop-in)
- Configure the
recurringobject intransactionDatawith frequency and expiration. - The customer enters their card details and completes payment.
- Drop-in automatically sets
processingModeltoMerchantInitiatedInitialRecurring. - The backend stores the
gatewayTokenIdfrom the transaction response.
When you include the recurring object in your transaction data, Drop-in automatically sets the processingModel to MerchantInitiatedInitialRecurring for the initial transaction.
Phase 2: Subsequent charges (via backend API)
- Your scheduling system triggers a charge based on the frequency.
- Your backend calls the Transactions API using the stored
gatewayTokenId. - Set
processingModeltoMerchantInitiatedSubsequentRecurringfor subsequent charges. - The payment processes without customer interaction.
Configure recurring payment frequency in your Drop-in initialisation:
import CheckoutDropIn from '@pxpio/web-components-sdk/src/checkoutDropIn/CheckoutDropIn';
import IntentType from '@pxpio/web-components-sdk/src/basePxpCheckout/types/IntentType';
import { BaseSubmitResult } from '@pxpio/web-components-sdk/src/checkoutDropIn/types/BaseSubmitResult';
import BaseSdkException from '@pxpio/web-components-sdk/src/types/sdkExceptions/BaseSdkException';
// Get session from backend
const response = await fetch('/api/create-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const result = await response.json();
if (!result.success || !result.data) {
console.error('Failed to create session:', result.error);
throw new Error('Session creation failed');
}
const sessionData = result.data;
// Initialize Drop-in with recurring configuration
const checkoutDropIn = CheckoutDropIn.initialize({
environment: 'test',
session: sessionData,
ownerId: 'MERCHANT-1',
ownerType: 'MerchantGroup',
transactionData: {
currency: 'USD',
amount: 9.99,
entryType: 'Ecom',
intent: { card: IntentType.Authorisation },
merchantTransactionId: crypto.randomUUID(),
merchantTransactionDate: () => new Date().toISOString(),
// Recurring payment configuration
recurring: {
frequencyInDays: 30, // Bill every 30 days
frequencyExpiration: '2025-12-31' // Token expires on this date
}
},
onGetShopper: () => Promise.resolve({ id: 'customer-123' }),
onSuccess: async (result: BaseSubmitResult) => {
console.log('Initial recurring payment setup completed');
// Verify payment and store gatewayTokenId on backend
const response = await fetch('/api/verify-and-setup-recurring', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
systemTransactionId: result.systemTransactionId,
merchantTransactionId: result.merchantTransactionId
})
}).then(r => r.json());
if (response.success) {
window.location.href = `/subscription-success?subscriptionId=${response.subscriptionId}`;
}
},
onError: (error: BaseSdkException) => {
console.error('Recurring payment setup failed:', error);
alert(`Subscription setup failed: ${error.message}`);
}
});
checkoutDropIn.create('checkout-drop-in-container');After the initial payment, extract and store the gatewayTokenId for future charges:
// BACKEND: Verify initial payment and set up recurring subscription
app.post('/api/verify-and-setup-recurring', async (req, res) => {
const { systemTransactionId, merchantTransactionId } = req.body;
try {
// 1. Query PXP API to get transaction details
const txnPath = `api/v1/transactions/${systemTransactionId}`;
const { authHeader, requestId } = createAuthHeader(
txnPath,
'',
process.env.PXP_TOKEN_ID,
process.env.PXP_TOKEN_VALUE
);
const transaction = await fetch(
`https://api-services.pxp.io/${txnPath}`,
{
headers: {
'X-Client-Id': process.env.PXP_CLIENT_ID,
'X-Request-Id': requestId,
'Authorization': authHeader
}
}
).then(r => r.json());
// 2. Verify transaction is successful
if (transaction.state !== 'Authorised' && transaction.state !== 'Captured') {
return res.json({ success: false, error: 'Transaction not successful' });
}
// 3. Store gatewayTokenId for recurring charges
if (transaction.fundingData?.gatewayTokenId) {
const subscription = await database.subscriptions.insert({
shopperId: req.user.shopperId,
gatewayTokenId: transaction.fundingData.gatewayTokenId,
amount: transaction.amounts?.transactionValue || 0,
currency: transaction.amounts?.currencyCode || 'USD',
frequencyInDays: 30,
nextChargeDate: calculateNextChargeDate(30),
status: 'active',
createdAt: new Date()
});
return res.json({ success: true, subscriptionId: subscription.id });
}
return res.json({ success: false, error: 'No gateway token found' });
} catch (error) {
console.error('Setup error:', error);
return res.json({ success: false, error: 'Setup failed' });
}
});Use the Transactions API to charge subsequent payments from your backend scheduler:
// BACKEND: Charge subsequent recurring payment
async function chargeRecurringPayment(subscription) {
const requestBody = {
merchant: "MERCHANT-1",
site: "SITE-1",
merchantTransactionId: `recurring-${Date.now()}`,
merchantTransactionDate: new Date().toISOString(),
transactionMethod: {
intent: "Purchase",
entryType: "Ecom",
fundingType: "Card"
},
fundingData: {
card: {
gatewayTokenId: subscription.gatewayTokenId
}
},
amounts: {
transaction: subscription.amount,
currencyCode: subscription.currency
},
recurring: {
processingModel: "MerchantInitiatedSubsequentRecurring"
}
};
const path = 'api/v1/transactions';
const { authHeader, requestId } = createAuthHeader(
path,
JSON.stringify(requestBody),
process.env.PXP_TOKEN_ID,
process.env.PXP_TOKEN_VALUE
);
const response = await fetch(
`https://api-services.pxp.io/${path}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Client-Id': process.env.PXP_CLIENT_ID,
'X-Request-Id': requestId,
'Authorization': authHeader
},
body: JSON.stringify(requestBody)
}
);
const result = await response.json();
if (result.state === 'Captured' || result.state === 'Authorised') {
// Update subscription with next charge date
await database.subscriptions.update(subscription.id, {
lastChargeDate: new Date(),
nextChargeDate: calculateNextChargeDate(subscription.frequencyInDays),
lastTransactionId: result.systemTransactionId
});
console.log('Recurring charge successful:', result.systemTransactionId);
return { success: true, transactionId: result.systemTransactionId };
} else {
console.error('Recurring charge failed:', result);
return { success: false, error: result.errorReason };
}
}Learn more about initiating transactions via API
Card-on-file allows returning customers to pay faster by selecting from their saved cards. The drop-in automatically displays saved cards when you implement the onGetShopper callback.
How it works:
- The customer completes initial payment and consents to save their card.
- The backend stores the
gatewayTokenIdfrom the transaction. - On a return visit,
onGetShopperprovides the shopper ID. - Drop-in displays saved cards for selection.
- The customer selects a saved card and pays.
To enable saved cards display, implement the required onGetShopper callback:
import CheckoutDropIn from '@pxpio/web-components-sdk/src/checkoutDropIn/CheckoutDropIn';
import IntentType from '@pxpio/web-components-sdk/src/basePxpCheckout/types/IntentType';
import { BaseSubmitResult } from '@pxpio/web-components-sdk/src/checkoutDropIn/types/BaseSubmitResult';
import BaseSdkException from '@pxpio/web-components-sdk/src/types/sdkExceptions/BaseSdkException';
// Get session data from backend
const response = await fetch('/api/create-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const result = await response.json();
if (!result.success || !result.data) {
console.error('Failed to create session:', result.error);
throw new Error('Session creation failed');
}
const sessionData = result.data;
const checkoutDropIn = CheckoutDropIn.initialize({
environment: 'test',
session: sessionData,
ownerId: 'MERCHANT-1',
ownerType: 'MerchantGroup',
transactionData: {
currency: 'USD',
amount: 99.99,
entryType: 'Ecom',
intent: { card: IntentType.Purchase },
merchantTransactionId: crypto.randomUUID(),
merchantTransactionDate: () => new Date().toISOString()
},
// REQUIRED: Provide shopper ID to enable card-on-file
onGetShopper: async () => {
// Get authenticated user's shopper ID from your session/auth system
const user = await getCurrentUser();
return { id: user.shopperId }; // e.g., { id: 'shopper-123' }
},
onSuccess: async (result: BaseSubmitResult) => {
await verifyPaymentOnBackend(result);
window.location.href = '/success';
},
onError: (error: BaseSdkException) => {
console.error('Payment failed:', error);
alert(`Payment failed: ${error.message}`);
}
});
checkoutDropIn.create('checkout-drop-in-container');When onGetShopper returns a shopper ID, the drop-in automatically:
- Fetches saved cards from the PXP API.
- Displays them in a "Saved Cards" section above the new card form.
- Allows customers to select and pay with saved cards.
Get customer consent before saving cards:
import PaymentMethod from '@pxpio/web-components-sdk/src/components/checkoutDropInComponents/types/PaymentMethod';
const checkoutDropIn = CheckoutDropIn.initialize({
// ... other config
onBeforeSubmit: async (paymentMethod: PaymentMethod) => {
// Only prompt for new card payments
if (paymentMethod === PaymentMethod.Card) {
const saveCard = confirm('Save this card for faster checkout next time?');
if (saveCard) {
// Store consent in your database
await storeCardConsent(user.id, true);
}
}
return true; // Proceed with payment
},
onSuccess: async (result: BaseSubmitResult) => {
// Verify payment and extract gatewayTokenId on backend
const response = await fetch('/api/verify-and-save-card', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
systemTransactionId: result.systemTransactionId,
merchantTransactionId: result.merchantTransactionId,
saveCard: true // Pass consent to backend
})
}).then(r => r.json());
if (response.success) {
window.location.href = `/success?orderId=${response.orderId}`;
}
}
});After a successful payment, extract the gatewayTokenId from the transaction and store it:
// BACKEND: Verify payment and save card token
app.post('/api/verify-and-save-card', async (req, res) => {
const { systemTransactionId, merchantTransactionId, saveCard } = req.body;
try {
// 1. Query PXP API to get transaction details
const txnPath = `api/v1/transactions/${systemTransactionId}`;
const { authHeader, requestId } = createAuthHeader(
txnPath,
'',
process.env.PXP_TOKEN_ID,
process.env.PXP_TOKEN_VALUE
);
const transaction = await fetch(
`https://api-services.pxp.io/${txnPath}`,
{
headers: {
'X-Client-Id': process.env.PXP_CLIENT_ID,
'X-Request-Id': requestId,
'Authorization': authHeader
}
}
).then(r => r.json());
// 2. Verify transaction is successful
if (transaction.state !== 'Authorised' && transaction.state !== 'Captured') {
return res.json({ success: false, error: 'Transaction not successful' });
}
// 3. Save gatewayTokenId if customer consented
if (saveCard && transaction.fundingData?.gatewayTokenId) {
await database.savedCards.insert({
shopperId: req.user.shopperId,
gatewayTokenId: transaction.fundingData.gatewayTokenId,
last4: transaction.fundingData.cardDetails?.last4Digits,
cardScheme: transaction.fundingData.cardScheme,
expiryMonth: transaction.fundingData.cardDetails?.expiryMonth,
expiryYear: transaction.fundingData.cardDetails?.expiryYear,
createdAt: new Date()
});
}
// 4. Fulfill order
const orderId = await fulfillOrder(transaction);
return res.json({ success: true, orderId });
} catch (error) {
console.error('Verification error:', error);
return res.json({ success: false, error: 'Verification failed' });
}
});