Skip to content

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 recurring

Important:

  • 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 verified

Important:

  • 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

  1. Use client-side mode for tokenization
  2. Use server-side mode for payments
  3. Validate card data before sending
  4. Store only the token (never store card numbers)
  5. Use HTTPS for all API calls
  6. Implement rate limiting for SMS requests
  7. Log all tokenization attempts

❌ Don'ts

  1. Never store raw card numbers
  2. Never store CVV codes
  3. Never expose tokens in URLs
  4. Never share tokens between users
  5. Never use client-side mode for payments
  6. 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

Released under MIT License.