
Mercado Pago Split Payments: Build a LATAM Marketplace with Seller Payouts (the Stripe Connect Most People Can't Use)
Mercado Pago Split Payments lets your marketplace charge a buyer once, settle on the seller's account, and take a cut via application_fee. Connect each seller with OAuth, store and rotate their access/refresh tokens, then create the payment with the seller's token. MP's own fee is deducted first; your fee comes off the remainder.
Why Stripe Connect leaves LATAM marketplaces stranded
Stripe Connect can’t pay out sellers in most of LATAM, so for a Mexican or Argentine seller base you build on Mercado Pago Split Payments instead: the buyer pays once on the seller’s OAuth token, you take an application_fee, and MP splits the money at settlement. That single payout-country gap is the entire reason this post exists.
Stripe Connect is the best marketplace payments API I’ve ever used. It’s also useless to most marketplaces I build, because the sellers live in the wrong country.
Here’s the wall: Stripe Connect can onboard connected accounts and pay them out only in Stripe’s supported payout countries, and that list excludes much of LATAM. A platform incorporated in the US can use Connect, but if the seller you need to pay out is sitting in Mexico or Argentina, Connect can’t deliver money into their hands. The API is elegant. The payout rail simply doesn’t reach them. (Confirm Stripe’s current supported-countries list in their docs — it shifts, and as of 2026 Mexico and Argentina are not Connect payout countries.)
I learned this the expensive way building Mercanto, a B2B marketplace where both buyers and local sellers were in Mexico. I had the whole Connect mental model loaded — destination charges, application fees, payout schedules — and none of it could pay a single seller. If you’ve already read my Stripe Connect counterpart guide, keep that model handy: a lot of it ports over conceptually. But the rail you’ll actually ship on in LATAM is different.
That rail is Mercado Pago Split Payments (their docs also call it “marketplace”). One platform charges the buyer once, the money settles, and MP splits it between the seller and your commission. It is the LATAM-native equivalent of Connect, and for a Mexican or Argentine seller base it’s frequently the only option that actually pays people. This post is the operational, gotcha-level guide I wish existed instead of the scattered official pages. The Split Payments docs are the spine; everything below is what they don’t dwell on.
How does a split payment actually work?
The mental flip from a normal Mercado Pago integration is this: the buyer pays once, but the payment settles on the seller’s account, not yours.
If you’ve done a plain MP integration — Checkout Pro, Bricks, webhooks — from my Mercado Pago basics guide, you created payments on your own access token and the money was yours. Split Payments inverts that. You create the payment using the seller’s OAuth access token, and you pass an application_fee equal to your commission. MP then routes the seller’s share to them and drops your application_fee into your marketplace account. Natively. Split at the moment of settlement.
This is fundamentally different from charging on your own account and then manually transferring money to sellers later. There’s no second transfer to reconcile, no float sitting on your books, no “we’ll pay sellers on Fridays” batch job. The funds land already divided. Refunds reverse the same way — proportionally from both the seller’s share and the marketplace’s share.
How the money splits in a single payment
The whole game, then, is two things: connect each seller’s MP account to your app, and create payments on their token. Everything hard about Split Payments lives in those two sentences.
Connecting sellers with OAuth (the authorization-code flow)
Each seller links their Mercado Pago account to your marketplace via the OAuth Authorization Code flow. This is the same shape as connecting any third-party account — if you’ve internalized the Stripe Connect onboarding and payouts mental model, this will feel familiar: redirect, consent, code, exchange.
Step one: send the seller to MP’s authorization URL with your client_id, your redirect_uri, and a state value you generate to defend against CSRF and to correlate the callback to the right seller.
// Build the seller authorization URL
const authUrl = new URL("https://auth.mercadopago.com/authorization");
authUrl.searchParams.set("client_id", process.env.MP_CLIENT_ID);
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("platform_id", "mp");
authUrl.searchParams.set("redirect_uri", process.env.MP_REDIRECT_URI);
authUrl.searchParams.set("state", sellerLinkState); // verify on callback
// NOTE: if PKCE is enabled on your app, also send code_challenge +
// code_challenge_method here, and pass the matching code_verifier on
// the exchange below. This snippet shows the non-PKCE path for brevity.
return res.redirect(authUrl.toString());
MP redirects the seller back to your redirect_uri with an authorization code in the query string. That code is short-lived — valid roughly 10 minutes — so exchange it immediately, server-side. POST to the token endpoint with grant_type=authorization_code:
// Exchange the authorization code for the seller's token set
const resp = await fetch("https://api.mercadopago.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "authorization_code",
client_id: process.env.MP_CLIENT_ID,
client_secret: process.env.MP_CLIENT_SECRET,
code: req.query.code,
redirect_uri: process.env.MP_REDIRECT_URI,
// code_verifier: ... // include this if you used PKCE on the auth URL
}),
});
const t = await resp.json();
// t.access_token, t.refresh_token,
// t.user_id (= seller's collector_id), t.token_type,
// t.expires_in, t.scope, t.live_mode
// t.public_key may also be present — don't depend on it always returning.
await saveSellerTokens(sellerId, {
collectorId: t.user_id, // persist as the seller's collector_id
accessToken: t.access_token,
refreshToken: t.refresh_token, // ROTATES — see next section
expiresAt: Date.now() + t.expires_in * 1000,
});
Persist user_id as the seller’s collector_id — that’s their MP account identity. Store the whole token set keyed to that seller. At scale you’ll hold one token set per seller, which is its own infrastructure problem (more on that below). MP also supports PKCE on this flow (a code_challenge on the auth URL plus a code_verifier on the exchange); turn it on for production. Endpoint, parameters, and the PKCE details are confirmed in MP’s Get Access Token docs.
The token lifecycle and the refresh-token rotation trap
This is the one that bites everyone, so read it twice.
The seller access_token is valid for 180 days (6 months), per MP’s docs (as of 2026 — confirm current lifetimes in the OAuth management docs). If it expires before you refresh it, there’s no quiet recovery — you have to drag the seller back through the entire authorization flow again. That’s a re-link email, a support ticket, a churned seller. So you refresh before day 180, not on it.
You refresh by POSTing to the same /oauth/token endpoint with grant_type=refresh_token:
async function refreshSeller(sellerId) {
const cur = await getSellerTokens(sellerId);
const resp = await fetch("https://api.mercadopago.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "refresh_token",
client_id: process.env.MP_CLIENT_ID,
client_secret: process.env.MP_CLIENT_SECRET,
refresh_token: cur.refreshToken,
}),
});
const t = await resp.json();
// THE TRAP: t.refresh_token is NEW. The old one is now dead.
// Persist atomically or you lock yourself out of this seller forever.
await db.transaction(async (tx) => {
await saveSellerTokens(sellerId, {
accessToken: t.access_token,
refreshToken: t.refresh_token, // store the rotated value
expiresAt: Date.now() + t.expires_in * 1000,
}, tx);
});
}
Here is the trap MP states plainly and most tutorials skip: the refresh_token rotates on every refresh. Each successful refresh returns a new refresh_token and invalidates the old one. The docs say it directly — every time you refresh the access_token, the refresh_token is also refreshed, so you have to store it again.
If your refresh job exchanges the token but crashes before persisting the new refresh_token — or worse, you keep using the old one — you are permanently locked out of that seller and the only fix is re-authorization. So: refresh on a schedule well before day 180, wrap the read-exchange-write in a transaction, and never, ever reuse a refresh_token after a successful exchange. Treat re-authorization as a first-class failure mode in your UX, with a “reconnect your Mercado Pago account” flow ready to fire.
The two traps that sink LATAM marketplaces
Creating the split payment with application_fee
Now the core transaction. You POST to https://api.mercadopago.com/v1/payments, but the critical detail is the Authorization header: it carries the seller’s OAuth access_token, not your marketplace token. That’s what makes the payment settle on the seller’s account.
You add one extra field versus a normal payment: application_fee, set to your commission in the payment currency. Everything else is a standard Payments API request — transaction_amount, payment_method_id, the card token, payer, etc. Send a unique X-Idempotency-Key so a retry never double-charges the buyer.
One fork to name before you copy anything: this post codes the Checkout API path, where you create payments on /v1/payments and your commission rides in application_fee. If you’re on Checkout Pro instead, the surface is different — you create a preference on /checkout/preferences and your commission is the marketplace_fee field, not application_fee. Same concept, different endpoint and field name; don’t copy application_fee onto a preference and wonder why your cut never lands.
// Create a split payment ON THE SELLER'S TOKEN (Checkout API path)
const sellerTokens = await getSellerTokens(sellerId);
const resp = await fetch("https://api.mercadopago.com/v1/payments", {
method: "POST",
headers: {
"Authorization": `Bearer ${sellerTokens.accessToken}`, // seller's token
"Content-Type": "application/json",
"X-Idempotency-Key": orderId, // unique per order; retries are safe
},
body: JSON.stringify({
transaction_amount: 1000, // buyer pays this (MXN)
application_fee: 100, // YOUR commission, same currency
token: cardToken, // card token from the frontend
payment_method_id: "visa", // illustrative — varies by method
installments: 1,
payer: { email: buyerEmail },
// description, external_reference, etc.
}),
});
const payment = await resp.json();
// payment.id, payment.status ("approved" | "pending" | ...)
// Do NOT fulfill yet — wait for the verified webhook.
The exact payment_method_id and the card-token plumbing depend on which checkout you use (Bricks, Checkout API, etc.) — treat those fields as illustrative and match them to your method. What’s not illustrative is the shape: seller token in the header, application_fee in the body, idempotency key always.
When this succeeds, the split happens automatically. The seller gets their share, your application_fee lands in your account, no manual transfer. But — and this is the discipline that ports straight over from Stripe — do not fulfill the order off this response or off the front-end redirect. Verify a signed webhook/IPN and re-fetch the payment to confirm status: "approved" first. The endpoint and the application_fee / marketplace_fee behavior are documented in MP’s Split Payments hub and the marketplace checkout how-to.
Stand up a Split Payments marketplace end-to-end
- Register your OAuth appGet client_id, client_secret, set redirect_uri in the MP dashboard
- Seller authorizes (auth-code flow)Redirect to MP, receive the ~10-min code on your callback
- Exchange + store the token setPersist access_token, rotating refresh_token, and user_id (collector_id) per seller
- Create the split paymentPOST /v1/payments on the seller's token with application_fee + idempotency key
- Verify webhook, then reconcileConfirm the signed event, then reconcile via settlement/releases reports
The fee-ordering gotcha: who gets paid first
This is the one that quietly breaks your unit economics if you quote sellers off a napkin.
Naively, you’d think: buyer pays $1,000, my take rate is 10%, so I set application_fee = 100 and the seller nets $900 minus whatever MP charges. Wrong order. Here’s how MP actually does it:
Mercado Pago’s own processing commission is deducted first, from the gross total. Your application_fee is then deducted from what remains — not from the gross. The docs are explicit that MP’s commission comes off before the marketplace’s. So:
seller net = total − MP fee − application_fee
Worked example with round numbers (MP fee illustrative — confirm your real rate):
total = 1000 MXN (buyer pays)
MP processing fee ≈ 4% = 40 MXN (deducted FIRST)
remainder = 960 MXN
your application_fee = 100 MXN (deducted from the remainder)
-----------------------------------------------
seller net = 860 MXN
you receive = 100 MXN
MP keeps = 40 MXN
The seller nets 860, not the 900 a naive “gross minus my 10%” model implies. If you promised a seller “you keep 90% of every sale,” you just under-delivered by 40 pesos on a 1,000-peso order, and they will notice on the first reconciliation. If you want to guarantee sellers a flat take rate, you have to back-solve your application_fee against MP’s current fee — and MP’s rates are time-sensitive, so confirm them in your dashboard (as of 2026). Model the exact order before you publish take rates, not after a seller emails you about the math.
Operational realities: reconciliation, restrictions, scale
The demo is easy. The marketplace that survives an audit is where the engineering time actually goes, and the docs breeze past all of it.
Reconciliation is API-driven. There’s no friendly cross-seller dashboard showing you “here’s every split, who got what, when it released.” You pull settlement and releases reports via API and build your own ledger. Budget for it. On Mercanto, the reconciliation ledger — mapping each payment to its split, its release date, and its reporting line — was a bigger build than the payment flow itself.
There are restrictions on moving funds externally. Don’t assume you have arbitrary payout or transfer flexibility on top of the split. The money lands where the split puts it; design within those constraints rather than planning a custom payout layer you can’t ship.
At scale, token management is real infrastructure. One token set per seller means: encrypted storage, a scheduled refresh job that runs well before day 180, monitoring for refresh failures, and a self-serve re-link flow for when a token set lapses anyway. None of that is optional once you have more than a handful of sellers.
Webhooks/IPN are your source of truth. Verify the signed event, then fetch the payment resource to confirm its status before granting any fulfillment. This is the same discipline I preach for every payment integration: the verified, signed event is authoritative — never the front-end redirect, never the create-payment response alone.
Stripe Connect vs Mercado Pago Split Payments for LATAM sellers
Stripe Connect
- Seller payout countries EXCLUDE much of LATAM (Mexico, Argentina) — confirm current list
- Platform can hold funds; rich payout-schedule control
- Commission via application_fee on a polished API
- Best-in-class reconciliation dashboard
- Can't pay out local Mexican/Argentine sellers — disqualifying for those marketplaces
MP Split Payments
- Built for LATAM seller payouts — Mexico, Argentina, and more
- Payment settles on the SELLER's account; split at settlement
- Commission via application_fee (Checkout API) or marketplace_fee (Checkout Pro)
- Reconciliation is API-driven — build your own ledger
- The realistic choice you can actually use for LATAM sellers
FAQ
Can I just use Stripe Connect in Mexico or Argentina instead? Not for paying out local sellers. Connect’s payout countries exclude much of LATAM (confirm Stripe’s current list). That gap is the entire reason MP Split Payments exists in this market.
Whose token creates the payment — mine or the seller’s? The seller’s OAuth access_token, in the Authorization header. Your cut rides along as application_fee. Using your own marketplace token would just charge on your account, which isn’t a split.
What happens if a seller’s access_token expires? You’re locked out of charging on their account until they re-authorize through the full OAuth flow again — unless you refreshed in time. Refresh well before the ~180-day mark.
Does the refresh_token stay the same across refreshes? No. It rotates on every refresh — each call returns a new one and invalidates the old. Persist the new value atomically or you permanently lose access to that seller.
Is my application_fee taken from the gross total? No. MP’s processing fee comes off the gross first; your application_fee is deducted from the remainder. Seller net = total − MP fee − application_fee.
Is the field application_fee or marketplace_fee? Depends on the checkout. Checkout API payments on /v1/payments use application_fee. Checkout Pro preferences on /checkout/preferences use marketplace_fee. Same concept, different surface.
How do I know a payment actually succeeded? Verify a signed webhook/IPN, fetch the payment, confirm status is approved, then fulfill. Never trust the redirect or the raw create-payment response.
Closing: build it on the verified event, not the redirect
Mercado Pago Split Payments is the realistic way to run a LATAM marketplace that pays third-party sellers. Stripe Connect’s API is nicer; it just can’t put money in your Mexican or Argentine sellers’ hands, and a marketplace that can’t pay its sellers isn’t a marketplace.
Win on two things and you’ve won most of the war. Never lose a rotated refresh_token — refresh early, persist atomically, treat re-link as a real flow. And always model MP’s fee before yours — the processing cut comes off first, your application_fee off what’s left, so seller net = total − MP fee − your fee. Quote sellers against that exact order or you’ll owe someone an awkward explanation.
And the discipline underneath all of it: grant fulfillment only on a verified, signed event. The webhook is the source of truth, never the redirect. This is the exact stack I wired behind Mercanto — if you’re building a LATAM marketplace and want a second set of eyes on the token lifecycle or the reconciliation ledger, that’s the part I’d want help with too. Reach out.