Card Tokenization Guide
Learn how to securely tokenize cards using Payme Subscribe API for recurring payments.
Overview
Card tokenization allows you to:
- Save customer cards securely
- Process recurring payments
- Avoid storing sensitive card data
- Comply with PCI DSS requirements
Tokenization Flow
Card Tokenization Sequence
Step 1: Create Card Token
Use client-side mode to create a card token:
typescript
import { PaymeSubscribe } from '@joyida/payme';
const subscribeClient = new PaymeSubscribe({
merchantId: process.env.PAYME_MERCHANT_ID!
}, 'client');
const result = await subscribeClient.cardsCreate({
card: {
number: '8600069195406311',
expire: '0399' // MMYY format (March 2099)
},
save: true,
account: { user_id: '12345' } // Optional
});
console.log('Card token:', result.card.token);
console.log('Verified:', result.card.verify); // false (not verified yet)
console.log('Recurrent:', result.card.recurrent); // true if supports recurringImportant:
- Card number: 13-19 digits
- Expiry format: MMYY (e.g., "0399" = March 2099)
- Token is not verified yet
- Save the token to your database
Step 2: Request SMS Verification
Request an SMS verification code:
typescript
const smsResult = await subscribeClient.cardsGetVerifyCode({
token: result.card.token
});
console.log('SMS sent:', smsResult.sent); // true
console.log('Phone:', smsResult.phone); // "99890***1234" (masked)
console.log('Wait time:', smsResult.wait); // 60000 (ms)What happens:
- SMS code is sent to cardholder's phone
- Phone number is masked for security
- Wait time indicates when you can request again
Step 3: Verify Card with SMS Code
Verify the card using the SMS code:
typescript
const verifyResult = await subscribeClient.cardsVerify({
token: result.card.token,
code: '666666' // SMS code from user
});
console.log('Verified:', verifyResult.card.verify); // true
console.log('Token:', verifyResult.card.token); // Same token, now verifiedImportant:
- Code is typically 6 digits
- Token remains the same after verification
- Verified token can be used for payments
Step 4: Check Card Status (Server-Side)
Use server-side mode to check card status:
typescript
const subscribeServer = new PaymeSubscribe({
merchantId: process.env.PAYME_MERCHANT_ID!,
password: process.env.PAYME_PASSWORD!
}, 'server');
const checkResult = await subscribeServer.cardsCheck({
token: result.card.token
});
console.log('Verified:', checkResult.card.verify); // true
console.log('Recurrent:', checkResult.card.recurrent); // true
console.log('Card number:', checkResult.card.number); // "860006******6311" (masked)
console.log('Expiry:', checkResult.card.expire); // "0399"Step 5: Use Token for Payments
Create Receipt
typescript
const receipt = await subscribeServer.receiptsCreate({
amount: 500000, // 5000 UZS in tiyin
account: { order_id: '123' },
description: 'Payment for order #123'
});
console.log('Receipt ID:', receipt.receipt._id);
console.log('State:', receipt.receipt.state); // 0 (waiting for payment)Pay with Token
typescript
const payment = await subscribeServer.receiptsPay({
id: receipt.receipt._id,
token: result.card.token
});
console.log('Payment state:', payment.receipt.state); // 1 (paid)Complete Tokenization Example
typescript
import { PaymeSubscribe, Validator, PaymeError } from '@joyida/payme';
// Client-side: Tokenize card
async function tokenizeCard(
cardNumber: string,
cardExpiry: string,
userId: string
) {
const subscribeClient = new PaymeSubscribe({
merchantId: process.env.PAYME_MERCHANT_ID!
}, 'client');
try {
// Validate card data
Validator.validateCardNumber(cardNumber);
Validator.validateCardExpiry(cardExpiry);
// 1. Create card token
const created = await subscribeClient.cardsCreate({
card: { number: cardNumber, expire: cardExpiry },
save: true,
account: { user_id: userId }
});
console.log('✅ Card token created:', created.card.token);
// 2. Request SMS verification
const sms = await subscribeClient.cardsGetVerifyCode({
token: created.card.token
});
console.log(`📱 SMS sent to ${sms.phone}`);
console.log(`⏳ Wait ${sms.wait}ms before requesting again`);
// 3. Get SMS code from user (your implementation)
const smsCode = await promptUserForSMSCode();
// 4. Verify card
const verified = await subscribeClient.cardsVerify({
token: created.card.token,
code: smsCode
});
if (verified.card.verify) {
console.log('✅ Card verified successfully!');
// Save token to database
await saveCardToken(userId, verified.card.token);
return verified.card.token;
} else {
throw new Error('Card verification failed');
}
} catch (error) {
if (error instanceof PaymeError) {
console.error('Payme error:', error.code, error.message);
}
throw error;
}
}
// Server-side: Use token for payment
async function payWithToken(
cardToken: string,
amount: number,
orderId: string
) {
const subscribeServer = new PaymeSubscribe({
merchantId: process.env.PAYME_MERCHANT_ID!,
password: process.env.PAYME_PASSWORD!
}, 'server');
try {
// 1. Check card status
const cardCheck = await subscribeServer.cardsCheck({
token: cardToken
});
if (!cardCheck.card.verify) {
throw new Error('Card not verified');
}
if (!cardCheck.card.recurrent) {
throw new Error('Card does not support recurring payments');
}
// 2. Create receipt
const receipt = await subscribeServer.receiptsCreate({
amount,
account: { order_id: orderId },
description: `Payment for order ${orderId}`
});
console.log('✅ Receipt created:', receipt.receipt._id);
// 3. Pay with token
const payment = await subscribeServer.receiptsPay({
id: receipt.receipt._id,
token: cardToken
});
if (payment.receipt.state === 1) {
console.log('✅ Payment successful!');
return payment.receipt;
} else {
throw new Error('Payment failed');
}
} catch (error) {
if (error instanceof PaymeError) {
console.error('Payme error:', error.code, error.message);
}
throw error;
}
}Card Validation
Validate Card Number
typescript
import { Validator, ValidationError } from '@joyida/payme';
try {
// Valid formats
Validator.validateCardNumber('8600069195406311'); // OK
Validator.validateCardNumber('8600 0691 9540 6311'); // OK (spaces allowed)
// Invalid formats
Validator.validateCardNumber('123'); // Throws ValidationError
Validator.validateCardNumber('abcd1234'); // Throws ValidationError
} catch (error) {
if (error instanceof ValidationError) {
console.error('Invalid card number:', error.message);
}
}Validate Card Expiry
typescript
try {
// Valid formats
Validator.validateCardExpiry('0399'); // OK (March 2099)
Validator.validateCardExpiry('1225'); // OK (December 2025)
// Invalid formats
Validator.validateCardExpiry('1323'); // Throws (invalid month)
Validator.validateCardExpiry('0320'); // Throws (expired)
Validator.validateCardExpiry('03/99'); // Throws (wrong format)
} catch (error) {
if (error instanceof ValidationError) {
console.error('Invalid expiry:', error.message);
}
}Remove Card Token
Remove a card token when no longer needed:
typescript
const subscribeServer = new PaymeSubscribe({
merchantId: process.env.PAYME_MERCHANT_ID!,
password: process.env.PAYME_PASSWORD!
}, 'server');
const result = await subscribeServer.cardsRemove({
token: cardToken
});
if (result.success) {
console.log('✅ Card token removed');
// Remove from your database
await deleteCardToken(userId, cardToken);
}Security Best Practices
✅ Do's
- Use client-side mode for tokenization
- Use server-side mode for payments
- Validate card data before sending
- Store only the token (never store card numbers)
- Use HTTPS for all API calls
- Implement rate limiting for SMS requests
- Log all tokenization attempts
❌ Don'ts
- Never store raw card numbers
- Never store CVV codes
- Never expose tokens in URLs
- Never share tokens between users
- Never use client-side mode for payments
- Never log sensitive card data
Error Handling
typescript
import { PaymeError, ValidationError } from '@joyida/payme';
try {
await subscribeClient.cardsCreate(params);
} catch (error) {
if (error instanceof ValidationError) {
// Input validation error
console.error('Validation error:', error.message);
// Show error to user
} else if (error instanceof PaymeError) {
// Payme API error
console.error('Payme error:', error.code, error.message);
// Handle specific errors
if (error.code === -31050) {
console.error('Invalid card number');
} else if (error.code === -31051) {
console.error('Invalid expiry date');
}
}
}Database Schema
Example schema for storing card tokens:
sql
CREATE TABLE card_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
token TEXT UNIQUE NOT NULL,
card_number TEXT NOT NULL, -- Masked (e.g., "860006******6311")
card_expire TEXT NOT NULL, -- MMYY format
verified BOOLEAN DEFAULT FALSE,
recurrent BOOLEAN DEFAULT FALSE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE INDEX idx_card_tokens_user_id ON card_tokens(user_id);
CREATE INDEX idx_card_tokens_token ON card_tokens(token);Recurring Payments
Use verified tokens for recurring payments:
typescript
async function processRecurringPayment(
userId: string,
amount: number,
orderId: string
) {
// Get user's saved card token
const cardToken = await getUserCardToken(userId);
if (!cardToken) {
throw new Error('No saved card found');
}
// Process payment
const payment = await payWithToken(cardToken, amount, orderId);
return payment;
}Next Steps
- Learn about Receipt Management
- Explore Database Integration
- Check Error Handling
- See Recurring Payment Examples