Overview
Cabina integrates Mercado Pago for credit purchases in the B2C model. The integration uses:
Checkout Preferences - Hosted checkout page
Webhooks - Automatic credit fulfillment
Edge Function - Server-side payment processing
This integration is used exclusively for the B2C (public app) model. B2B partner purchases are handled separately.
Architecture
Setup
1. Create Mercado Pago Account
Create application
Navigate to Your integrations
Click Create application
Fill in app details
Save application
Get credentials
Go to Credentials and copy:
Test Access Token (for development)
Production Access Token (for production)
Format: APP_USR-1234567890123456-123456-abcdef...
# Development/Test
supabase secrets set MP_ACCESS_TOKEN=TEST-1234567890-123456-abcdef
# Production
supabase secrets set MP_ACCESS_TOKEN=APP_USR-1234567890-123456-abcdef
3. Set Up Webhook
Get webhook URL
Your webhook URL: https://<project-ref>.supabase.co/functions/v1/mercadopago-payment?webhook=true
Example: https://elesttjfwfhvzdvldytn.supabase.co/functions/v1/mercadopago-payment?webhook=true
Configure in Mercado Pago
Go to Your integrations → Webhooks
Click Configure webhooks
Enter your webhook URL
Select events:
payment.created
payment.updated
Save
Test webhook
Mercado Pago will send a test notification. Check Edge Function logs: supabase functions logs mercadopago-payment --tail
Implementation
Frontend: Create Payment
import { supabase } from '../lib/supabaseClient' ;
const handlePurchase = async (
userId : string ,
packName : string ,
credits : number ,
price : number
) => {
try {
// Create payment preference
const { data , error } = await supabase . functions . invoke (
'mercadopago-payment' ,
{
body: {
user_id: userId ,
credits: credits ,
price: price ,
pack_name: packName ,
redirect_url: window . location . origin + '/success'
}
}
);
if ( error || data . error ) {
throw new Error ( data ?. message || 'Error creating payment' );
}
// Redirect to Mercado Pago checkout
window . location . href = data . init_point ;
} catch ( error ) {
console . error ( 'Payment error:' , error );
alert ( 'Error al procesar el pago' );
}
};
// Usage in component
< button onClick = {() => handlePurchase (
user.id,
'Pack Premium' ,
500,
2500.00
)} >
Comprar 500 Créditos - $2 , 500
</ button >
Backend: Edge Function
See full implementation in mercadopago-payment API reference .
Key parts:
supabase/functions/mercadopago-payment/index.ts
// Create preference
const preference = {
items: [{
title: `Pack ${ pack_name } - ${ credits } Créditos` ,
unit_price: Number ( price ),
quantity: 1 ,
currency_id: "ARS"
}],
external_reference: ` ${ user_id } : ${ credits } ` ,
notification_url: webhookUrl
};
const response = await fetch (
'https://api.mercadopago.com/checkout/preferences' ,
{
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ MP_ACCESS_TOKEN } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ( preference )
}
);
From source: supabase/functions/mercadopago-payment/index.ts:35-59
Webhook Handler
supabase/functions/mercadopago-payment/index.ts
// Fetch payment details
const mpRes = await fetch (
`https://api.mercadopago.com/v1/payments/ ${ paymentId } ` ,
{
headers: { 'Authorization' : `Bearer ${ MP_ACCESS_TOKEN } ` }
}
);
const paymentData = await mpRes . json ();
if ( paymentData . status === 'approved' ) {
// Parse external_reference
const [ userId , creditsToAdd ] = paymentData . external_reference . split ( ':' );
// Check idempotency
const existing = await checkIfProcessed ( paymentId );
if ( ! existing ) {
// Record payment
await recordPayment ( paymentId , paymentData );
// Add credits to user
await addCreditsToUser ( userId , parseInt ( creditsToAdd ));
}
}
From source: supabase/functions/mercadopago-payment/index.ts:87-128
Credit Packs Configuration
Define your credit packs in the frontend:
export const CREDIT_PACKS = [
{
name: 'Starter' ,
credits: 100 ,
price: 500 ,
currency: 'ARS' ,
popular: false
},
{
name: 'Premium' ,
credits: 500 ,
price: 2000 ,
currency: 'ARS' ,
popular: true ,
discount: 20 // 20% off
},
{
name: 'Pro' ,
credits: 1000 ,
price: 3500 ,
currency: 'ARS' ,
popular: false ,
discount: 30
}
];
Testing
Test Cards
Mercado Pago provides test cards for different scenarios:
Approved payment:
Card Number: 5031 7557 3453 0604
Expiry: 11/25
CVV: 123
Name: APRO
Rejected payment:
Card Number: 5031 7557 3453 0604
Expiry: 11/25
CVV: 123
Name: OTROC
Pending payment:
Card Number: 5031 7557 3453 0604
Expiry: 11/25
CVV: 123
Name: CONT
Complete test card list
Test Workflow
Use test credentials
Set MP_ACCESS_TOKEN to your Test access token
Create test payment
await handlePurchase ( testUserId , 'Test Pack' , 100 , 1 );
Complete checkout
Use test card from above
Fill in any name and email
Complete payment
Verify webhook
Check function logs: supabase functions logs mercadopago-payment
Should see: Webhook received body: {"type":"payment","data":{"id":"123"}}
Processing payment with ID: 123
Payment Data from MP: {"status":"approved",...}
Verify credits added
SELECT credits FROM profiles WHERE id = '<test-user-id>' ;
Local Testing with ngrok
To test webhooks locally:
# Terminal 1: Start Supabase functions
supabase functions serve
# Terminal 2: Expose with ngrok
ngrok http 54321
# Use ngrok URL in Mercado Pago webhook config
https://abc123.ngrok.io/functions/v1/mercadopago-payment?webhook = true
Error Handling
Common Errors
Invalid client_id or client_secret
Cause: MP_ACCESS_TOKEN is incorrect or expiredSolution:
Regenerate token in Mercado Pago dashboard
Update edge function secret
Ensure you’re using Access Token, not Client ID
Possible causes:
Wrong webhook URL configured
Edge function not deployed
Firewall blocking Mercado Pago
Debug: # Check function is accessible
curl https://your-project.supabase.co/functions/v1/mercadopago-payment?webhook= true
# Should return 404 (not 403)
Credits not added after payment
Check:
Webhook received? (check logs)
Payment approved? (check Mercado Pago dashboard)
Already processed? (check payment_notifications table)
Manual fix: UPDATE profiles
SET credits = credits + 500
WHERE id = '<user-id>' ;
Cause: Webhook sent multiple times, idempotency check failedPrevention:
The function checks payment_notifications.mercadopago_id before processingFix: -- Check duplicates
SELECT mercadopago_id, COUNT ( * )
FROM payment_notifications
GROUP BY mercadopago_id
HAVING COUNT ( * ) > 1 ;
-- Remove excess credits manually
Production Checklist
Use production credentials
Configure production webhook
Security Best Practices
Never expose access token Always use edge functions, never client-side
Validate webhook origin Optionally verify requests come from Mercado Pago IPs
Implement idempotency Always check payment_notifications before crediting
Log all transactions Store full webhook payload for auditing
Monitoring & Analytics
Track Purchases
-- Daily revenue
SELECT
DATE (created_at) as date ,
COUNT ( * ) as purchases,
SUM (amount) as revenue,
SUM (credits_added) as credits_sold
FROM payment_notifications
WHERE status = 'approved'
GROUP BY DATE (created_at)
ORDER BY date DESC ;
-- Top buyers
SELECT
p . email ,
COUNT ( * ) as purchases,
SUM ( pn . credits_added ) as total_credits
FROM payment_notifications pn
JOIN profiles p ON p . id = pn . user_id
WHERE pn . status = 'approved'
GROUP BY p . email
ORDER BY purchases DESC
LIMIT 10 ;
Mercado Pago Dashboard
Monitor in Mercado Pago Dashboard :
Total sales
Approval rate
Chargebacks
Failed payments
Advanced Features
Installment Payments
Enable installments in preference:
const preference = {
// ... other fields
installments: 3 , // Max installments
payment_methods: {
installments: 3
}
};
Discounts and Coupons
const preference = {
// ... other fields
marketplace_fee: 0 ,
differential_pricing: {
id: 123 // Discount ID from MP
}
};
Split Payments (Marketplace)
For revenue sharing with partners:
const preference = {
// ... other fields
marketplace_fee: 100 , // Your commission in ARS
marketplace: 'YOUR_MARKETPLACE_ID'
};
Next Steps
Payment Webhook API Complete API reference
Edge Functions Learn about edge functions