npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

convex-mercadopago

v0.0.4

Published

A Mercado Pago component for Convex.

Downloads

673

Readme

Convex Mercado Pago Component

[!WARNING] This component is under active development and has not reached a stable release. APIs may change without notice between versions, it is not an official @convex-dev component, and it has not yet been battle-tested in production. Pin an exact version, review the CHANGELOG before upgrading, and use at your own risk.

Add Mercado Pago payments and subscriptions to your Convex app: Checkout Pro, Checkout API card payments via Bricks tokens, subscriptions (preapproval), refunds, chargebacks, and a webhook ingestion pipeline with signature verification, deduplication, and pull reconciliation.

Check out the example app for a complete example.

// Create a Checkout Pro preference and redirect the buyer
const { preference } = await mercadopago.createPreference(ctx, {
  items: [{ title: "Pro plan", quantity: 1, unit_price: 9990 }],
  external_reference: orderId,
  back_urls: { success: "https://example.com/success" },
});

// React: the payment status updates live when the webhook lands
const { payment, isLoading } = usePaymentStatus(api.mercadopago.getPayment, {
  paymentId,
});

Features

  • 💳 Checkout Pro & Checkout API - Redirect checkout via preferences, or card payments from Bricks tokens (never raw card data)
  • 🔁 Subscriptions - Preapprovals with or without a plan: create, pause, resume, cancel, and card updates
  • 💸 Refunds & chargebacks - Full/partial refunds and chargeback sync into payment state
  • 🔔 Webhook pipeline - x-signature HMAC verification, fingerprinted event ledger with deduplication, fast acknowledgement, fetch-and-apply
  • 🧭 Forward-only state machine - Out-of-order and duplicate webhooks can never regress a mirrored payment or subscription status
  • 🔄 Reconciliation - A cron-friendly action that reprocesses failed events, syncs recent resources, and refreshes stale payments through the same write path as webhooks
  • 🪝 Lifecycle hooks - onPaymentApproved, onSubscriptionCancelled, onChargeback, and friends fire exactly once per transition
  • 📊 Real-time data - Mirrored tables queried reactively from Convex, with thin React hooks

Prerequisites

Convex App

You'll need a Convex app to use the component. Follow any of the Convex quickstarts to set one up.

Mercado Pago Account

  • Create a Mercado Pago developer application
  • Copy the access token (TEST or production) from the application's credentials page
  • Configure Webhooks for the application and copy the generated secret key for signature validation

Installation

Install the component package:

npm install convex-mercadopago

Create a convex.config.ts file in your app's convex/ folder and install the component:

// convex/convex.config.ts
import { defineApp } from "convex/server";
import mercadopago from "convex-mercadopago/convex.config.js";

const app = defineApp();
app.use(mercadopago);

export default app;

Set your credentials as environment variables in your Convex deployment:

npx convex env set MERCADOPAGO_ACCESS_TOKEN TEST-...
npx convex env set MERCADOPAGO_WEBHOOK_SECRET ...

Usage

Initialize the client

Instantiate the MercadoPago client in your Convex backend. The identify callback is the single identity seam: it tells the component which app user owns each payment, and client-supplied user ids are never trusted.

// convex/mercadopago.ts
import { MercadoPago } from "convex-mercadopago";
import { api, components } from "./_generated/api";
import { query } from "./_generated/server";

export const identifyUser = query({
  args: {},
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Not authenticated");
    return { userId: identity.tokenIdentifier, email: identity.email };
  },
});

export const mercadopago = new MercadoPago(components.mercadopago, {
  identify: async (ctx) => await ctx.runQuery(api.mercadopago.identifyUser),
  events: {
    onPaymentApproved: async (ctx, payment) => {
      // Grant access, send a receipt, etc.
    },
    onSubscriptionCancelled: async (ctx, preapproval) => {
      // Revoke access.
    },
  },
});

Constructor options:

  • identify: (Optional) Function returning the current user's { userId, email }. Required for the api() factory and user-scoped reads
  • events: (Optional) Lifecycle hooks (see Webhooks)
  • accessToken: (Optional) Mercado Pago access token. Falls back to the MERCADOPAGO_ACCESS_TOKEN env var
  • webhookSecret: (Optional) Webhook secret key. Falls back to the MERCADOPAGO_WEBHOOK_SECRET env var
  • apiBaseUrl: (Optional) Override the https://api.mercadopago.com base URL
  • allowUnverified: (Optional) Accept webhooks without a configured secret. Defaults to false; do not enable in production

Set up webhooks

Mount the webhook route in convex/http.ts:

// convex/http.ts
import { httpRouter } from "convex/server";
import { mercadopago } from "./mercadopago";

const http = httpRouter();
mercadopago.registerRoutes(http);
export default http;

Then, in the Mercado Pago dashboard under Your integrations → Webhooks, point notifications at your Convex site URL:

https://<your-deployment>.convex.site/mercadopago/webhook

Subscribe at least to the payment, merchant_order, and subscription topics you use. The route path can be overridden with registerRoutes(http, { path: "/custom/path" }).

Checkout Pro

Create a preference and redirect the buyer to init_point (sandbox_init_point with TEST credentials):

// convex/mercadopago.ts
import { v } from "convex/values";
import { action } from "./_generated/server";

export const createCheckout = action({
  args: { title: v.string(), amount: v.number(), orderId: v.string() },
  handler: async (ctx, args) =>
    await mercadopago.createPreference(ctx, {
      items: [{ title: args.title, quantity: 1, unit_price: args.amount }],
      external_reference: args.orderId,
      back_urls: { success: "https://example.com/success" },
    }),
});

[!TIP] Always pass external_reference. It is the canonical join key between your app's orders and Mercado Pago resources, and it makes preference creation retry-idempotent — Mercado Pago does not honor X-Idempotency-Key on /checkout/preferences, so without it a crash retry can create duplicates.

Checkout API (card payments)

Tokenize the card in the browser with Bricks / the JS SDK — card numbers must never reach your backend — then create the payment with the token:

await mercadopago.createCardPayment(ctx, {
  token: cardToken,
  transaction_amount: 1000,
  installments: 1,
  payment_method_id: "visa",
  payer: { email },
});

Two-step authorization and capture:

await mercadopago.createCardPayment(ctx, { ...args, capture: false });
await mercadopago.capturePayment(ctx, { paymentId, amount: 1000 });
// or release the hold:
await mercadopago.cancelPayment(ctx, { paymentId });

Refunds

await mercadopago.refundPayment(ctx, { paymentId }); // full
await mercadopago.refundPayment(ctx, { paymentId, amount: 500 }); // partial

Distinct partial refunds get distinct idempotency keys automatically; retrying the same call reuses the same key. After a successful refund, the payment mirror is re-synced immediately when Mercado Pago can return the payment snapshot. Lifecycle hooks such as onPaymentRefunded still fire from webhook or reconciliation transitions. Partial refunds keep the payment approved until it is fully refunded, and some Mercado Pago sandbox accounts reject partial refunds with error 2084.

Subscriptions

Create a preapproval directly or from a plan. Without a card_token_id the subscription starts pending and the buyer authorizes it at init_point:

// Standalone subscription
await mercadopago.createPreapproval(ctx, {
  reason: "Monthly membership",
  external_reference: subscriptionId,
  payer_email: email,
  back_url: "https://example.com/subscriptions/return",
  auto_recurring: {
    frequency: 1,
    frequency_type: "months",
    transaction_amount: 1000,
    currency_id: "CLP",
  },
});

// From a plan
await mercadopago.createPreapproval(ctx, {
  preapproval_plan_id: planId,
  payer_email: email,
});

back_url is required when creating a preapproval without a plan.

Manage the lifecycle:

await mercadopago.pausePreapproval(ctx, { preapprovalId });
await mercadopago.resumePreapproval(ctx, { preapprovalId });
await mercadopago.cancelPreapproval(ctx, { preapprovalId });
await mercadopago.updatePreapprovalCard(ctx, { preapprovalId, cardTokenId });

Recurring charge attempts arrive as subscription_authorized_payment webhooks and are mirrored into the authorizedPayments table.

Query payment and subscription state

Mirrored state lives in component tables and is queried reactively — your UI updates live when a webhook lands:

export const getMyPayments = query({
  args: {},
  handler: async (ctx) => {
    const { userId } = await ctx.runQuery(api.mercadopago.identifyUser);
    return await mercadopago.listPaymentsByUser(ctx, { userId });
  },
});

const status = await mercadopago.getSubscriptionStatus(ctx, { userId });
// { active: true, source: "preapproval", preapprovalId: "..." }

getSubscriptionStatus counts only authorized preapprovals by default. Pass approvedPaymentWindowMs to also count recent approved subscription payments within a time window.

Ready-made host functions

api() returns the core methods pre-bound for re-export, with identity resolved exclusively through your identify callback:

export const {
  createPreference,
  createCardPayment,
  capturePayment,
  cancelPayment,
  refundPayment,
  createPreapproval,
  getPayment,
  getSubscriptionStatus,
  reconcile,
} = mercadopago.api();

API Reference

MercadoPago Client

Commands (call the Mercado Pago API, then apply the result to the mirror):

  • createPreference(ctx, args) - Create a Checkout Pro preference
  • createCardPayment(ctx, args) - Create a payment from a Bricks token; supports capture: false
  • capturePayment(ctx, { paymentId, amount?, idempotencyKey? }) - Capture an authorized payment
  • cancelPayment(ctx, { paymentId, idempotencyKey? }) - Cancel a payment
  • refundPayment(ctx, { paymentId, amount?, idempotencyKey? }) - Full or partial refund
  • createPreapproval(ctx, args) / cancelPreapproval / pausePreapproval / resumePreapproval / updatePreapprovalCard - Subscription lifecycle
  • createPlan(ctx, args) / updatePlan(ctx, { planId, ...args }) - Preapproval plan management
  • getOrCreateCustomer(ctx, { email, userId? }) - Find-by-email or create a Mercado Pago customer, mirror it, and sync its saved cards

Reads (reactive queries over mirrored tables):

  • getPayment(ctx, { paymentId }) / getPaymentByExternalReference(ctx, { externalReference }) / listPaymentsByUser(ctx, { userId })
  • getPreference(ctx, { preferenceId })
  • getPreapproval(ctx, { preapprovalId }) / listPreapprovalsByUser(ctx, { userId })
  • getSubscriptionStatus(ctx, { userId, approvedPaymentWindowMs? })
  • getCustomerByUserId(ctx, { userId })

Sync (fetch from Mercado Pago and apply through the canonical write path; used by webhooks and reconciliation, callable manually):

  • syncPayment(ctx, { paymentId })
  • syncPreapproval(ctx, { preapprovalId })
  • syncMerchantOrder(ctx, { merchantOrderId })
  • syncAuthorizedPayment(ctx, { authorizedPaymentId })

Infrastructure:

  • registerRoutes(http, { path? }) - Mount the webhook route (default /mercadopago/webhook)
  • reconcile(ctx, { lookbackDays? }) - Run pull reconciliation (see Reconciliation)
  • api() - Pre-bound host functions (see above)

React

  • usePaymentStatus(queryRef, args) - Wraps useQuery over a host-exposed payment query; returns { payment, isLoading }
  • useSubscriptionStatus(queryRef, args) - Same shape for subscription status
  • getCheckoutUrl(preference, { sandbox }) - Picks init_point or sandbox_init_point

Webhooks

Handled topics: payment, merchant_order, preapproval, subscription_preapproval, subscription_authorized_payment, subscription_preapproval_plan, chargebacks, and their topic_*_wh aliases.

For every delivery the route:

  1. Reads the raw request body and verifies the x-signature HMAC-SHA256 against it (constant-time comparison, timestamp tolerance). Only QR Code notifications (point_integration_wh) are exempt; everything else is rejected with 401 when the signature is missing or invalid.
  2. Records a fingerprinted event in the ledger. Duplicates are acknowledged with 200 immediately and never re-run your hooks.
  3. Acknowledges within Mercado Pago's 22-second window, fetching the full resource by id (notifications only carry data.id) and applying it through the same canonical mutation used by reconciliation.
  4. Diffs the before/after state and fires your lifecycle hooks exactly once per transition.

Available hooks: onPaymentCreated, onPaymentApproved, onPaymentRejected, onPaymentRefunded, onPaymentChargedBack, onPaymentInMediation, onSubscriptionAuthorized, onSubscriptionPaused, onSubscriptionCancelled, onAuthorizedPaymentProcessed, onChargeback, and the catch-all onEvent.

Chargeback notifications fetch the chargeback resource, sync every payment in its payments[] array, and fire onChargeback once in addition to the per-payment hooks.

If a redelivered event matches a ledger row in failed state it is reset and processed again (with retryCount incremented), so Mercado Pago's automatic retries recover transient failures. Any other existing state stays a duplicate.

Reconciliation

Webhooks can be missed, and sandbox credentials never send them. Register a cron that calls the reconciliation action:

// convex/crons.ts
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";

const crons = cronJobs();
crons.interval(
  "reconcile Mercado Pago",
  { hours: 6 },
  internal.mercadopago.reconcile,
  {},
);
export default crons;

reconcile(ctx, { lookbackDays? }) (default 3 days):

  • Reprocesses up to 25 failed ledger events, plus events stuck in processing for over 15 minutes after an interrupted run
  • Searches recent payments, preapprovals, and authorized payments and applies them through the same canonical write path as webhooks
  • Re-syncs mirrored payments still in a non-terminal status (pending / in_process / authorized) older than one hour

It returns { failedReprocessed, failedStillFailing, stuckReprocessed, paymentsSynced, preapprovalsSynced, authorizedPaymentsSynced, staleResynced, errors }. Lifecycle hooks fire on reconciliation-applied transitions exactly as they do for webhooks. Each search endpoint uses its own filter semantics: payments by date range, preapprovals by range=last_modified:after:<iso>, and authorized payments by preapproval id.

Database Schema

The component owns these tables (all provider ids stored as strings and indexed; raw provider payloads kept for audit):

| Table | Purpose | Key fields | | --- | --- | --- | | preferences | Checkout Pro preferences | preferenceId, externalReference, userId, initPoint, items, status | | payments | Payment mirror | paymentId, externalReference, userId, status, statusDetail, transactionAmount, preferenceId, preapprovalId, mpLastModified | | refunds | Refunds per payment | refundId, paymentId, amount, status | | merchantOrders | Checkout Pro order aggregation | merchantOrderId, preferenceId, orderStatus, paidAmount, refundedAmount | | preapprovals | Subscriptions | preapprovalId, preapprovalPlanId, externalReference, userId, status, autoRecurring, nextPaymentDate | | preapprovalPlans | Subscription plan templates | planId, status, reason, autoRecurring | | authorizedPayments | Recurring charge attempts | authorizedPaymentId, preapprovalId, paymentId, status, retryAttempt | | customers | Mercado Pago customers | mpCustomerId, userId, email | | customerCards | Saved card metadata (never PAN/CVV) | cardId, mpCustomerId, lastFour, expirationMonth, expirationYear | | webhookEvents | Ingestion ledger | fingerprint, topic, dataId, verified, processingState, retryCount |

Statuses are stored as strings on purpose — Mercado Pago adds new ones — and every write is gated by a forward-only state machine plus a date_last_updated guard, so stale or out-of-order data can never overwrite newer state.

Testing

The ./test export provides a register helper for convex-test:

import { convexTest } from "convex-test";
import { register } from "convex-mercadopago/test";
import schema from "./convex/schema";

const t = convexTest(schema);
register(t);

register(t, name = "mercadopago") registers the component schema and modules under the same component name used in convex.config.ts.

Sandbox testing

Mercado Pago sandbox behavior is driven by test cards and the cardholder name (APRO approves, CONT stays pending, FUND/SECU reject). Two manual scripts exercise the real sandbox with credentials from .env.local (never committed, never printed):

node scripts/sandbox-smoke.mjs        # preference round-trip + payment search
node scripts/sandbox-live-tests.mjs   # 10-test suite incl. idempotency probes

Example App

See example/ for a complete backend-only host app: client initialization with identify, checkout and refund commands, webhook registration, lifecycle hook logging, and a reconciliation cron.

Troubleshooting

401 on webhook delivery: check for a wrong or rotated MERCADOPAGO_WEBHOOK_SECRET, and verify the signature against the raw request body before any re-serialization. The only unsigned-topic exception is QR Code point_integration_wh; Checkout Pro merchant-order webhooks must be signed when a secret is configured.

Mercado Pago error code 4292: a non-GET request is missing a valid X-Idempotency-Key. The client generates deterministic keys for built-in write methods unless you override them.

No sandbox webhooks: Mercado Pago does not deliver webhooks for payments made with test credentials. Use reconcile() or manual polling during development.

Error 2034 "Invalid users involved" or 4390 "Payer email forbidden" in sandbox: when paying with TEST credentials of a production account, the payer.email must be a plain email address that is not registered on Mercado Pago at all — not a @testuser.com address and not your own account email. @testuser.com payer emails are rejected (4390 for payments, 400 for preapproval), and unpaired fabricated ones cause 2034 or raw 500s. The same rule applies to payer_email on preapproval creation.

Refund returns 500 internal_error right after payment: sandbox refunds fail with a raw 500 until the payment settles; live testing observed this can take up to ~2 minutes after creation. Retry after a short delay — the same request succeeds once settled. The adapter's bounded retry does not cover this window; treat an immediate post-payment refund 500 as retryable later, not as a permanent failure.

Duplicate preferences after retries: Mercado Pago does not enforce X-Idempotency-Key on /checkout/preferences (verified live — replaying the same key creates a new preference), so the client never auto-retries that endpoint. Pass external_reference and deduplicate on it.

License

MIT — a deliberate divergence from the Apache-2.0 used by official @convex-dev components.