Skip to main content

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

userToken
string
Firebase authentication token (alternative to context.auth)
items
array
required
Array of cart items
shippingCost
number
default:"0"
Shipping cost in COP
buyerInfo
object
required
Customer information
extraData
object
Additional order data

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

preferenceId
string
MercadoPago preference ID
initPoint
string
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:

1. Extract Payment ID

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

StatusDescriptionOrder Action
approvedPayment approvedSet to PAGADO, deduct stock
pendingPayment in processKeep as PENDIENTE_PAGO
rejectedPayment rejectedSet to RECHAZADO
cancelledPayment cancelled by userSet to RECHAZADO
refundedPayment refundedManual handling required
charged_backChargeback filedManual 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

  1. Create a test account at https://www.mercadopago.com.co/developers
  2. Get test access token
  3. Set MP_ACCESS_TOKEN to test token
  4. 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 variables
firebase 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

Build docs developers (and LLMs) love