@qazuor/qzpay-mercadopago
v1.1.1
Published
MercadoPago payment adapter for QZPay billing library
Downloads
159
Maintainers
Readme
@qazuor/qzpay-mercadopago
MercadoPago payment provider adapter for the QZPay billing library.
Installation
pnpm add @qazuor/qzpay-mercadopago mercadopagoFeatures
- Payment Processing: Create, capture, cancel payments
- Subscriptions: Create and manage recurring payments (preapprovals)
- 3D Secure Support: Secure card payments with challenge flow
- Refunds: Full and partial refunds
- Webhooks/IPN: Event handling and HMAC signature verification
- Error Handling: Comprehensive error mapping with QZPayMercadoPagoError
- Multiple Payment Methods: Credit/debit cards, PIX, bank transfers
Quick Start
Basic Setup
import { createQZPayMercadoPagoAdapter } from '@qazuor/qzpay-mercadopago';
const mpAdapter = createQZPayMercadoPagoAdapter({
accessToken: process.env.MERCADOPAGO_ACCESS_TOKEN!,
webhookSecret: process.env.MERCADOPAGO_WEBHOOK_SECRET, // Optional
timeout: 5000 // Optional, default 5000ms
});With QZPayBilling
import { QZPayBilling } from '@qazuor/qzpay-core';
import { createDrizzleAdapter } from '@qazuor/qzpay-drizzle';
const billing = new QZPayBilling({
storage: createDrizzleAdapter({ db }),
paymentAdapter: mpAdapter
});Usage
Payment Processing
First-time Payment (with token)
// Create a payment with a token (generated on frontend)
const payment = await mpAdapter.payments.create('customer_mp_id', {
amount: 10000, // $100.00 in cents
currency: 'ARS',
token: 'card_token_from_frontend', // From MercadoPago.js
paymentMethodId: 'visa', // or 'master', 'pix', etc.
saveCard: true // Optional: save card for future use
});
// Retrieve payment status
const status = await mpAdapter.payments.retrieve(payment.id);
// Capture an authorized payment
await mpAdapter.payments.capture(payment.id);
// Refund a payment (partial or full)
await mpAdapter.payments.refund({ amount: 5000 }, payment.id);
// Cancel a payment
await mpAdapter.payments.cancel(payment.id);Recurring Payment (with saved card)
// Charge a previously saved card
// The adapter automatically generates a new token from the card_id
const payment = await mpAdapter.payments.create('customer_mp_id', {
amount: 10000, // $100.00 in cents
currency: 'ARS',
cardId: 'saved_card_id_123', // Previously saved card
paymentMethodId: 'visa', // Optional
installments: 1 // Optional
});Checkout Sessions
// Create a checkout session with optional notification URL
const checkout = await mpAdapter.checkout.create(
{
mode: 'payment',
successUrl: 'https://example.com/success',
cancelUrl: 'https://example.com/cancel',
lineItems: [{ priceId: 'price_1', quantity: 1, description: 'Product' }],
notificationUrl: 'https://example.com/webhooks/mercadopago', // Optional webhook URL
customerEmail: '[email protected]', // Optional
expiresInMinutes: 30 // Optional expiration
},
['price_1'] // Provider price IDs
);
console.log('Checkout URL:', checkout.url);
// Retrieve checkout session
const session = await mpAdapter.checkout.retrieve(checkout.id);
// Expire a checkout session
await mpAdapter.checkout.expire(checkout.id);Notification URL: When provided, MercadoPago will send IPN (Instant Payment Notification) events to this URL for payment status updates. This is useful for tracking payment completion server-side.
Subscriptions (Preapprovals)
// Create a subscription
const subscription = await mpAdapter.subscriptions.create('customer_mp_id', {
priceId: 'price_plan_basic', // Your plan identifier
trialDays: 14 // Optional trial period
});
// Retrieve subscription
const sub = await mpAdapter.subscriptions.retrieve(subscription.id);
// Update subscription
await mpAdapter.subscriptions.update(subscription.id, {
priceId: 'price_plan_premium' // Upgrade/downgrade
});
// Cancel subscription
await mpAdapter.subscriptions.cancel(subscription.id);3D Secure
import {
isMP3DSRequired,
extractMP3DSResult,
extractMP3DSPaymentInfo
} from '@qazuor/qzpay-mercadopago';
// Handle 3DS in webhook
function handlePaymentWebhook(event: QZPayWebhookEvent) {
const info = extractMP3DSPaymentInfo(event);
if (info.requires3DS) {
// Redirect user to challenge
const challengeUrl = info.challengeUrl;
return { action: 'redirect', url: challengeUrl };
}
if (info.threeDSecure?.status === 'authenticated') {
// Payment authenticated successfully
return { action: 'complete' };
}
if (info.threeDSecure?.status === 'failed') {
// Authentication failed
return { action: 'failed', reason: '3DS authentication failed' };
}
}Webhook/IPN Handling
import { createQZPayMercadoPagoAdapter } from '@qazuor/qzpay-mercadopago';
const mpAdapter = createQZPayMercadoPagoAdapter({
accessToken: process.env.MERCADOPAGO_ACCESS_TOKEN!,
webhookSecret: process.env.MERCADOPAGO_WEBHOOK_SECRET!
});
// Verify and parse webhook
app.post('/webhooks/mercadopago', async (c) => {
const payload = await c.req.text();
const signature = c.req.header('x-signature');
const requestId = c.req.header('x-request-id');
const event = await mpAdapter.webhooks.verifyAndParse(payload, signature, requestId);
if (!event) {
return c.json({ error: 'Invalid signature' }, 401);
}
switch (event.type) {
case 'payment.created':
case 'payment.updated':
// Handle payment event
break;
case 'subscription.created':
case 'subscription.updated':
// Handle subscription event
break;
}
return c.json({ received: true });
});Saved Card Service
Unified service for saving and managing payment cards with a consistent API.
import { createSavedCardService } from '@qazuor/qzpay-mercadopago';
// Create the service
const cardService = createSavedCardService({
provider: 'mercadopago',
mercadopagoAccessToken: process.env.MERCADOPAGO_ACCESS_TOKEN!,
getProviderCustomerId: async (customerId) => {
// Resolve your local customer ID to MercadoPago customer ID
const customer = await db.customers.findById(customerId);
return customer.mercadopagoCustomerId;
},
});
// Save a card from token (created on frontend with MercadoPago.js)
const card = await cardService.save({
customerId: 'local_cus_123',
token: 'card_token_xxx', // From MercadoPago.createCardToken()
setAsDefault: true, // Note: App must track this separately
});
console.log(`Card saved: ${card.brand} ending in ${card.last4}`);
console.log(`First 6 digits: ${card.firstSixDigits}`); // Available in MercadoPago
// List all saved cards
const cards = await cardService.list('local_cus_123');
cards.forEach((card) => {
console.log(`${card.brand} ${card.firstSixDigits}****${card.last4}`);
});
// Remove a card
await cardService.remove('local_cus_123', 'card_id_xxx');
// Note: setDefault() throws an error for MercadoPago
// You must track the default card in your application databaseFrontend Integration
// Create card token on frontend using MercadoPago.js
const mp = new MercadoPago('PUBLIC_KEY');
const cardForm = mp.cardForm({
amount: '100.5',
iframe: true,
form: {
id: 'form-checkout',
cardNumber: {
id: 'form-checkout__cardNumber',
placeholder: 'Card number',
},
expirationDate: {
id: 'form-checkout__expirationDate',
placeholder: 'MM/YY',
},
securityCode: {
id: 'form-checkout__securityCode',
placeholder: 'CVV',
},
cardholderName: {
id: 'form-checkout__cardholderName',
placeholder: 'Cardholder name',
},
issuer: {
id: 'form-checkout__issuer',
placeholder: 'Issuer',
},
installments: {
id: 'form-checkout__installments',
placeholder: 'Installments',
},
identificationType: {
id: 'form-checkout__identificationType',
placeholder: 'Document type',
},
identificationNumber: {
id: 'form-checkout__identificationNumber',
placeholder: 'Document number',
},
},
callbacks: {
onFormMounted: error => {
if (error) return console.warn('Form mounted handling error: ', error);
console.log('Form mounted');
},
onSubmit: event => {
event.preventDefault();
const { token } = cardForm.getCardFormData();
// Send token to backend
fetch('/api/cards/save', {
method: 'POST',
body: JSON.stringify({
customerId: 'local_cus_123',
token: token,
setAsDefault: true,
}),
});
},
},
});SavedCard Type
interface SavedCard {
id: string; // Card ID
customerId: string; // Your local customer ID
providerCustomerId: string; // MercadoPago customer ID
provider: 'mercadopago';
last4: string; // Last 4 digits
brand: string; // visa, master, amex, etc.
expMonth: number; // 1-12
expYear: number; // 4 digits
isDefault: boolean; // Always false (track in your DB)
cardholderName?: string; // Cardholder name
firstSixDigits?: string; // First 6 digits (MercadoPago specific)
createdAt: Date; // When the card was saved
}Important Notes on Default Payment Method
MercadoPago doesn't have a native concept of default payment method at the API level. This means:
- The
isDefaultfield will always befalsewhen listing cards - The
setAsDefault()method will throw an error - Your application must track the default card ID in your database
Example Database Schema:
interface Customer {
id: string;
email: string;
mercadopagoCustomerId: string;
defaultMercadoPagoCardId?: string; // Track this yourself
}When Creating Payments:
// Get customer's default card from your database
const customer = await db.customers.findById(customerId);
if (!customer.defaultMercadoPagoCardId) {
throw new Error('Customer has no default payment method');
}
// Create payment with explicit card ID
// The adapter will automatically generate a new token from the saved card
const payment = await mpAdapter.payments.create(
customer.mercadopagoCustomerId,
{
amount: 1000,
currency: 'ARS',
cardId: customer.defaultMercadoPagoCardId, // Use your tracked card ID
}
);Card on File Payment Flow
When using saved cards for recurring payments, QZPay automatically handles token generation:
// 1. Customer saves a card (first time)
const savedCard = await mpAdapter.customers.saveCard(
customer.mercadopagoCustomerId,
'card_token_from_frontend'
);
// Store card ID in your database
await db.update(customers)
.set({ defaultMercadoPagoCardId: savedCard.id })
.where(eq(customers.id, customerId));
// 2. Later, charge the saved card (recurring payment)
// The adapter automatically:
// - Generates a new card token from the saved card_id using CardToken API
// - Uses that token to create the payment
const payment = await mpAdapter.payments.create(
customer.mercadopagoCustomerId,
{
amount: 2999, // $29.99 in cents
currency: 'ARS',
cardId: savedCard.id, // Saved card ID
paymentMethodId: 'visa', // Optional
installments: 1 // Optional, default 1
}
);
// 3. The payment is processed with the saved card
console.log(`Payment ${payment.id} - Status: ${payment.status}`);Technical Details:
Under the hood, when you provide cardId, the adapter:
- Calls the MercadoPago CardToken API with the
card_id - Receives a new single-use token
- Uses that token to create the payment
- Stores the original
card_idin payment metadata for tracking
This is the recommended MercadoPago flow for Card on File payments.
Configuration
interface QZPayMercadoPagoConfig {
/** MercadoPago access token (required) */
accessToken: string;
/** Webhook secret for signature verification (optional) */
webhookSecret?: string;
/** Request timeout in ms (default: 5000) */
timeout?: number;
/** Integrator ID for attribution (optional) */
integratorId?: string;
/** Platform ID for marketplace (optional) */
platformId?: string;
}API Reference
Adapters
| Adapter | Description |
|---------|-------------|
| mpAdapter.customers | Customer management |
| mpAdapter.subscriptions | Subscription/preapproval management |
| mpAdapter.payments | Payment operations |
| mpAdapter.checkout | Checkout preferences |
| mpAdapter.prices | Price management (in-memory) |
| mpAdapter.webhooks | Webhook/IPN handling |
Limitations vs Stripe
MercadoPago has some functional differences compared to Stripe that you should be aware of when designing your integration:
Customer Metadata
| Feature | Stripe | MercadoPago | |---------|--------|-------------| | Custom metadata on customers | ✅ Full support | ⚠️ Limited | | Arbitrary key-value pairs | ✅ Up to 50 keys | ❌ Not supported | | Searchable metadata | ✅ Yes | ❌ No |
Workaround: Store extended customer data in your own database linked by provider_customer_id.
Saved Cards
| Feature | Stripe | MercadoPago | |---------|--------|-------------| | Save card | ✅ PaymentMethod | ✅ Card token | | List saved cards | ✅ Yes | ✅ Yes | | Remove card | ✅ Yes | ✅ Yes | | Default payment method | ✅ Native API support | ❌ Not supported | | First 6 digits | ❌ No | ✅ Available |
Workaround for default card: Track the default card ID in your application database. See the "Saved Card Service" section for implementation details.
Split Payments (Marketplace)
| Feature | Stripe | MercadoPago | |---------|--------|-------------| | Split payments | ✅ Stripe Connect | ⚠️ v2 (prepared) | | Application fees | ✅ Yes | ⚠️ v2 (prepared) | | Direct charges | ✅ Yes | ⚠️ v2 (prepared) | | Destination charges | ✅ Yes | ⚠️ v2 (prepared) |
Note: Split payments are prepared in the adapter but not yet implemented. Use the MercadoPago API directly for now:
// For split payments, use MercadoPago SDK directly
// See: https://www.mercadopago.com.ar/developers/en/docs/split-paymentsSubscription Features
| Feature | Stripe | MercadoPago | |---------|--------|-------------| | Trial periods | ✅ Native | ✅ Via preapproval | | Proration | ✅ Automatic | ❌ Manual handling | | Pause/resume | ✅ Native | ⚠️ Via status update | | Multiple items | ✅ Yes | ❌ One price per subscription | | Quantity changes | ✅ Yes | ❌ Not supported | | Usage-based billing | ✅ Metered | ❌ Not supported |
Workaround for proration: Calculate prorated amounts manually and apply as credits or adjustments.
Webhook/IPN Differences
| Feature | Stripe | MercadoPago | |---------|--------|-------------| | Signature algorithm | HMAC-SHA256 | HMAC-SHA256 | | Timestamp validation | ✅ 5 min tolerance | ⚠️ Manual | | Retry mechanism | ✅ Automatic | ✅ Automatic | | Event types | ~100+ types | ~20 types | | Test mode events | ✅ Yes | ✅ Yes |
Payment Methods
| Feature | Stripe | MercadoPago | |---------|--------|-------------| | Credit cards | ✅ Global | ✅ Regional (LATAM) | | Debit cards | ✅ Yes | ✅ Yes | | Bank transfers | ✅ ACH, SEPA | ✅ Regional | | PIX (Brazil) | ❌ No | ✅ Yes | | Boleto (Brazil) | ❌ No | ✅ Yes | | PSE (Colombia) | ❌ No | ✅ Yes | | OXXO (Mexico) | ❌ No | ✅ Yes |
Currency Support
| Feature | Stripe | MercadoPago | |---------|--------|-------------| | Multi-currency | ✅ 135+ currencies | ⚠️ Regional currencies | | Currency conversion | ✅ Automatic | ❌ Local only |
Supported MercadoPago currencies: ARS, BRL, CLP, COP, MXN, PEN, UYU, VES
API Rate Limits
| Provider | Limits | |----------|--------| | Stripe | 100 req/sec per key (live), 25 req/sec (test) | | MercadoPago | Not publicly documented, be conservative |
Refund Policies
| Feature | Stripe | MercadoPago | |---------|--------|-------------| | Partial refunds | ✅ Yes | ✅ Yes | | Multiple refunds | ✅ Yes | ✅ Yes | | Refund window | 90 days | Varies by payment method | | Refund to different card | ❌ No | ❌ No |
Migration from Stripe
If you're considering migrating from Stripe to MercadoPago (or supporting both), here are key considerations:
1. Customer Migration
// Stripe customer
const stripeCustomer = await stripe.customers.retrieve('cus_xxx');
// Create in MercadoPago (without metadata)
const mpCustomer = await mpAdapter.customers.create({
email: stripeCustomer.email,
name: stripeCustomer.name,
externalId: `migrated_${stripeCustomer.id}`
});
// Store mapping in your database
await db.insert(customerMappings).values({
stripeId: stripeCustomer.id,
mercadoPagoId: mpCustomer,
metadata: stripeCustomer.metadata // Store locally
});2. Subscription Migration
// Subscriptions need to be recreated
// MercadoPago preapprovals are different from Stripe subscriptions
const subscription = await mpAdapter.subscriptions.create(mpCustomerId, {
priceId: yourPriceMapping[stripeSubscription.items[0].price.id]
});
// Handle existing billing cycle manually3. Webhook Mapping
| Stripe Event | MercadoPago Event |
|--------------|-------------------|
| payment_intent.succeeded | payment.updated (status=approved) |
| payment_intent.payment_failed | payment.updated (status=rejected) |
| customer.subscription.created | subscription_preapproval.created |
| customer.subscription.updated | subscription_preapproval.updated |
| invoice.paid | subscription_authorized_payment.created |
Best Practices
1. Always Store Provider IDs
// Store both internal and provider IDs
const customer = await billing.customers.create({
email: '[email protected]',
externalId: 'your_user_123'
});
// customer.providerCustomerId contains MercadoPago ID
await db.update(users)
.set({ mpCustomerId: customer.providerCustomerId })
.where(eq(users.id, 'your_user_123'));2. Handle 3DS Properly
// Always check for 3DS requirement in payment flow
const payment = await mpAdapter.payments.create(customerId, options);
if (payment.status === 'pending' && payment.statusDetail === 'pending_challenge') {
// Return challenge URL to frontend
return { status: 'requires_action', challengeUrl: payment.initPoint };
}3. Implement Idempotency
// Use external_reference for idempotency
const payment = await mpAdapter.payments.create(customerId, {
amount: 10000,
currency: 'ARS',
metadata: {
orderId: 'order_123',
idempotencyKey: `payment_${orderId}_${Date.now()}`
}
});Error Handling
All MercadoPago adapters now include comprehensive error handling:
import { QZPayMercadoPagoError, QZPayErrorCode } from '@qazuor/qzpay-mercadopago';
try {
const payment = await mpAdapter.payments.create(customerId, {
amount: 10000,
currency: 'ARS',
paymentMethodId: 'visa'
});
} catch (error) {
if (error instanceof QZPayMercadoPagoError) {
console.log('Error code:', error.code);
console.log('Error message:', error.message);
console.log('Status detail:', error.statusDetail);
console.log('Original error:', error.originalError);
switch (error.code) {
case QZPayErrorCode.CARD_DECLINED:
// Handle card declined
// Check error.statusDetail for specific reason
break;
case QZPayErrorCode.INSUFFICIENT_FUNDS:
// Handle insufficient funds
break;
case QZPayErrorCode.INVALID_CARD:
// Handle invalid card details
break;
case QZPayErrorCode.AUTHENTICATION_ERROR:
// Handle API authentication issues
break;
case QZPayErrorCode.RATE_LIMIT_ERROR:
// Implement backoff strategy
break;
case QZPayErrorCode.RESOURCE_NOT_FOUND:
// Handle not found errors
break;
default:
// Handle other provider errors
}
}
}Error Mapping
The adapter maps MercadoPago errors to standardized QZPay error codes:
// MercadoPago status_detail -> QZPayErrorCode
const errorMap = {
'cc_rejected_insufficient_amount': 'insufficient_funds',
'cc_rejected_bad_filled_card_number': 'invalid_card',
'cc_rejected_bad_filled_security_code': 'invalid_card',
'cc_rejected_blacklist': 'card_declined',
'cc_rejected_high_risk': 'card_declined',
'cc_rejected_duplicated_payment': 'duplicate_transaction',
// ... and many more
};
// All adapters wrap operations with error mapping
try {
return await operation();
} catch (error) {
throw mapMercadoPagoError(error, 'CustomerAdapter.create');
}Fixed Issues
- Payer Email in Subscriptions: Now properly fetches and includes customer email when creating preapprovals.
- Checkout Prices: Fixed unit_price and currency_id extraction from price objects.
- Try/Catch Coverage: All adapter methods now have proper error handling.
Common Error Codes
| Code | Description | Action |
|------|-------------|--------|
| cc_rejected_insufficient_amount | Insufficient funds | Ask for different card |
| cc_rejected_bad_filled_security_code | Invalid CVV | Re-enter card details |
| cc_rejected_card_expired | Card expired | Use different card |
| cc_rejected_blacklist | Card blocked | Contact issuer |
| cc_rejected_other_reason | Generic rejection | Try different payment method |
v2 Features (Coming Soon)
- Split Payments: Full marketplace support
- Application Fees: Platform commission handling
- Multi-Seller: Advanced marketplace features
- Enhanced Webhooks: More event types
License
MIT
