🚀 Lanzamiento v2.3: Activa y desactiva métodos de pago para tu Conekta Checkout desde el Panel, sin tocar tu código. Ver documentación →

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 /payees y /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 /payees devolvía directamente en la respuesta.
  • Verificación de firma en tu manejador → Verificar firmas, obligatoria para procesar los eventos payout.*, payee.* y payout_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: scheduled o recurrent). Si hoy mantienes un cron que llama a /transfers, puedes retirarlo.
  • Alta masiva de beneficiarios por CSV.
  • Consulta de estado por API — GET /payouts/disbursements y GET /payouts/batches, sin depender de haber guardado el payload del webhook.
  • Cancelación antes de la ejecución — una instrucción one_time que todavía no se ejecutó se puede eliminar; en /transfers no 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_time creada 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ónAntes (v2.2.0)Ahora (SPEI)Nota
Crear beneficiarioPOST /payees — objeto payee con destinations[]POST /payouts/payees (doc) — objeto external_payee, una sola clabeUn beneficiario por CLABE — ver Un payee ahora es una sola CLABE
Editar beneficiarioEditabas destinations[]PATCH /payouts/payees/{id} — alias, rfc, email, max_amount_limit, o pausar/reactivar; si está rejected, también clabe/name para reintentar validaciónVer Editar un beneficiario tiene límites
Límite máximo por beneficiarioNo existíamax_amount_limit — obligatorio al crear el beneficiarioVer Los beneficiarios necesitan un límite máximo para recibir pagos
Alta masivaNo existíaPOST /payouts/payees/bulk (CSV)Funcionalidad nueva
Enviar dinero puntualPOST /transfers — el pago quedaba disparado en la misma llamada y salía el siguiente día hábilPOST /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 recurrenteLo resolvías con un cron propioPOST /payout_rules con rule_type: scheduled o recurrentPuedes retirar tu scheduler — pero recurrent exige end_date (máx. 12 meses); ver Las instrucciones recurrentes ahora vencen
Cancelar una instrucciónNo existíaDELETE /payout_rules/{id}Sin restricción para scheduled/recurrent; one_time solo mientras no se haya ejecutado
Reintentar un pago fallido por saldoNo existíaAutomá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 momentoEl reintento automático solo cubre el batch del día — ver Seguimiento de dispersiones
Consultar el estado de un pagoVía el payload del webhookGET /payouts/disbursements, GET /payouts/payees/{id}/disbursements (doc)—
Consultar el proceso del díaNo existíaGET /payouts/batches—
Webhooks de dispersiónpayout.created, payout.paid_out, payout.failedpayout.created, payout.in_transit, payout.paid_out, payout.failed, payout.retrying, payout.skipped_limitin_transit, retrying y skipped_limit son nuevos
Webhooks de beneficiariopayee.created, payee.updated, payee.deletedpayee.payout_method.created / .updated / .deleted, payee.max_amount_limit.updatedLos de /payees siguen activos para ese flujo — no son reemplazados, conviven con el stream nuevo
Unidad del montoamount en centavos, enteroamount en centavos, entero — sin cambiomax_amount_limit usa la misma unidad y tipo
FondeoSolo desde saldo generado por cobros en ConektaSaldo 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
  1. Das de alta al beneficiario con POST /payouts/payees, incluyendo su max_amount_limit. La respuesta trae validation_status: pending_validation: el beneficiario existe pero todavía no puede recibir dinero.
  2. Conekta valida la cuenta contra el banco. El resultado te llega por el webhook payee.payout_method.updated, o lo consultas con GET /payouts/payees/{id}.
  3. Creas la instrucción con POST /payout_rules, apuntando al payee_id ya validado.
  4. Conekta ejecuta la instrucción el día que corresponde, cuando hay fondos disponibles en tu cuenta de dispersiones.
  5. Sigues el resultado por los webhooks payout.* o consultando GET /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
❗

Importante

Un PATCH sobre un beneficiario que no está rejected solo edita alias/rfc/email/max_amount_limit o alterna validation_status entre validated_active y validated_inactive — nunca dispara una nueva validación. Si el beneficiario está rejected, un PATCH con clabe y/o name sí lo re-somete: lo regresa a pending_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 nada

Si el PATCH de un beneficiario rejected envía la misma clabe/name que ya tenía, no se dispara una nueva validación. Si la CLABE nueva ya está registrada en otro beneficiario tuyo, el PATCH responde 422 clabe_already_registered sin 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_limit es obligatorio al crear un beneficiario: si falta, o si viene con decimales o como string, la API responde 422 invalid_max_amount_limit_format en los tres casos. Si migras un catálogo grande por CSV, la columna max_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): si amount excede el max_amount_limit del beneficiario, la API responde 422 amount_exceeds_payee_limit de 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 el max_amount_limit por debajo del amount de una instrucción activa, o subiste el amount de la instrucción con un PATCH por encima del límite. El PATCH que sube el monto responde 200 sin revalidar el límite — es el camino silencioso, y solo te enteras el día de la ejecución.
📘

Una dispersión skipped se puede recuperar sola

No es terminal: en cuanto llega un fondeo adicional a tu cuenta de dispersiones, Conekta vuelve a evaluar las instrucciones skipped de ese día contra el max_amount_limit vigente del beneficiario, y las regresa a pending si ya caben. Si no corriges el límite o el monto, se queda skipped indefinidamente — 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.

❗

Importante

Si tu integración vieja creaba el payee y creaba la dispersión inmediatamente después, ese patrón ya no funciona. POST /payout_rules responde 422 si el payee_id no está en validated_active. Espera el webhook payee.payout_method.updated, o haz polling a GET /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.

❗

Importante

Ambos campos exigen un entero. Un valor con decimales o enviado como string responde 422 — invalid_amount_format para amount, invalid_max_amount_limit_format para max_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_renewAl llegar end_dateSe puede recuperar
falsePasa a expired — terminalNo, hay que crear una instrucción nueva
truePasa a paused_by_expiration — deja de ejecutarseSí, 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_rules no es indefinido por diseño. Si necesitas que la recurrencia siga sin intervención manual, usa auto_renew: true y atiende el webhook payout_rule.renewal.upcoming (a 30, 15 y 3 días de vencer) para confirmar la renovación con POST /payout_rules/{id}/confirm_renewal antes de que expire.

Ver Vigencia y renovación de instrucciones recurrentes para el ciclo completo.

Cómo migrar tu integración

  1. Actualiza la versión de API. Cambia el header Accept — o su equivalente en tu SDK — a application/vnd.conekta-v2.3.0+json.
  2. Recrea tus beneficiarios con POST /payouts/payees, uno por CLABE. Si son muchos, usa el alta masiva por CSV.
  3. Define max_amount_limit para cada beneficiario al darlo de alta, o en la columna correspondiente del CSV — es obligatorio, un POST (o una fila del CSV) sin este campo se rechaza.
  4. Guarda el mapeo payee_id viejo → id nuevo en tu base de datos.
  5. Espera validated_active antes de crear cualquier instrucción para ese beneficiario, y maneja el desenlace rejected.
  6. Reemplaza cada POST /transfers por POST /payout_rules: las llamadas ad hoc pasan a rule_type: one_time; lo que tu cron disparaba en fechas fijas o con periodicidad pasa a scheduled o recurrent, y retiras el cron.
  7. Si migras un cron recurrente, define end_date (máximo 12 meses) y decide auto_renew — una instrucción recurrent sin fecha de fin no existe en este flujo.
  8. Actualiza tu manejador de webhooks: agrega payout.in_transit, payout.retrying, payout.skipped_limit, los de beneficiario payee.payout_method.* y payee.max_amount_limit.updated, y — si migras un cron a recurrent — payout_rule.renewal.upcoming y payout_rule.paused_by_expiration, que son los que evitan que tu recurrencia se apague sola al llegar a end_date. La verificación de firma es obligatoria. Ver Referencia de eventos.
  9. 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) o POST /payouts/batches/{fecha}/retry (todas las elegibles de un grupo).
  10. Reemplaza tu lógica de conciliación por GET /payouts/disbursements o GET /payouts/batches en lugar de depender del payload del webhook que guardaste — la comisión se lee al consultar (fee_charged por dispersión, total_fees por grupo), no viaja en el webhook. Concilia contra el grupo cerrado (completed, failed o incomplete), no contra uno todavía processing: 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í que total_fees de un grupo abierto puede no reflejar todavía el cargo completo.
  11. Prueba el flujo completo en modo de pruebas — montos, límites, beneficiarios validados y webhooks recibidos — antes de cambiar a llaves productivas.
  12. 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 real

La 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:

SituaciónAntesAhora
Beneficiario aún no validadoNo existía el estado422 al crear la instrucción; espera validated_active
CLABE que el banco no reconoceFallaba al transferirEl beneficiario queda en rejected en el alta, antes de mover dinero
Monto excede el límite del beneficiarioNo existía422 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áximoNo 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 aplicaba422 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ónprocessing_error · Insufficient funds, sin reintentoLa 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


Did this page help you?