Make Token Payments
Process payments using saved tokens for returning customers, subscriptions, and merchant-initiated transactions
Make Token Payments
Once you've created a token, you can use it to process payments without requiring the customer to re-enter their card details. This guide covers all scenarios for making payments with tokens.
Overview
Token payments fall into two categories based on who initiates the payment:
| Scenario | Shopper Interaction | Use Case |
|---|---|---|
| Customer-initiated | Ecommerce or ContractPresent | One-click checkout, reorders |
| Merchant-initiated | ContractPresent | Subscriptions, usage billing, automatic top-ups |
Customer-Initiated Payments
When the customer is present and actively initiating the payment (e.g., clicking "Pay Now" on your checkout), use shopper_interaction: "Ecommerce".
curl -X POST https://api.plexypay.com/v2/payments \
-H "x-api-key: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 15000,
"currency": "KZT",
"token_id": "tok_card_visa_4242",
"shopper_id": "shopper_123456",
"recurring_processing_model": "CardOnFile",
"shopper_interaction": "Ecommerce",
"return_url": "https://yoursite.com/order/complete",
"description": "Order #12345"
}'import { Plexy } from '@plexy/plexy-web';
const plexy = new Plexy({
apiKey: process.env.PLEXY_SECRET_KEY,
});
const payment = await plexy.payments.create({
amount: 15000,
currency: 'KZT',
token_id: 'tok_card_visa_4242',
shopper_id: 'shopper_123456',
recurring_processing_model: 'CardOnFile',
shopper_interaction: 'Ecommerce',
return_url: 'https://yoursite.com/order/complete',
description: 'Order #12345',
});
if (payment.status === 'requires_action') {
// 3D Secure required - redirect customer
console.log('Redirect to:', payment.redirect_url);
} else if (payment.status === 'authorized') {
console.log('Payment authorized:', payment.id);
}import os
from plexy import Plexy
plexy = Plexy(api_key=os.environ['PLEXY_SECRET_KEY'])
payment = plexy.payments.create(
amount=15000,
currency='KZT',
token_id='tok_card_visa_4242',
shopper_id='shopper_123456',
recurring_processing_model='CardOnFile',
shopper_interaction='Ecommerce',
return_url='https://yoursite.com/order/complete',
description='Order #12345',
)
if payment.status == 'requires_action':
# 3D Secure required - redirect customer
print(f'Redirect to: {payment.redirect_url}')
elif payment.status == 'authorized':
print(f'Payment authorized: {payment.id}')Response
{
"id": "pay_xyz789abc123",
"status": "authorized",
"amount": 15000,
"currency": "KZT",
"token_id": "tok_card_visa_4242",
"shopper_id": "shopper_123456",
"recurring_processing_model": "CardOnFile",
"shopper_interaction": "Ecommerce",
"payment_method": {
"type": "card",
"card": {
"brand": "visa",
"last4": "4242",
"exp_month": 12,
"exp_year": 2028
}
},
"created_at": "2026-03-25T14:20:00Z"
}For customer-initiated payments, 3D Secure authentication may be required.
Always handle the requires_action status and redirect the customer to
complete authentication.
Merchant-Initiated Payments
When you charge a customer without their active participation (e.g., subscription renewal, usage billing), use shopper_interaction: "ContractPresent".
Subscription Payments
For fixed-amount recurring charges on a regular schedule:
curl -X POST https://api.plexypay.com/v2/payments \
-H "x-api-key: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 9900,
"currency": "USD",
"token_id": "tok_card_visa_4242",
"shopper_id": "shopper_123456",
"recurring_processing_model": "Subscription",
"shopper_interaction": "ContractPresent",
"description": "Pro Plan - April 2026",
"metadata": {
"subscription_id": "sub_abc123",
"billing_period": "2026-04"
}
}'const payment = await plexy.payments.create({
amount: 9900,
currency: 'USD',
token_id: 'tok_card_visa_4242',
shopper_id: 'shopper_123456',
recurring_processing_model: 'Subscription',
shopper_interaction: 'ContractPresent',
description: 'Pro Plan - April 2026',
metadata: {
subscription_id: 'sub_abc123',
billing_period: '2026-04',
},
});
if (payment.status === 'authorized') {
console.log('Subscription payment successful');
await capturePayment(payment.id);
} else if (payment.status === 'declined') {
console.log('Payment declined:', payment.decline_reason);
await notifyCustomerPaymentFailed(shopper_id);
}payment = plexy.payments.create(
amount=9900,
currency='USD',
token_id='tok_card_visa_4242',
shopper_id='shopper_123456',
recurring_processing_model='Subscription',
shopper_interaction='ContractPresent',
description='Pro Plan - April 2026',
metadata={
'subscription_id': 'sub_abc123',
'billing_period': '2026-04',
},
)
if payment.status == 'authorized':
print('Subscription payment successful')
capture_payment(payment.id)
elif payment.status == 'declined':
print(f'Payment declined: {payment.decline_reason}')
notify_customer_payment_failed(shopper_id)Unscheduled Card-on-File Payments
For variable amounts or irregular timing (usage-based billing, account top-ups):
curl -X POST https://api.plexypay.com/v2/payments \
-H "x-api-key: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 4523,
"currency": "EUR",
"token_id": "tok_card_visa_4242",
"shopper_id": "shopper_123456",
"recurring_processing_model": "UnscheduledCardOnFile",
"shopper_interaction": "ContractPresent",
"description": "API usage charges - March 2026",
"metadata": {
"usage_type": "api_calls",
"period_start": "2026-03-01",
"period_end": "2026-03-31"
}
}'const payment = await plexy.payments.create({
amount: 4523,
currency: 'EUR',
token_id: 'tok_card_visa_4242',
shopper_id: 'shopper_123456',
recurring_processing_model: 'UnscheduledCardOnFile',
shopper_interaction: 'ContractPresent',
description: 'API usage charges - March 2026',
metadata: {
usage_type: 'api_calls',
period_start: '2026-03-01',
period_end: '2026-03-31',
},
});payment = plexy.payments.create(
amount=4523,
currency='EUR',
token_id='tok_card_visa_4242',
shopper_id='shopper_123456',
recurring_processing_model='UnscheduledCardOnFile',
shopper_interaction='ContractPresent',
description='API usage charges - March 2026',
metadata={
'usage_type': 'api_calls',
'period_start': '2026-03-01',
'period_end': '2026-03-31',
},
)Merchant-initiated transactions (MIT) require prior customer consent. Ensure you have documented agreement from the customer before processing MIT payments.
Shopper Interaction Types
| Value | Description | When to Use |
|---|---|---|
Ecommerce | Customer-initiated online payment | Checkout, one-click pay buttons |
ContractPresent | Merchant-initiated under prior agreement | Subscriptions, scheduled billing, usage charges |
Understanding the Difference
Ecommerce (Customer-Initiated)
- Customer clicks a button to pay
- Customer is present during the transaction
- 3D Secure may be triggered
- Lower fraud risk, higher approval rates
ContractPresent (Merchant-Initiated)
- Merchant charges without customer interaction
- Customer previously agreed to future charges
- 3D Secure exemption applies
- Requires proper consent documentation
Handling 3D Secure
For customer-initiated payments, the card issuer may require 3D Secure authentication.
After creating a payment, check if authentication is required.
const payment = await plexy.payments.create({
amount: 15000,
currency: 'KZT',
token_id: 'tok_card_visa_4242',
shopper_id: 'shopper_123456',
recurring_processing_model: 'CardOnFile',
shopper_interaction: 'Ecommerce',
return_url: 'https://yoursite.com/checkout/complete',
});If the status is requires_action, redirect the customer.
if (payment.status === 'requires_action') {
// Store payment ID to retrieve later
await storePaymentIdInSession(payment.id);
// Redirect to 3DS authentication
res.redirect(payment.redirect_url);
}After authentication, the customer returns to your return_url.
// On your return URL handler
app.get('/checkout/complete', async (req, res) => {
const paymentId = await getPaymentIdFromSession();
const payment = await plexy.payments.retrieve(paymentId);
if (payment.status === 'authorized') {
// Payment successful
res.render('success', { payment });
} else {
// Payment failed
res.render('failed', { reason: payment.decline_reason });
}
});Auto-Capture vs Manual Capture
By default, payments are authorized but not captured. You can control this behavior:
Auto-Capture
Set capture: true to capture immediately:
curl -X POST https://api.plexypay.com/v2/payments \
-H "x-api-key: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 9900,
"currency": "USD",
"token_id": "tok_card_visa_4242",
"shopper_id": "shopper_123456",
"recurring_processing_model": "Subscription",
"shopper_interaction": "ContractPresent",
"capture": true
}'const payment = await plexy.payments.create({
amount: 9900,
currency: 'USD',
token_id: 'tok_card_visa_4242',
shopper_id: 'shopper_123456',
recurring_processing_model: 'Subscription',
shopper_interaction: 'ContractPresent',
capture: true,
});
// Payment is immediately capturedpayment = plexy.payments.create(
amount=9900,
currency='USD',
token_id='tok_card_visa_4242',
shopper_id='shopper_123456',
recurring_processing_model='Subscription',
shopper_interaction='ContractPresent',
capture=True,
)
# Payment is immediately capturedManual Capture
Authorize first, then capture when ready (e.g., after shipping):
# Capture an authorized payment
curl -X POST https://api.plexypay.com/v2/payments/pay_xyz789abc123/capture \
-H "x-api-key: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 9900
}'// Capture the full amount
const captured = await plexy.payments.capture('pay_xyz789abc123');
// Or capture a partial amount
const captured = await plexy.payments.capture('pay_xyz789abc123', {
amount: 5000, // Capture only part of the authorization
});# Capture the full amount
captured = plexy.payments.capture('pay_xyz789abc123')
# Or capture a partial amount
captured = plexy.payments.capture('pay_xyz789abc123', amount=5000)Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | integer | Yes | Amount in smallest currency unit (cents, tiyn, etc.) |
currency | string | Yes | Three-letter ISO currency code |
token_id | string | Yes | The token ID from tokenization |
shopper_id | string | Yes | Customer identifier matching the token |
recurring_processing_model | string | Yes | CardOnFile, Subscription, or UnscheduledCardOnFile |
shopper_interaction | string | Yes | Ecommerce or ContractPresent |
capture | boolean | No | Auto-capture payment (default: false) |
description | string | No | Payment description for records |
metadata | object | No | Custom key-value data |
return_url | string | No* | URL for 3DS redirect return |
*Required for customer-initiated payments that may require 3D Secure
Error Handling
import { Plexy, PlexyError } from '@plexy/plexy-web';
try {
const payment = await plexy.payments.create({
amount: 9900,
currency: 'USD',
token_id: 'tok_card_visa_4242',
shopper_id: 'shopper_123456',
recurring_processing_model: 'Subscription',
shopper_interaction: 'ContractPresent',
});
} catch (error) {
if (error instanceof PlexyError) {
switch (error.code) {
case 'token_not_found':
console.error('Token does not exist or was deleted');
break;
case 'token_expired':
console.error('Card has expired, request new payment method');
break;
case 'insufficient_funds':
console.error('Card declined due to insufficient funds');
break;
case 'card_declined':
console.error('Card was declined:', error.decline_reason);
break;
case 'shopper_mismatch':
console.error('Token does not belong to this shopper');
break;
default:
console.error('Payment failed:', error.message);
}
}
}from plexy import Plexy, PlexyError
try:
payment = plexy.payments.create(
amount=9900,
currency='USD',
token_id='tok_card_visa_4242',
shopper_id='shopper_123456',
recurring_processing_model='Subscription',
shopper_interaction='ContractPresent',
)
except PlexyError as error:
if error.code == 'token_not_found':
print('Token does not exist or was deleted')
elif error.code == 'token_expired':
print('Card has expired, request new payment method')
elif error.code == 'insufficient_funds':
print('Card declined due to insufficient funds')
elif error.code == 'card_declined':
print(f'Card was declined: {error.decline_reason}')
elif error.code == 'shopper_mismatch':
print('Token does not belong to this shopper')
else:
print(f'Payment failed: {error.message}')Best Practices
For Subscription Payments
- Retry logic - Implement smart retries for failed payments
- Grace periods - Give customers time to update payment methods
- Notifications - Alert customers before and after charges
- Dunning management - Have a process for handling failed renewals
For Card-on-File Payments
- Confirm at checkout - Show saved card details before charging
- CVC re-entry - Consider asking for CVC on high-value orders
- Default payment method - Let customers set a preferred card
For All Token Payments
- Monitor decline rates - High declines may indicate token issues
- Keep tokens updated - Use Account Updater for automatic updates
- Handle expiration - Proactively request new cards before expiry
Testing
Use these test tokens to verify payment scenarios:
| Test Token | Result |
|---|---|
tok_test_success | Payment succeeds |
tok_test_declined | Card declined |
tok_test_insufficient_funds | Insufficient funds |
tok_test_3ds_required | 3D Secure challenge |
tok_test_expired | Card expired |
Next Steps
- Manage Tokens - List, update, and delete tokens
- Forward Payment Details - Share tokens with third parties
- 3D Secure - Learn about authentication requirements