Si tu negocio ES una plataforma — un marketplace, un ISV, una app que cobra a nombre de otros negocios — Winal Connect te deja cobrar a tu comprador, partir ese cobro entre tu comisión y tus sub-comercios, y dispersar cada porción a la CLABE de cada quien. Mismo principio de toda la API (ADR-0001, sin custodia): el dinero nunca pasa por una cuenta de Winal — se liquida a tu cuenta de plataforma y de ahí se dispersa a cada sub-comercio por el riel de dispersión (Payouts), igual que Stripe Connect pero sobre rieles mexicanos (SPEI vía STP).
Cobras a tu comprador
Un payment_intent normal — nada cambia en confirm.
Partes el cobro (split)
Tu comisión (application_fee_minor) se queda contigo; el resto se vuelve una obligación con el sub.
Dispersas al sub-comercio
Un payout a la CLABE del sub-comercio salda la obligación — evidencia del proveedor, nunca timeout.
paidapproved — es donde vive la CLABE de
liquidación del sub-comercio y su verificación KYB. Sin eso, POST /v1/connect/accounts
responde connect.application_not_approved.
1. Liga un sub-comercio aprobado
curl -s https://api.winal.com.mx/v1/connect/accounts \
-H "Authorization: Bearer $SK" \
-H "Content-Type: application/json" \
-d '{ "onboarding_application_id": "0732573a-5567-4b2a-87b6-8721654c5060" }'
{
"id": "0d0de738-0133-4625-97e2-9c285ae20f9e",
"object": "connect_account",
"onboarding_application_id": "0732573a-5567-4b2a-87b6-8721654c5060",
"settlement_clabe_masked": "**** **** **** 0004",
"sub_merchant_name": "Sub Comercio Sim SA de CV",
"status": "active",
"connector_key": "sim",
"livemode": false,
"created_at": "2026-07-08T06:49:58.292982+00:00"
}
connector_key es opcional (por defecto stp, el riel de dispersión real);
usa "sim" mientras pruebas sin cuenta STP — igual que en Payouts.
La CLABE del sub jamás se expone completa. Ligar la misma solicitud dos veces da
409 connect.account_conflict.
2. Marca un cobro como "de plataforma"
Hay dos formas — ambas soportadas hoy, elige según tu caso:
Vía A — metadata en el intent (recomendada, automática)
Al crear el payment_intent, pon en metadata el connect_account_id
del sub-comercio y, opcional, tu application_fee_minor (ambos como strings, igual
que cualquier valor de metadata). Cubre el caso de un solo sub-comercio por cobro.
curl -s https://api.winal.com.mx/v1/payment_intents \
-H "Authorization: Bearer $SK" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"amount_minor": 100000,
"currency": "MXN",
"metadata": {
"connect_account_id": "0d0de738-0133-4625-97e2-9c285ae20f9e",
"application_fee_minor": "10000"
}
}'
Confirma el intent como cualquier otro (tok_sim_ok en pruebas). En cuanto llega a
succeeded, el Worker lee la metadata y arma el split automáticamente — sin
llamada extra de tu parte. Es asíncrono: espera unos segundos (o escucha el webhook) antes de
consultar GET /v1/connect/transfers.
Vía B — endpoint manual (post-cobro, múltiples sub-comercios)
POST /v1/connect/transfers reparte un cobro ya exitoso entre varios
sub-comercios a la vez. Exige Idempotency-Key (compromete dinero). La regla de cuadre es
estricta: application_fee_minor + Σ splits[].amount_minor debe ser exactamente
igual a charge_amount_minor, en la misma moneda — si no, 400
connect.split_mismatch.
curl -s https://api.winal.com.mx/v1/connect/transfers \
-H "Authorization: Bearer $SK" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"payment_intent_id": "1a1a1190-e378-4731-9c7f-454b3815e80f",
"charge_amount_minor": 50000,
"application_fee_minor": 2500,
"currency": "MXN",
"splits": [
{ "connect_account_id": "8c90e712-b8d0-4158-81f8-38a7c1430fcf", "amount_minor": 47500 }
]
}'
splits admite más de una entrada para repartir un mismo cobro entre varios
sub-comercios (un solo application_fee_minor para toda la plataforma). Un cobro solo se
reparte una vez — reintentar sobre el mismo payment_intent_id con datos distintos falla
antes de tocar dinero dos veces.
3. Dispersión a cada sub-comercio
curl -s https://api.winal.com.mx/v1/connect/transfers \
-H "Authorization: Bearer $SK"
{
"object": "list",
"data": [
{
"id": "53fb6fd5-92c8-4e13-93a1-f01aefb21ce8",
"object": "connect_transfer",
"payment_intent_id": "aaab3de3-3cfe-4d55-9bbd-24a89977abf7",
"charge_amount_minor": 100000,
"application_fee_minor": 10000,
"currency": "MXN",
"status": "split",
"created_at": "2026-07-08T06:50:09.078051+00:00",
"splits": [
{
"id": "09702eba-5dc9-485f-bf64-083fec7f7078",
"connect_account_id": "0d0de738-0133-4625-97e2-9c285ae20f9e",
"amount_minor": 90000,
"status": "dispersing",
"payout_id": "fa888dfc-8457-41b7-9726-4623e31bd5e6"
}
]
}
]
}
Cada porción (splits[]) es en el fondo un payout — sigue su
propio ciclo de vida (pending → dispersing → paid, o failed). Cuando el
payout llega a paid, la obligación con el sub queda saldada. Consulta
GET /v1/payouts/{payout_id} para ver el detalle de la dispersión en curso.
Estado del connect_transfer | Significa |
|---|---|
split | El reparto ya se contabilizó (tu comisión + la(s) porción(es) del/los sub(s)); la dispersión está en curso. |
settled | Todas las porciones ya se pagaron al sub-comercio correspondiente. |
Estado de una porción (splits[].status) | Significa |
|---|---|
pending | Contabilizada, dispersión aún no ordenada. |
dispersing | El payout ya se ordenó; en espera de evidencia del proveedor (regla 5 — nunca por timeout). |
paid | El sub-comercio ya recibió su porción. |
failed | La dispersión falló (ver GET /v1/payouts/{payout_id} para el motivo). |
dispersingpayout se crean correctamente incluso con el conector sim,
pero Sim no tiene forma de forzar el pago final por HTTP (solo lo resuelven credenciales STP reales
en producción, o las utilerías internas de los tests). Es exactamente el comportamiento correcto de
"esperando evidencia del proveedor" — no un bug: el reparto y la contabilidad ya ocurrieron, la
liquidación final del riel es lo único gated por el trámite STP del dueño.
Endpoints
| Método | Ruta | Notas |
|---|---|---|
| POST | /v1/connect/accounts | Liga un sub-comercio aprobado. |
| GET | /v1/connect/accounts | ?limit= opcional. |
| GET | /v1/connect/accounts/{id} | 404 connect.account_not_found si no existe. |
| POST | /v1/connect/transfers | Split manual; requiere Idempotency-Key. |
| GET | /v1/connect/transfers | ?limit= opcional. |
| GET | /v1/connect/transfers/{id} | 404 connect.transfer_not_found si no existe. |
Errores de connect
| HTTP | code | Causa |
|---|---|---|
400 | connect.invalid_application_id | onboarding_application_id ausente o no es un uuid. |
404 | connect.application_not_found | La solicitud de Onboarding referida no existe. |
400 | connect.application_not_approved | La solicitud existe pero no está approved. |
409 | connect.account_conflict | Esa solicitud ya está ligada a una cuenta Connect. |
404 | connect.account_not_found | El id de la cuenta (o un connect_account_id dentro de splits) no existe. |
400 | connect.account_suspended | La cuenta Connect no está activa. |
400 | connect.invalid_payment_intent | payment_intent_id ausente o no es un uuid. |
400 | connect.invalid_charge | Falta charge_amount_minor positivo o currency. |
400 | connect.invalid_currency | Moneda ISO 4217 inválida. |
400 | connect.missing_allocations | splits vacío o ausente. |
400 | connect.invalid_allocation | Una porción trae connect_account_id inválido o amount_minor no positivo. |
400 | connect.split_mismatch | application_fee_minor + Σ splits no cuadra exactamente con charge_amount_minor. |
404 | connect.transfer_not_found | El id del transfer no existe. |