PlexySDK DOCS

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:

ScenarioShopper InteractionUse Case
Customer-initiatedEcommerce or ContractPresentOne-click checkout, reorders
Merchant-initiatedContractPresentSubscriptions, 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

ValueDescriptionWhen to Use
EcommerceCustomer-initiated online paymentCheckout, one-click pay buttons
ContractPresentMerchant-initiated under prior agreementSubscriptions, 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 captured
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',
    capture=True,
)
# Payment is immediately captured

Manual 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

ParameterTypeRequiredDescription
amountintegerYesAmount in smallest currency unit (cents, tiyn, etc.)
currencystringYesThree-letter ISO currency code
token_idstringYesThe token ID from tokenization
shopper_idstringYesCustomer identifier matching the token
recurring_processing_modelstringYesCardOnFile, Subscription, or UnscheduledCardOnFile
shopper_interactionstringYesEcommerce or ContractPresent
capturebooleanNoAuto-capture payment (default: false)
descriptionstringNoPayment description for records
metadataobjectNoCustom key-value data
return_urlstringNo*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

  1. Retry logic - Implement smart retries for failed payments
  2. Grace periods - Give customers time to update payment methods
  3. Notifications - Alert customers before and after charges
  4. Dunning management - Have a process for handling failed renewals

For Card-on-File Payments

  1. Confirm at checkout - Show saved card details before charging
  2. CVC re-entry - Consider asking for CVC on high-value orders
  3. Default payment method - Let customers set a preferred card

For All Token Payments

  1. Monitor decline rates - High declines may indicate token issues
  2. Keep tokens updated - Use Account Updater for automatic updates
  3. Handle expiration - Proactively request new cards before expiry

Testing

Use these test tokens to verify payment scenarios:

Test TokenResult
tok_test_successPayment succeeds
tok_test_declinedCard declined
tok_test_insufficient_fundsInsufficient funds
tok_test_3ds_required3D Secure challenge
tok_test_expiredCard expired

Next Steps

На этой странице