Overview
MercadoPago is Latin America’s leading payment platform, supporting credit/debit cards, digital wallets, and local payment methods. This integration uses the official MercadoPago SDK for Node.js.
Prerequisites
Required Credentials
MercadoPago account
Access Token (production or sandbox)
Webhook URL configured in MercadoPago dashboard
Configuration
Environment Variables
Add these to your Firebase Functions configuration:
MP_ACCESS_TOKEN = your_mercadopago_access_token
Code Configuration
In ~/workspace/source/functions/mercadopago.js:5-11:
const { MercadoPagoConfig , Preference , Payment } = require ( "mercadopago" );
const MP_TOKEN = process . env . MP_ACCESS_TOKEN ;
const client = MP_TOKEN ? new MercadoPagoConfig ({
accessToken: MP_TOKEN
}) : null ;
const WEBHOOK_URL = "https://us-central1-pixeltechcol.cloudfunctions.net/mercadoPagoWebhook" ;
Creating Payment Preferences
Function: createPreference
Creates a MercadoPago checkout preference with 30-minute expiration.
Parameters
Firebase authentication token (alternative to context.auth)
Array of cart items Product ID from Firestore
Customer information Phone number with area code
Additional order data Show Extra data properties
Detailed shipping information
Billing information if different
Example Request
const createPreference = firebase . functions (). httpsCallable ( 'createPreference' );
const result = await createPreference ({
userToken: await firebase . auth (). currentUser . getIdToken (),
items: [
{
id: 'prod_123' ,
quantity: 2 ,
color: 'Negro' ,
capacity: '128GB'
}
],
shippingCost: 15000 ,
buyerInfo: {
name: 'Juan Pérez' ,
phone: '3001234567' ,
address: 'Calle 123 #45-67' ,
postal: '110111'
},
extraData: {
clientDoc: '1234567890' ,
needsInvoice: false ,
shippingData: {
address: 'Calle 123 #45-67' ,
city: 'Bogotá' ,
department: 'Cundinamarca'
}
}
});
console . log ( result . data );
// { preferenceId: 'xxxxx', initPoint: 'https://www.mercadopago.com.co/checkout/v1/redirect?pref_id=xxxxx' }
Response
MercadoPago preference ID
Checkout URL to redirect the customer
Implementation Flow
From ~/workspace/source/functions/mercadopago.js:18-165:
1. Authentication
const userToken = data . userToken ;
let uid , email ;
if ( userToken ) {
const decodedToken = await auth . verifyIdToken ( userToken );
uid = decodedToken . uid ;
email = decodedToken . email ;
} else if ( context . auth ) {
uid = context . auth . uid ;
email = context . auth . token . email ;
} else {
throw new Error ( "Sin credenciales." );
}
2. Price Validation
Always validate prices from the database:
for ( const item of rawItems ) {
const pDoc = await db . collection ( 'products' ). doc ( item . id ). get ();
if ( ! pDoc . exists ) continue ;
const pData = pDoc . data ();
const realPrice = Number ( pData . price ) || 0 ; // Server-side price
const quantity = parseInt ( item . quantity ) || 1 ;
subtotal += realPrice * quantity ;
mpItems . push ({
id: item . id ,
title: pData . name ,
quantity: quantity ,
unit_price: realPrice ,
currency_id: 'COP' ,
picture_url: pData . mainImage || ''
});
}
3. Create Order in Firestore
const newOrderRef = db . collection ( 'orders' ). doc ();
await newOrderRef . set ({
source: 'TIENDA_WEB' ,
createdAt: admin . firestore . FieldValue . serverTimestamp (),
userId: uid ,
userEmail: email ,
userName: extraData . userName || buyerInfo . name ,
phone: extraData . phone || buyerInfo . phone || "" ,
clientDoc: extraData . clientDoc || "" ,
shippingData: shippingData ,
items: dbItems ,
subtotal: subtotal ,
shippingCost: shippingCost ,
total: totalAmount ,
status: 'PENDIENTE_PAGO' ,
paymentMethod: 'MERCADOPAGO' ,
paymentStatus: 'PENDING' ,
isStockDeducted: false
});
4. Set Expiration (30 Minutes)
const expirationDate = new Date ();
expirationDate . setMinutes ( expirationDate . getMinutes () + 30 );
5. Create MercadoPago Preference
const preference = new Preference ( client );
const result = await preference . create ({
body: {
items: mpItems ,
payer: {
name: buyerInfo . name ,
email: email ,
phone: { area_code: "57" , number: buyerInfo . phone },
address: { street_name: buyerInfo . address , zip_code: buyerInfo . postal }
},
back_urls: {
success: "https://pixeltechcol.com/shop/success.html" ,
failure: "https://pixeltechcol.com/shop/success.html" ,
pending: "https://pixeltechcol.com/shop/success.html"
},
auto_return: "approved" ,
statement_descriptor: "PIXELTECH" ,
external_reference: newOrderRef . id ,
notification_url: WEBHOOK_URL ,
date_of_expiration: expirationDate . toISOString ()
}
});
return { preferenceId: result . id , initPoint: result . init_point };
Webhook Handling
Function: webhook
Processes payment notifications from MercadoPago.
Webhook URL Setup
Configure in MercadoPago dashboard:
https://us-central1-pixeltechcol.cloudfunctions.net/mercadoPagoWebhook
Webhook Flow
From ~/workspace/source/functions/mercadopago.js:174-326:
const paymentId = req . query . id ||
req . query [ 'data.id' ] ||
req . body ?. data ?. id ||
req . body ?. id ;
const topic = req . query . topic || req . body ?. topic ;
// Ignore merchant_order notifications
if ( topic === 'merchant_order' ) return res . status ( 200 ). send ( "OK" );
if ( ! paymentId ) return res . status ( 200 ). send ( "OK" );
2. Verify Payment Status
const payment = new Payment ( client );
const paymentData = await payment . get ({ id: paymentId });
const status = paymentData . status ; // approved, rejected, cancelled, pending
const orderId = paymentData . external_reference ;
3. Process Approved Payments
if ( status === 'approved' ) {
await db . runTransaction ( async ( t ) => {
const docSnap = await t . get ( orderRef );
// Prevent duplicate processing
if ( ! docSnap . exists || docSnap . data (). status === 'PAGADO' ) return ;
const oData = docSnap . data ();
// 1. Deduct inventory
if ( ! oData . isStockDeducted ) {
for ( const item of oData . items ) {
const pRef = db . collection ( 'products' ). doc ( item . id );
const pDoc = await t . get ( pRef );
if ( pDoc . exists ) {
const pData = pDoc . data ();
let newStock = ( pData . stock || 0 ) - ( item . quantity || 1 );
let combinations = pData . combinations || [];
// Handle variants
if ( item . color || item . capacity ) {
const idx = combinations . findIndex ( c =>
( c . color === item . color || ( ! c . color && ! item . color )) &&
( c . capacity === item . capacity || ( ! c . capacity && ! item . capacity ))
);
if ( idx >= 0 ) {
combinations [ idx ]. stock = Math . max ( 0 , combinations [ idx ]. stock - item . quantity );
}
}
t . update ( pRef , {
stock: Math . max ( 0 , newStock ),
combinations: combinations
});
}
}
}
// 2. Update treasury
const accQ = await t . get (
db . collection ( 'accounts' )
. where ( 'gatewayLink' , '==' , 'MERCADOPAGO' )
. limit ( 1 )
);
let accDoc = accQ . empty ? null : accQ . docs [ 0 ];
if ( accDoc ) {
// Update balance
t . update ( accDoc . ref , {
balance: ( Number ( accDoc . data (). balance ) || 0 ) + Number ( oData . total )
});
// Create income record
const incRef = db . collection ( 'expenses' ). doc ();
t . set ( incRef , {
amount: Number ( oData . total ),
category: "Ingreso Ventas Online" ,
description: `Venta MP # ${ orderId . slice ( 0 , 8 ) } ` ,
paymentMethod: accDoc . data (). name ,
date: admin . firestore . FieldValue . serverTimestamp (),
type: 'INCOME' ,
orderId: orderId ,
supplierName: oData . userName
});
}
// 3. Create remission
const remRef = db . collection ( 'remissions' ). doc ( orderId );
t . set ( remRef , {
orderId ,
source: 'WEBHOOK_MP' ,
items: oData . items ,
clientName: oData . userName ,
clientPhone: oData . phone ,
clientDoc: oData . clientDoc ,
clientAddress: ` ${ oData . shippingData ?. address } , ${ oData . shippingData ?. city } ` ,
total: oData . total ,
status: 'PENDIENTE_ALISTAMIENTO' ,
type: 'VENTA_WEB' ,
createdAt: admin . firestore . FieldValue . serverTimestamp ()
});
// 4. Update order
t . update ( orderRef , {
status: 'PAGADO' ,
paymentStatus: 'PAID' ,
paymentId: paymentId ,
updatedAt: admin . firestore . FieldValue . serverTimestamp (),
isStockDeducted: true
});
});
}
4. Handle Rejected Payments
else if ( status === 'rejected' || status === 'cancelled' ) {
await orderRef . update ({
status: 'RECHAZADO' ,
paymentId: paymentId ,
statusDetail: paymentData . status_detail ,
updatedAt: admin . firestore . FieldValue . serverTimestamp ()
});
}
Payment Status Flow
MercadoPago Status Codes
Payment Statuses
Status Description Order Action approvedPayment approved Set to PAGADO, deduct stock pendingPayment in process Keep as PENDIENTE_PAGO rejectedPayment rejected Set to RECHAZADO cancelledPayment cancelled by user Set to RECHAZADO refundedPayment refunded Manual handling required charged_backChargeback filed Manual handling required
Status Details
Common status_detail values:
accredited - Payment credited
cc_rejected_bad_filled_card_number - Invalid card number
cc_rejected_bad_filled_date - Invalid expiration date
cc_rejected_bad_filled_security_code - Invalid security code
cc_rejected_insufficient_amount - Insufficient funds
cc_rejected_high_risk - Rejected for risk
Testing
Sandbox Configuration
Create a test account at https://www.mercadopago.com.co/developers
Get test access token
Set MP_ACCESS_TOKEN to test token
Use test cards from MercadoPago docs
Test Cards
Approved:
Card: 5031 7557 3453 0604
CVV: 123
Expiry: 11/25
Rejected (insufficient funds):
Card: 5031 4332 1540 6351
CVV: 123
Expiry: 11/25
Test Webhook
Test webhook locally using ngrok:
ngrok http 5001
# Update WEBHOOK_URL to ngrok URL
Treasury Configuration
Create a treasury account linked to MercadoPago:
// In Firestore: accounts collection
{
name : "MercadoPago" ,
gatewayLink : "MERCADOPAGO" ,
balance : 0 ,
isDefaultOnline : true , // Optional fallback
type : "ONLINE_PAYMENT"
}
Error Handling
Common Errors
Cause: MP_ACCESS_TOKEN not setSolution: Add token to environment variablesfirebase functions:config:set mercadopago.token="YOUR_TOKEN"
Cause: User not authenticatedSolution: Ensure userToken is passed or user is logged in via Firebase Auth
Cause: No items in cartSolution: Validate cart has items before calling function
Cause: Slow Firestore operationsSolution: MercadoPago will retry. Webhook should be idempotent.
Best Practices
Always validate prices server-side - Never trust client-submitted prices
Use transactions - Prevent race conditions in stock updates
Set expiration times - 30 minutes is recommended
Log all events - Helps with debugging and reconciliation
Handle all statuses - Including pending, refunded, and charged_back
Monitoring
Key Metrics to Track
Payment creation success rate
Webhook processing time
Failed payment reasons
Stock deduction accuracy
Treasury balance reconciliation
Logging
All operations are logged:
console . log ( "🚀 Iniciando Checkout MP..." );
console . log ( "✅ MP Order Approved:" , orderId );
console . log ( "❌ MP Order Rejected:" , orderId );
console . error ( "❌ Error MP Create:" , error );
Next Steps
Add ADDI Add buy now, pay later option
Configure Treasury Set up payment accounts
Webhook Guide Learn more about webhooks
Order Management Manage incoming orders