Migrar de Dispersión a Terceros a Dispersiones a Terceros vía SPEI
Guía de migración para negocios que ya usan /payees + /transfers y quieren pasar al nuevo flujo de payout_rules
Si tu integración dispersa dinero a terceros con POST /payees y POST /transfers, esta guía te muestra qué reemplaza a qué en Dispersiones a Terceros vía SPEI, y los cambios de comportamiento que rompen una migración hecha 1:1.
El flujo nuevo separa dos cosas que antes iban juntas: el beneficiario, que ahora se valida contra el banco antes de poder recibir dinero, y la instrucción de dispersión, que ahora es una regla que Conekta ejecuta por ti — una vez, en una fecha, o con una frecuencia — en lugar de una orden que disparas cada vez desde tu código.
¿Sigue funcionando mi integración vieja?Sí — ambos flujos operan en paralelo hoy. Puedes migrar por partes: mover un beneficiario al flujo nuevo no afecta a los que sigues dispersando con
/transfers. Confirma con tu Ejecutivo de Cuentas o [email protected] si tu negocio tiene una fecha de corte planeada para/payeesy/transfers.
Antes de empezar
- Dispersiones a Terceros vía SPEI activado en tu negocio. La activación requiere la firma de un acuerdo con las condiciones y tarifas del servicio — pídela a tu Ejecutivo de Cuentas o a [email protected].
- Tus llaves de API → API Keys — Testing para la migración, API Keys — Production para el corte.
- Un webhook configurado → Configurar un webhook. El flujo nuevo entrega por webhook el resultado de la validación del beneficiario — información que
/payeesdevolvía directamente en la respuesta. - Verificación de firma en tu manejador → Verificar firmas, obligatoria para procesar los eventos
payout.*,payee.*ypayout_rule.*. - Visibilidad de tu saldo → Consulta de balance, para saber si una instrucción tiene fondos el día que se ejecuta.
Qué ganas al migrar
- Validación de la cuenta destino antes de mandar dinero. Conekta confirma con el banco que la CLABE existe y corresponde al titular declarado. Una CLABE mal capturada se detecta en el alta, no cuando el dinero ya salió.
- Programación y recurrencia sin código propio. Los pagos a fecha fija o con periodicidad se configuran como reglas (
rule_type: scheduledorecurrent). Si hoy mantienes un cron que llama a/transfers, puedes retirarlo. - Alta masiva de beneficiarios por CSV.
- Consulta de estado por API —
GET /payouts/disbursementsyGET /payouts/batches, sin depender de haber guardado el payload del webhook. - Cancelación antes de la ejecución — una instrucción
one_timeque todavía no se ejecutó se puede eliminar; en/transfersno había forma de dar marcha atrás. - Fondeo propio por SPEI, además del saldo generado por tus cobros en Conekta.
- Ejecución el mismo día. Una instrucción
one_timecreada con saldo disponible se dispersa de inmediato por ejecución on-demand, en lugar de esperar al proceso de apertura del día. El resto de las instrucciones conserva el comportamiento de/transfers: se ejecutan en el proceso de apertura del día del día que les corresponde.
Qué reemplaza a qué
| Acción | Antes (v2.2.0) | Ahora (SPEI) | Nota |
|---|---|---|---|
| Crear beneficiario | POST /payees — objeto payee con destinations[] | POST /payouts/payees (doc) — objeto external_payee, una sola clabe | Un beneficiario por CLABE — ver Un payee ahora es una sola CLABE |
| Editar beneficiario | Editabas destinations[] | PATCH /payouts/payees/{id} — alias, rfc, email, max_amount_limit, o pausar/reactivar; si está rejected, también clabe/name para reintentar validación | Ver Editar un beneficiario tiene límites |
| Límite máximo por beneficiario | No existía | max_amount_limit — obligatorio al crear el beneficiario | Ver Los beneficiarios necesitan un límite máximo para recibir pagos |
| Alta masiva | No existía | POST /payouts/payees/bulk (CSV) | Funcionalidad nueva |
| Enviar dinero puntual | POST /transfers — el pago quedaba disparado en la misma llamada y salía el siguiente día hábil | POST /payout_rules con rule_type: one_time (doc) | Crea una instrucción, no un pago — ver La instrucción no es una orden de ejecución |
| Pago programado o recurrente | Lo resolvías con un cron propio | POST /payout_rules con rule_type: scheduled o recurrent | Puedes retirar tu scheduler — pero recurrent exige end_date (máx. 12 meses); ver Las instrucciones recurrentes ahora vencen |
| Cancelar una instrucción | No existía | DELETE /payout_rules/{id} | Sin restricción para scheduled/recurrent; one_time solo mientras no se haya ejecutado |
| Reintentar un pago fallido por saldo | No existía | Automático en cuanto llega un fondeo adicional el mismo día, sin configuración y sin tope de intentos; manual con POST /payouts/disbursements/{id}/retry o POST /payouts/batches/{fecha}/retry en cualquier momento | El reintento automático solo cubre el batch del día — ver Seguimiento de dispersiones |
| Consultar el estado de un pago | Vía el payload del webhook | GET /payouts/disbursements, GET /payouts/payees/{id}/disbursements (doc) | — |
| Consultar el proceso del día | No existía | GET /payouts/batches | — |
| Webhooks de dispersión | payout.created, payout.paid_out, payout.failed | payout.created, payout.in_transit, payout.paid_out, payout.failed, payout.retrying, payout.skipped_limit | in_transit, retrying y skipped_limit son nuevos |
| Webhooks de beneficiario | payee.created, payee.updated, payee.deleted | payee.payout_method.created / .updated / .deleted, payee.max_amount_limit.updated | Los de /payees siguen activos para ese flujo — no son reemplazados, conviven con el stream nuevo |
| Unidad del monto | amount en centavos, entero | amount en centavos, entero — sin cambio | max_amount_limit usa la misma unidad y tipo |
| Fondeo | Solo desde saldo generado por cobros en Conekta | Saldo disponible o depósito directo por SPEI | — |
Cómo funciona el flujo nuevo
sequenceDiagram
autonumber
participant Tu as Tu servidor
participant Conekta
participant Banco as Banco del beneficiario
Tu->>Conekta: POST /payouts/payees (alias + name + clabe + max_amount_limit)
Conekta-->>Tu: external_payee · pending_validation
Conekta->>Banco: Validación de la cuenta destino
Banco-->>Conekta: Resultado
Conekta-->>Tu: Webhook payee.payout_method.updated · validated_active
Tu->>Conekta: POST /payout_rules (payee_id, amount, rule_type)
Conekta-->>Tu: Instrucción creada
Note over Conekta: Llega el día de la instrucción y hay fondos en la cuenta de dispersiones
Conekta->>Banco: Transferencia SPEI
Conekta-->>Tu: Webhook payout.in_transit → payout.paid_out
- Das de alta al beneficiario con
POST /payouts/payees, incluyendo sumax_amount_limit. La respuesta traevalidation_status: pending_validation: el beneficiario existe pero todavía no puede recibir dinero. - Conekta valida la cuenta contra el banco. El resultado te llega por el webhook
payee.payout_method.updated, o lo consultas conGET /payouts/payees/{id}. - Creas la instrucción con
POST /payout_rules, apuntando alpayee_idya validado. - Conekta ejecuta la instrucción el día que corresponde, cuando hay fondos disponibles en tu cuenta de dispersiones.
- Sigues el resultado por los webhooks
payout.*o consultandoGET /payouts/disbursements.
Estados del beneficiario
Solo un beneficiario en validated_active puede recibir dispersiones:
stateDiagram-v2
[*] --> pending_validation: POST /payouts/payees
pending_validation --> validated_active: cuenta confirmada
pending_validation --> rejected: la validación falla
rejected --> pending_validation: PATCH clabe/name (reintenta validación)
validated_active --> validated_inactive: PATCH validation_status
validated_inactive --> validated_active: PATCH validation_status
ImportanteUn
PATCHsobre un beneficiario que no estárejectedsolo editaalias/rfc/max_amount_limito alternavalidation_statusentrevalidated_activeyvalidated_inactive— nunca dispara una nueva validación. Si el beneficiario estárejected, unPATCHconclabey/onamesí lo re-somete: lo regresa apending_validation. Ver Editar un beneficiario tiene límites.
Siete cambios de comportamiento
Estos siete puntos son los que causan incidentes en una migración hecha por reemplazo directo de endpoints.
Un payee ahora es una sola CLABE
Un payee podía tener varios destinations[] bajo un mismo payee_id, con un default_destination_id. El external_payee tiene un solo campo clabe.
Si tenías un beneficiario con dos o más destinos, en el flujo nuevo son dos o más beneficiarios distintos, cada uno con su propio id. Usa el campo alias (obligatorio) para distinguirlos — es el rol que antes cumplía default_destination_id.
Los payee_id del flujo viejo no sirven en el flujo nuevo. Si tu base de datos guarda payee_id de /payees, necesitas una tabla de mapeo: cada beneficiario recreado devuelve un id nuevo.
Editar un beneficiario tiene límites
PATCH /payouts/payees/{id} acepta alias, rfc, email, max_amount_limit, y el cambio de validation_status entre validated_active y validated_inactive — ninguna de estas operaciones dispara una nueva validación de cuenta. Editar name o clabe solo es posible cuando el beneficiario está rejected: ese PATCH sí lo re-somete a validación bancaria.
Si tu integración vieja corregía datos de un destination sin importar su estado, ese patrón no aplica aquí fuera del caso rejected — para un beneficiario validated_active o validated_inactive con datos mal capturados, la única salida sigue siendo eliminarlo y crear uno nuevo.
Reintentar sin cambiar nada no hace nadaSi el
PATCHde un beneficiariorejectedenvía la mismaclabe/nameque ya tenía, no se dispara una nueva validación. Si la CLABE nueva ya está registrada en otro beneficiario tuyo, elPATCHresponde422 clabe_already_registeredsin someter nada a validación.
Los beneficiarios necesitan un límite máximo para recibir pagos
Cada beneficiario tiene un campo max_amount_limit — el monto máximo, en centavos MXN (mismo tipo y unidad que el amount de una instrucción: entero, sin decimales), que puede recibir por instrucción de dispersión. No hay valor por omisión ni tope superior.
Importante
max_amount_limites obligatorio al crear un beneficiario: si falta, o si viene con decimales o como string, la API responde422 invalid_max_amount_limit_formaten los tres casos. Si migras un catálogo grande por CSV, la columnamax_amount_limit(en centavos) también es requerida: una fila con ese valor en blanco se rechaza igual que un alta individual sin el campo.
Esta validación se aplica en dos momentos distintos:
- Al crear la instrucción (
POST /payout_rules): siamountexcede elmax_amount_limitdel beneficiario, la API responde422 amount_exceeds_payee_limitde inmediato — no se crea la instrucción. - En la ejecución de la apertura del día: la instrucción queda
skipped(skip_reason: payee_limit_exceeded) — no se intenta el envío a SPEI y no cuenta como falla del batch. Llegas aquí por dos caminos: bajaste elmax_amount_limitpor debajo delamountde una instrucción activa, o subiste elamountde la instrucción con unPATCHpor encima del límite. ElPATCHque sube el monto responde200sin revalidar el límite — es el camino silencioso, y solo te enteras el día de la ejecución.
Una dispersiónskippedse puede recuperar solaNo es terminal: en cuanto llega un fondeo adicional a tu cuenta de dispersiones, Conekta vuelve a evaluar las instrucciones
skippedde ese día contra elmax_amount_limitvigente del beneficiario, y las regresa apendingsi ya caben. Si no corriges el límite o el monto, se quedaskippedindefinidamente — nada la reintenta sin que el límite alcance.
Ver Límite máximo por beneficiario para el detalle completo, incluida la advertencia no bloqueante que recibes al bajar un límite con instrucciones activas apuntándole.
La validación del beneficiario puede fallar
POST /payees devolvía un beneficiario listo para usar. POST /payouts/payees inicia una validación bancaria asíncrona, y rejected es un desenlace normal, no un error de tu integración — un beneficiario rechazado no puede recibir dispersiones y no se arregla reintentando la misma carga sin cambios: hay que corregir clabe/name (lo que lo re-somete a validación) o contactar soporte si los datos son correctos. Contempla ese estado en tu manejador de payee.payout_method.updated desde el primer día.
ImportanteSi tu integración vieja creaba el payee y creaba la dispersión inmediatamente después, ese patrón ya no funciona.
POST /payout_rulesresponde422si elpayee_idno está envalidated_active. Espera el webhookpayee.payout_method.updated, o haz polling aGET /payouts/payees/{id}, antes de crear la primera instrucción de un beneficiario nuevo.
La instrucción no es una orden de ejecución
POST /transfers era una orden: se creaba y se pagaba. POST /payout_rules crea una instrucción que Conekta evalúa y ejecuta el día que corresponde según el rule_type — one_time en el próximo proceso de apertura del día (o de inmediato si hay saldo disponible, ver Ejecución on-demand), scheduled en la fecha que definas, recurrent según la frecuencia. Si tu código asumía que crear la transferencia equivalía a haberla pagado, esa suposición hay que reemplazarla por seguir los webhooks payout.* o consultar GET /payouts/disbursements.
El monto conserva su unidad
POST /transfers recibía amount en centavos como entero, y POST /payout_rules lo recibe igual: centavos, entero — "amount": 5000 son $50.00 MXN en ambos endpoints. max_amount_limit usa la misma unidad y el mismo tipo, así que el valor que ya calculas para una transferencia sirve tal cual en los dos campos.
ImportanteAmbos campos exigen un entero. Un valor con decimales o enviado como string responde
422—invalid_amount_formatparaamount,invalid_max_amount_limit_formatparamax_amount_limit. Verifica el monto de tus primeras dispersiones en modo de pruebas antes de mover tráfico real.
Las instrucciones recurrentes ahora vencen
POST /transfers no tenía concepto de recurrencia — resolvías la periodicidad con un cron propio que, en teoría, corría indefinidamente. Una instrucción recurrent en payout_rules sí tiene fecha de fin: end_date es obligatorio, con un máximo de 12 meses desde hoy. No existen instrucciones recurrentes indefinidas.
auto_renew (boolean, default false) decide qué pasa al llegar end_date sin renovar:
auto_renew | Al llegar end_date | Se puede recuperar |
|---|---|---|
false | Pasa a expired — terminal | No, hay que crear una instrucción nueva |
true | Pasa a paused_by_expiration — deja de ejecutarse | Sí, con POST /payout_rules/{id}/reactivate, dentro del periodo de gracia (30 días por default). Pasado ese plazo queda archived, sin vuelta atrás |
Si migras un cron que corría "para siempre"El reemplazo directo en
payout_rulesno es indefinido por diseño. Si necesitas que la recurrencia siga sin intervención manual, usaauto_renew: truey atiende el webhookpayout_rule.renewal.upcoming(a 30, 15 y 3 días de vencer) para confirmar la renovación conPOST /payout_rules/{id}/confirm_renewalantes de que expire.
Ver Vigencia y renovación de instrucciones recurrentes para el ciclo completo.
Cómo migrar tu integración
- Actualiza la versión de API. Cambia el header
Accept— o su equivalente en tu SDK — aapplication/vnd.conekta-v2.3.0+json. - Recrea tus beneficiarios con
POST /payouts/payees, uno por CLABE. Si son muchos, usa el alta masiva por CSV. - Define
max_amount_limitpara cada beneficiario al darlo de alta, o en la columna correspondiente del CSV — es obligatorio, unPOST(o una fila del CSV) sin este campo se rechaza. - Guarda el mapeo
payee_idviejo →idnuevo en tu base de datos. - Espera
validated_activeantes de crear cualquier instrucción para ese beneficiario, y maneja el desenlacerejected. - Reemplaza cada
POST /transfersporPOST /payout_rules: las llamadas ad hoc pasan arule_type: one_time; lo que tu cron disparaba en fechas fijas o con periodicidad pasa ascheduledorecurrent, y retiras el cron. - Si migras un cron recurrente, define
end_date(máximo 12 meses) y decideauto_renew— una instrucciónrecurrentsin fecha de fin no existe en este flujo. - Actualiza tu manejador de webhooks: agrega
payout.in_transit,payout.retrying,payout.skipped_limit, los de beneficiariopayee.payout_method.*ypayee.max_amount_limit.updated, y — si migras un cron arecurrent—payout_rule.renewal.upcomingypayout_rule.paused_by_expiration, que son los que evitan que tu recurrencia se apague sola al llegar aend_date. La verificación de firma es obligatoria. Ver Referencia de eventos. - Decide tu estrategia de reintento por saldo insuficiente: no necesitas configurar nada — Conekta reintenta automáticamente la misma transacción en cuanto detecta un fondeo adicional, sin tope de intentos, mientras siga siendo el mismo día del batch. Para forzar un reintento en cualquier momento, usa
POST /payouts/disbursements/{id}/retry(una dispersión) oPOST /payouts/batches/{fecha}/retry(todas las elegibles de un grupo). - Reemplaza tu lógica de conciliación por
GET /payouts/disbursementsoGET /payouts/batchesen lugar de depender del payload del webhook que guardaste — la comisión se lee al consultar (fee_chargedpor dispersión,total_feespor grupo), no viaja en el webhook. Concilia contra el grupo cerrado (completed,failedoincomplete), no contra uno todavíaprocessing: la comisión se cobra después de que el dinero sale — Conekta despacha primero todas las dispersiones elegibles de la corrida y recién al final del ciclo genera un único cargo por la suma de esa corrida — así quetotal_feesde un grupo abierto puede no reflejar todavía el cargo completo. - Prueba el flujo completo en modo de pruebas — montos, límites, beneficiarios validados y webhooks recibidos — antes de cambiar a llaves productivas.
- Coordina la fecha de corte del flujo viejo con tu Ejecutivo de Cuentas o [email protected].
Antes y después, en código
Crear el beneficiario:
# Antes — v2.2.0
curl -X POST https://api.conekta.io/payees \
-H "Accept: application/vnd.conekta-v2.2.0+json" \
-H "Authorization: Bearer key_xxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Juan Pérez García",
"destinations": [
{ "type": "clabe", "account_number": "012180XXXXXXXXXXXX" }
]
}'# Ahora — v2.3.0
curl -X POST https://api.conekta.io/payouts/payees \
-H "Accept: application/vnd.conekta-v2.3.0+json" \
-H "Authorization: Bearer key_xxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"alias": "proveedor-norte",
"name": "Juan Pérez García",
"clabe": "012180XXXXXXXXXXXX",
"max_amount_limit": 2000000
}'La respuesta trae el beneficiario en validación:
{
"id": "payee_2tXx9jDpCqIrjVKZ",
"object": "external_payee",
"name": "Juan Pérez García",
"alias": "proveedor-norte",
"clabe": "012180XXXXXXXXXXXX",
"bank_code": "012",
"validation_status": "pending_validation",
"max_amount_limit": 2000000
}
Usa una CLABE realLa CLABE de estos ejemplos está enmascarada a propósito y no pasa la validación del banco — sustitúyela por la cuenta real del beneficiario que vas a dar de alta. La respuesta real de la API incluye la CLABE completa: Conekta la necesita para operar el SPEI. No la registres en logs ni la muestres en interfaces de cliente sin enmascarar.
Enviar $50.00 MXN a ese beneficiario:
# Antes — v2.2.0 · amount en centavos, ejecución implícita
curl -X POST https://api.conekta.io/transfers \
-H "Accept: application/vnd.conekta-v2.2.0+json" \
-H "Authorization: Bearer key_xxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "payee_id": "payee_2tXx9jDpCqIrjVKZ", "amount": 5000 }'# Ahora — v2.3.0 · amount sigue en centavos, pero crea una instrucción, no un pago
curl -X POST https://api.conekta.io/payout_rules \
-H "Accept: application/vnd.conekta-v2.3.0+json" \
-H "Authorization: Bearer key_xxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"rule_type": "one_time",
"payee_id": "payee_2tXx9jDpCqIrjVKZ",
"amount": 5000,
"name": "Liquidación proveedor"
}'Errores: qué cambia
El catálogo genérico de /transfers (authentication_error, processing_error por saldo insuficiente, parameter_validation_error por CLABE o payee inválidos) se reemplaza por respuestas 422 específicas por endpoint, documentadas en cada página:
- Errores de
POST /payouts/payees→ Gestión de beneficiarios - Errores de
POST /payout_rules→ Tipos de dispersión
| Situación | Antes | Ahora |
|---|---|---|
| Beneficiario aún no validado | No existía el estado | 422 al crear la instrucción; espera validated_active |
| CLABE que el banco no reconoce | Fallaba al transferir | El beneficiario queda en rejected en el alta, antes de mover dinero |
| Monto excede el límite del beneficiario | No existía | 422 amount_exceeds_payee_limit al crear la instrucción; skipped en la ejecución de la apertura del día si el límite se bajó después |
recurrent sin fecha de fin, o excede el máximo | No existía (end_date era opcional) | 422 end_date_required al faltar, o 422 end_date_exceeds_max_commitment si excede 12 meses |
amount o max_amount_limit falta o no es un entero (string, decimal, etc.) | No aplicaba | 422 invalid_amount_format / 422 invalid_max_amount_limit_format — mismo código si falta el campo o si el formato es inválido |
| Saldo insuficiente el día de la ejecución | processing_error · Insufficient funds, sin reintento | La dispersión queda failed (failure_reason: insufficient_balance). Se reintenta sola — misma transacción, sin crear una nueva — en cuanto llega un fondeo adicional ese mismo día, sin configuración y sin tope de intentos |
El reintento automático y el manual comparten el mismo mecanismo — ambos hacen que la misma transacción vuelva a pending y ambos disparan payout.retrying. Ver Reintentar una dispersión fallida por saldo insuficiente.
Related
- Dispersión a Terceros — resumen (v2.2.0) — el flujo que estás migrando.
- Dispersiones a Terceros vía SPEI — resumen — visión general del flujo nuevo.
- Gestión de beneficiarios — alta, validación, edición y CSV masivo.
- Tipos de dispersión —
payout_rules, tipos, límite por beneficiario y vigencia de instrucciones recurrentes. - Seguimiento de dispersiones — consulta de batches, disbursements y reintentos.
- Verificación de firma de webhooks — obligatoria para procesar eventos
payout.*. - Eventos de dispersión — payload de
payout.*. - Eventos de beneficiario — payload de
payee.*. - Eventos de instrucción — payload de
payout_rule.*.
Updated 11 days ago

