Implementing Split Payments with Mercado Pago in a Next.js Marketplace
TL;DR: I added Mercado Pago split‑payment support so a provider’s fee is automatically divided among the provider, their accountant, and a lawyer. This required schema changes, new env variables, a webhook handler, and admin UI tweaks to configure the percentages.
The Problem
Our marketplace originally offered two payment modes:
- Disabled – no payment processing.
- Transfer – a full‑amount bank transfer to the provider after the service is marked complete.
When we introduced legal‑backed services (accounting and legal advice) we needed a third mode: Mercado Pago split. The idea is simple on paper—take the total amount a client pays, keep a configurable commission, then automatically split the remainder into three shares:
| Share | Recipient |
|---|---|
| Provider | Service provider |
| Accountant | Assigned accountant |
| Lawyer | Assigned lawyer |
In practice the lack of split support caused two concrete issues:
-
Payment failures – the old transfer endpoint tried to send the full amount to the provider, then manually debit the accountant and lawyer, which Mercado Pago rejected with
Error: split_amounts_total does not equal payment amount. - Missing audit trail – we had no persisted record of how the split percentages were defined, making it impossible to generate the required legal documents for the accountant and lawyer.
The symptom was a 500 error on the checkout API and a “payment rejected” entry in the logs:
2026-11-09T14:32:11.842Z ERROR webhook.handler - MercadoPago error: split_amounts_total does not equal payment amount
What I Tried First
My first attempt was to keep the existing processPayment function in src/app/api/payments/route.ts and calculate the three shares on the client, then send them as part of the request body:
// client side (bad idea)
const split = {
provider: amount * 0.70,
accountant: amount * 0.20,
lawyer: amount * 0.10,
};
await fetch('/api/payments', { method: 'POST', body: JSON.stringify({ amount, split }) });
The server simply forwarded the payload to Mercado Pago’s /checkout/preferences endpoint. This approach broke for two reasons:
- Rounding errors – floating‑point math caused the sum of the three shares to be a few cents off, triggering the API error above.
- Security risk – anyone could tamper with the split percentages in the request body.
I rolled back that change and decided to move the split logic entirely to the backend, persisting the percentages in the database and letting the webhook verify the final distribution.
The Implementation
Below is a step‑by‑step of the files that changed, why they changed, and the key code snippets.
1. Environment configuration
/.env.example got a new variable for the Mercado Pago access token and a flag to enable split mode:
+ MERCADO_PAGO_ACCESS_TOKEN=your_mp_access_token_here
+ MERCADO_PAGO_SPLIT_ENABLED=true
The flag is read in src/config/payment.ts to guard the new route.
2. Database schema
We needed to store the split percentages per provider. I added three new columns to the User model (providers are also users) in prisma/schema.prisma:
model User {
id Int @id @default(autoincrement())
email String @unique
role Role
// Existing fields …
+ // Split percentages (values between 0 and 1)
+ providerShare Float @default(0.7)
+ accountantShare Float @default(0.2)
+ lawyerShare Float @default(0.1)
// Relations
accountant Accountant? @relation(fields: [accountantId], references: [id])
lawyer Lawyer? @relation(fields: [lawyerId], references: [id])
}
Running prisma migrate dev generated prisma/migrations/20261108000000_revisiones_profesionales/migration.sql with the following SQL:
ALTER TABLE "User"
ADD COLUMN "providerShare" DOUBLE PRECISION NOT NULL DEFAULT 0.7,
ADD COLUMN "accountantShare" DOUBLE PRECISION NOT NULL DEFAULT 0.2,
ADD COLUMN "lawyerShare" DOUBLE PRECISION NOT NULL DEFAULT 0.1;
3. Seed data
To keep the local dev environment functional, I updated prisma/seed/index.ts:
await prisma.user.upsert({
where: { email: 'provider@example.com' },
update: {},
create: {
email: 'provider@example.com',
role: 'PROVIDER',
providerShare: 0.65,
accountantShare: 0.25,
lawyerShare: 0.10,
},
});
4. Admin UI – configuring splits
The admin configuration page (src/app/admin/configuracion/page.tsx) now shows three numeric inputs bound to the new fields:
// src/app/admin/configuracion/page.tsx
<FormField label="Provider share (%)">
<input
type="number"
step="0.01"
value={providerShare}
onChange={e => setProviderShare(parseFloat(e.target.value) / 100)}
/>
</FormField>
<FormField label="Accountant share (%)">
<input
type="number"
step="0.01"
value={accountantShare}
onChange={e => setAccountantShare(parseFloat(e.target.value) / 100)}
/>
</FormField>
<FormField label="Lawyer share (%)">
<input
type="number"
step="0.01"
value={lawyerShare}
onChange={e => setLawyerShare(parseFloat(e.target.value) / 100)}
/>
</FormField>
<Button onClick={save}>Save</Button>
The save handler calls PUT /api/admin/configuracion which updates the User record.
5. Backend – creating the split preference
The core of the new flow lives in src/app/api/webhooks/payments/[provider]/route.ts. The route receives a request to create a payment preference and builds the split array based on the provider’s stored percentages:
ts
// src/app/api/webhooks/payments
---
*Part of my [Build in Public](https://dev.to/zaerohell) series — sharing the real process of building SaaS projects from Playa del Carmen, México.*
*Repo: `zaerohell/home-services-marketplace` · 2026-11-09*
\#playadev #buildinpublic
Top comments (0)