Skip to main content

Endpoint

POST /functions/v1/mercadopago-payment

Overview

The mercadopago-payment edge function handles two operations:
  1. Create Payment Preference - Generate Mercado Pago checkout link
  2. Webhook Handler - Process payment notifications and credit user accounts
This function is used exclusively for the B2C model (direct user purchases).

Create Payment Preference

Request

Headers

Authorization
string
required
Bearer token with Supabase anon key
Content-Type
string
required
Must be application/json

Body Parameters

user_id
uuid
required
User ID purchasing credits
credits
integer
required
Number of credits to purchaseExample: 500
price
number
required
Price in ARS (Argentine Pesos)Example: 2500.00
pack_name
string
required
Display name for the credit packExample: "Pack Premium"
redirect_url
string
default:"https://metalab30.com/cabina/"
URL to redirect after payment completion

Response

Success

{
  "id": "1234567890-abcd-1234-abcd-1234567890ab",
  "init_point": "https://www.mercadopago.com.ar/checkout/v1/redirect?pref_id=1234567890-abcd",
  "sandbox_init_point": "https://sandbox.mercadopago.com.ar/checkout/v1/redirect?pref_id=test-1234"
}
id
string
Mercado Pago preference ID
init_point
string
Production checkout URL - redirect user here
sandbox_init_point
string
Test checkout URL (for sandbox testing)

Error

{
  "error": true,
  "message": "invalid_client_id",
  "code": "MP_ERROR",
  "details": { ... }
}

Code Example

import { supabase } from './supabaseClient';

const createPayment = async (
  userId: string,
  credits: number,
  price: number,
  packName: string
) => {
  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
      }
    }
  );

  if (error || data.error) {
    throw new Error(data?.message || 'Payment creation failed');
  }

  // Redirect user to Mercado Pago checkout
  window.location.href = data.init_point;
};

// Usage
await createPayment(
  user.id,
  500,
  2500.00,
  'Pack Premium'
);

Webhook Handler

Webhook URL

Configure this URL in Mercado Pago dashboard:
https://<project-ref>.supabase.co/functions/v1/mercadopago-payment?webhook=true
Example:
https://elesttjfwfhvzdvldytn.supabase.co/functions/v1/mercadopago-payment?webhook=true

Request (from Mercado Pago)

Mercado Pago sends POST notifications when payment status changes.

Body Structure

{
  "type": "payment",
  "data": {
    "id": "1234567890"
  }
}
type
string
Notification typeValues: payment, merchant_order, pay
data.id
string
Payment ID to fetch details

Response

{
  "received": true
}
Always return 200 OK to acknowledge webhook receipt, even if processing fails.

Implementation Details

Creating Preference

The function builds a Mercado Pago preference object:
const preference = {
  items: [{
    title: `Pack ${pack_name} - ${credits} Créditos`,
    unit_price: Number(price),
    quantity: 1,
    currency_id: "ARS"
  }],
  external_reference: `${user_id}:${credits}`,
  back_urls: {
    success: redirect_url,
    failure: redirect_url,
    pending: redirect_url
  },
  auto_return: "approved",
  notification_url: `https://${host}/functions/v1/mercadopago-payment?webhook=true`
};

const res = 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

External Reference Format

The external_reference field stores metadata:
<user_id>:<credits>
Example:
550e8400-e29b-41d4-a716-446655440000:500
This is parsed in the webhook to credit the correct user.

Webhook Processing

When a payment is approved:
if (paymentData.status === "approved") {
  const [userId, creditsToAdd] = paymentData.external_reference.split(":");
  const creditsNum = parseInt(creditsToAdd);

  // Check if already processed (idempotency)
  const { data: existing } = await supabase
    .from("payment_notifications")
    .select("id")
    .eq("mercadopago_id", id.toString())
    .maybeSingle();

  if (!existing) {
    // Record notification
    await supabase.from("payment_notifications").insert({
      mercadopago_id: id.toString(),
      data: paymentData,
      status: "approved",
      user_id: userId,
      credits_added: creditsNum,
      amount: paymentData.transaction_amount
    });

    // Add credits to user
    const { data: profile } = await supabase
      .from("profiles")
      .select("credits")
      .eq("id", userId)
      .single();

    await supabase
      .from("profiles")
      .update({ credits: (profile.credits || 0) + creditsNum })
      .eq("id", userId);
  }
}
From source: supabase/functions/mercadopago-payment/index.ts:95-128

Idempotency

Webhooks may be sent multiple times. The function checks payment_notifications table to prevent duplicate credits:
const { data: existing } = await supabase
  .from("payment_notifications")
  .select("id")
  .eq("mercadopago_id", id.toString())
  .maybeSingle();

if (!existing) {
  // Process payment
}

Mercado Pago Setup

1. Get Credentials

  1. Go to Mercado Pago Developers
  2. Navigate to Your integrationsCredentials
  3. Copy Access Token (Production or Test)

2. Configure Webhook

  1. Go to Your integrationsWebhooks
  2. Click Add webhook
  3. Set URL: https://<project-ref>.supabase.co/functions/v1/mercadopago-payment?webhook=true
  4. Select events: payment.created, payment.updated
  5. Save

3. Set Edge Function Secret

supabase secrets set MP_ACCESS_TOKEN=APP_USR-your-access-token-here

Testing

Test Mode

Use Mercado Pago test credentials:
  1. In Mercado Pago dashboard, switch to Test mode
  2. Use test access token in MP_ACCESS_TOKEN
  3. Use test cards from Mercado Pago docs
Test card (always approved):
Card: 5031 7557 3453 0604
Expiry: 11/25
CVV: 123
Name: APRO

Local Testing

# Serve function locally
supabase functions serve mercadopago-payment --env-file .env.local

# Test preference creation
curl -i --location --request POST 'http://localhost:54321/functions/v1/mercadopago-payment' \
  --header 'Authorization: Bearer <ANON_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "user_id": "550e8400-e29b-41d4-a716-446655440000",
    "credits": 500,
    "price": 2500,
    "pack_name": "Test Pack"
  }'

Webhook Testing

Use ngrok to expose local function:
# Start local functions
supabase functions serve

# In another terminal, expose port
ngrok http 54321

# Use ngrok URL in Mercado Pago webhook config
https://abc123.ngrok.io/functions/v1/mercadopago-payment?webhook=true

Environment Variables

SUPABASE_URL
string
required
Auto-injected by Supabase
SUPABASE_SERVICE_ROLE_KEY
string
required
Auto-injected by Supabase
MP_ACCESS_TOKEN
string
required
Mercado Pago access tokenTest: TEST-1234...
Production: APP_USR-1234...
Set via: supabase secrets set MP_ACCESS_TOKEN=your-token

Error Handling

invalid_client_id
string
MP_ACCESS_TOKEN is invalid or expiredSolution: Regenerate token in Mercado Pago dashboard
invalid_parameter
string
Price or credits value is invalidSolution: Ensure price is positive number, credits is integer
unauthorized
string
Access token doesn’t have required permissionsSolution: Use access token from “Credentials” page, not client ID

Security Considerations

Validate webhook origin

Optionally verify webhook IP is from Mercado Pago

Idempotency is critical

Always check payment_notifications before crediting

Never trust external_reference

Validate user_id exists before crediting

Log all webhooks

Store full payload for debugging and auditing

Next Steps

Mercado Pago Integration

Complete integration guide

Edge Functions

General edge function docs

Build docs developers (and LLMs) love