Aprobación dinámica


SPEI Dynamic Approval

Feature flag: spei_dynamic_approval_enabled

¿Qué es?

Por defecto, cuando un cliente realiza una transferencia SPEI a la CLABE generada por Conekta, la orden se marca automáticamente como pagada sin intervención del merchant.

Con SPEI Dynamic Approval, Conekta envía un webhook al endpoint del merchant por cada intento de pago y espera una respuesta síncrona: el merchant responde payable: true para aprobar o payable: false para rechazar. La decisión ocurre en tiempo real, en la misma respuesta HTTP — no hay una llamada API separada.

Aplica tanto para cargos SPEI de un solo uso (spei) como para CLABE recurrente (spei_recurrent).

Sin Dynamic Approval:
  Transferencia recibida → order.paid (automático)

Con Dynamic Approval:
  Transferencia recibida → POST a endpoint del merchant (inbound_payment.payment_attempt)
                        ← { "payable": true }  → order.paid
                        ← { "payable": false } → order sigue en pending_payment (cliente puede reintentar)

Requisitos previos

  1. Tener una cuenta Conekta activa con acceso a la API.
  2. Solicitar la habilitación del flag spei_dynamic_approval_enabled a tu ejecutivo de cuenta o a soporte Conekta.
  3. Exponer un endpoint HTTPS en tu servidor capaz de responder en menos de 2 segundos.
  4. Configurar ese endpoint como webhook en el Panel Conekta con el evento inbound_payment.payment_attempt habilitado.

Diferencia entre SPEI de un solo uso y CLABE recurrente

El comportamiento del evento inbound_payment.payment_attempt varía según el tipo de integración:

SPEI (spei)CLABE recurrente (spei_recurrent)
¿Existe una orden previa?Sí — creada por el merchant antes del pagoNo — no hay orden al momento del intento
charge_id en el payloadApunta al charge de la orden existenteNo hay orden ni charge previo
¿Cómo identificar al cliente?Por customer_id o charge_idSolo por customer_id y los datos del remitente en payment_attempts
¿Qué sucede al aprobar?La orden existente pasa a paidConekta crea la orden y el cargo en ese momento

Para CLABE recurrente, la información del remitente dentro de payment_attempts (nombre, RFC, CLABE origen, monto) es la única fuente de datos disponible para tomar la decisión de aprobación.


Flujo completo

SPEI de un solo uso

Merchant (servidor)           Conekta                      Cliente
        |                        |                             |
        |-- POST /orders -------->|                             |
        |   payment_method: spei  |                             |
        |<-- CLABE + charge_id ---|                             |
        |                        |                             |
        |-- Muestra CLABE al cliente                           |
        |                        |<--- Transferencia SPEI ------|
        |                        |                             |
        |<-- inbound_payment.payment_attempt (con charge_id) ---|
        |                        |                             |
        |-- HTTP 200 ------------>|                             |
        |  { "payable": true }    |                             |
        |                        |                             |
        |<-- Webhook: order.paid -|                             |

CLABE recurrente

Merchant (servidor)           Conekta                      Cliente
        |                        |                             |
        |-- POST /customers ----->|                             |
        |   spei_recurrent        |                             |
        |<-- CLABE permanente ----|                             |
        |                        |                             |
        |-- Comparte CLABE al cliente (una sola vez)           |
        |                        |<--- Transferencia SPEI ------|
        |                        |                             |
        |<-- inbound_payment.payment_attempt (sin orden previa) |
        |                        |                             |
        |-- HTTP 200 ------------>|                             |
        |  { "payable": true }    |                             |
        |                        |                             |
        |<-- Webhook: order.paid -|  (Conekta crea orden/cargo) |

Implementación paso a paso

1a. Crear una orden con SPEI (cargo único)

curl --location --request POST 'https://api.conekta.io/orders' \
  --header 'Accept: application/vnd.conekta-v2.2.0+json' \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer key_xxxxxxxxxxxxxxxx' \
  --data-raw '{
    "currency": "MXN",
    "customer_info": {
      "name": "Juan Pérez",
      "email": "[email protected]",
      "phone": "+525512345678"
    },
    "line_items": [
      {
        "name": "Producto ejemplo",
        "unit_price": 50000,
        "quantity": 1
      }
    ],
    "charges": [
      {
        "payment_method": {
          "type": "spei"
        }
      }
    ]
  }'

La respuesta incluye una clabe única para esa orden. Muéstrasela al cliente para que realice su transferencia.

1b. Crear CLABE recurrente (spei_recurrent)

curl --location --request POST 'https://api.conekta.io/customers' \
  --header 'Accept: application/vnd.conekta-v2.2.0+json' \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer key_xxxxxxxxxxxxxxxx' \
  --data-raw '{
    "name": "Juan Pérez",
    "email": "[email protected]",
    "phone": "+525512345678",
    "payment_sources": [
      {
        "type": "spei_recurrent"
      }
    ]
  }'

La respuesta incluye una clabe permanente asociada al cliente. Cada transferencia que el cliente haga a esa CLABE generará un evento inbound_payment.payment_attempt.


2. Configurar el Webhook

  1. Accede a panel.conekta.comDesarrolladoresWebhooks.
  2. Haz clic en Crear Webhook.
  3. Ingresa la URL de tu endpoint (debe ser HTTPS y públicamente accesible).
  4. Habilita los eventos:
    • inbound_payment.payment_attempt ← obligatorio para el flujo de aprobación
    • order.paid ← para confirmar cuándo la orden quedó pagada
  5. Haz clic en Confirmar — el webhook debe aparecer como activo.

Si usas firewall, permite el IP de Conekta: 52.200.151.182 (puertos 80, 443 o 1025–10001).


3. Implementar el endpoint de aprobación

Cuando Conekta recibe una transferencia SPEI, hace un POST síncrono a tu endpoint. Tu servidor debe:

  • Evaluar si el pago es válido según tus reglas de negocio.
  • Responder con HTTP 200 en menos de 2 segundos.
  • Incluir { "payable": true } para aprobar o { "payable": false } para rechazar.

Si no respondes en 2 segundos, Conekta rechaza el intento automáticamente.

Payload entrante:

{
  "data": {
    "object": {
      "payment_method": {
        "clabe": "734180008033354999",
        "bank": "FINCO PAY",
        "issuing_account_holder_name": null,
        "issuing_account_tax_id": null,
        "issuing_account_bank": null,
        "issuing_account_number": null,
        "receiving_account_holder_name": null,
        "receiving_account_tax_id": null,
        "receiving_account_number": "734180008033354999",
        "receiving_account_bank": "FINCO PAY",
        "reference_number": null,
        "description": null,
        "tracking_code": null,
        "executed_at": null,
        "payment_attempts": [
          {
            "status": null,
            "failure_code": null,
            "failure_message": null,
            "issuing_account_number": "014890568660710403",
            "issuing_account_bank": "40014",
            "tracking_code": "2026041540014TRAPP033414020200",
            "issuing_account_holder_name": "JARED CAMACHO",
            "issuing_account_tax_id": "EACG030618Q9X",
            "reference_number": "8484259",
            "description": "TRANSFERENCIA A JARED CAMACHO",
            "amount": 3000,
            "executed_at": 1776259138
          }
        ],
        "object": "bank_transfer_payment",
        "type": "spei",
        "expires_at": 0
      },
      "charge_id": "69dfe4a2a3ad1f00164177gf",
      "livemode": true,
      "created_at": 1776280738,
      "object": "inbound_payment",
      "amount": 3000,
      "currency": "MXN",
      "customer_id": "cus_2zrEd24vJqaX6ZVXX",
      "customer_custom_reference": null
    },
    "previous_attributes": {}
  }
}

Nota para CLABE recurrente: el charge_id no apunta a una orden previa — no existe orden al momento del intento. La decisión debe tomarse usando customer_id y los datos del remitente en payment_attempts.

Respuesta para aprobar:

HTTP 200
{ "payable": true }

Respuesta para rechazar:

HTTP 200
{ "payable": false }

Ejemplo en Node.js (Express):

app.post('/webhook/spei-approval', async (req, res) => {
  const { type, data } = req.body;

  if (type === 'inbound_payment.payment_attempt') {
    const inboundPayment = data.object;
    const attempt = inboundPayment.payment_method.payment_attempts[0];

    // Aplica tus reglas de negocio.
    // Para cargo único: puedes cruzar charge_id con tus órdenes.
    // Para recurrente: no hay orden previa; valida por customer_id y datos del remitente.
    const isValid = await validatePayment({
      chargeId: inboundPayment.charge_id,       // null si es recurrente sin orden previa
      customerId: inboundPayment.customer_id,
      amount: inboundPayment.amount,
      clabe: inboundPayment.payment_method.clabe,
      senderName: attempt.issuing_account_holder_name,
      senderTaxId: attempt.issuing_account_tax_id,
      trackingCode: attempt.tracking_code,
    });

    // Responder SIEMPRE con HTTP 200; el campo payable decide la acción
    return res.status(200).json({ payable: isValid });
  }

  // order.paid y otros eventos asincrónicos no requieren respuesta especial
  return res.status(200).send('ok');
});

4. Recibir confirmación del pago

Si aprobaste el intento, Conekta dispara el evento asíncrono order.paid con la orden completada. En el caso de CLABE recurrente, Conekta crea la orden y el cargo en este momento.

Este evento es informativo — responde HTTP 200 y procede a cumplir el pedido.


Eventos relevantes

EventoTipoCuándo se dispara
inbound_payment.payment_attemptSíncronoConekta recibió una transferencia SPEI y espera aprobación (max 2s)
order.paidAsíncronoEl merchant aprobó (payable: true) y la orden quedó pagada

Al rechazar (payable: false) no se genera ningún evento. La orden en pending_payment sigue activa (cargo único) o el cliente puede volver a transferir (recurrente).


Estados de la orden

Cargo único (spei)

created → pending_payment ── payable: true  ──→ paid
               ↑
               └── payable: false (sigue en pending_payment, acepta nuevos intentos)

CLABE recurrente (spei_recurrent)

(sin orden previa) ── payable: true  ──→ order creada → paid
                   ── payable: false ──→ (no se crea nada)

Preguntas frecuentes

¿Tengo que llamar a un endpoint de Conekta para aprobar? No. La aprobación ocurre directamente en la respuesta HTTP al webhook. Respondes { "payable": true } y Conekta completa la orden.

¿Qué pasa si mi servidor tarda más de 2 segundos? Conekta rechaza el intento automáticamente.

¿El cliente puede volver a transferir si fue rechazado? Sí. Para cargo único, la orden sigue en pending_payment. Para recurrente, el cliente puede transferir nuevamente en cualquier momento; cada intento genera un nuevo evento.

¿Puedo activarlo solo para algunas órdenes? No. El flag spei_dynamic_approval_enabled aplica a nivel de company. Si está habilitado, todos los pagos SPEI del merchant pasan por el flujo de aprobación.


Soporte