Skip to content

Instantly share code, notes, and snippets.

@jluisflo
Created March 12, 2026 18:58
Show Gist options
  • Select an option

  • Save jluisflo/51f4f6fa15377265772242c5e74f277e to your computer and use it in GitHub Desktop.

Select an option

Save jluisflo/51f4f6fa15377265772242c5e74f277e to your computer and use it in GitHub Desktop.
mpay Webhooks — Guía de Integración para Sistemas Externos

mpay Webhooks — Guía de Integración para Sistemas Externos

Descripción General

mpay emite webhooks HTTP para notificar a sistemas externos sobre eventos relevantes en el ciclo de vida de créditos, cuotas y montos asignados. Los webhooks se envían al endpoint configurado por cada client (webhook_url) como requests POST con body JSON.


Configuración

Cada client tiene dos campos en su configuración:

Campo Descripción
webhook_url URL HTTPS donde se enviarán los webhooks
webhook_secret Clave secreta para firmar los requests (HMAC-SHA256)

Contacta al administrador de mpay para configurar estos valores.


Protocolo de Entrega

Propiedad Valor
Método HTTP POST
Content-Type application/json
Header de firma Signature
Algoritmo HMAC-SHA256
Timeout 3 segundos
Reintentos 3 (con backoff exponencial)
SSL Requerido

Verificación de Firma

Cada request incluye un header Signature con un hash HMAC-SHA256 del body usando tu webhook_secret. Siempre verifica la firma antes de procesar el webhook.

PHP

$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_SIGNATURE'] ?? '';

$expected = hash_hmac('sha256', $payload, $webhookSecret);

if (! hash_equals($expected, $signature)) {
    http_response_code(403);
    exit('Firma inválida');
}

$event = json_decode($payload, true);
// Procesar $event...

Node.js

const crypto = require('crypto');

app.post('/webhook', (req, res) => {
  const signature = req.headers['signature'];
  const payload = JSON.stringify(req.body);
  const expected = crypto.createHmac('sha256', WEBHOOK_SECRET).update(payload).digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    return res.status(403).send('Firma inválida');
  }

  // Responder inmediatamente, procesar después
  res.status(200).send('OK');
  processEvent(req.body);
});

Python

import hmac
import hashlib

def verify_signature(payload_bytes: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), payload_bytes, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

Estructura General del Payload

Todos los webhooks siguen esta estructura:

{
  "event": "nombre_del_evento",
  "timestamp": "2026-03-12T14:30:45.000000Z",
  "data": { ... }
}
Campo Tipo Descripción
event string Identificador único del tipo de evento
timestamp string (ISO 8601) Momento en que se generó el evento
data object Datos específicos del evento

Eventos

1. installment_state_changed

Se emite cada vez que cambia el estado de una cuota (installment). Este es el evento principal para detectar pagos de cuotas.

{
  "event": "installment_state_changed",
  "timestamp": "2026-03-12T14:30:45.000000Z",
  "data": {
    "customer_uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "order_uuid": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
    "order_status": "active",
    "period_uuid": "550e8400-e29b-41d4-a716-446655440000",
    "installment": {
      "status": "paid",
      "amount": 100000,
      "total_amount": 150000,
      "total_pending_amount": 0,
      "number": 1,
      "expired_at": "2026-04-12T00:00:00.000000Z"
    }
  }
}

Campos del payload

Campo Tipo Descripción
data.customer_uuid UUID Identificador del cliente
data.order_uuid UUID Identificador del crédito
data.order_status string Estado actual de la orden/crédito
data.period_uuid UUID | null Identificador del periodo (null para créditos externos)
data.installment.status string Nuevo estado de la cuota
data.installment.amount integer Monto base de la cuota (en centavos)
data.installment.total_amount integer Monto total de la cuota incluyendo intereses, mora, etc. (en centavos)
data.installment.total_pending_amount integer Monto pendiente de pago (en centavos)
data.installment.number integer Número secuencial de la cuota (1, 2, 3...)
data.installment.expired_at string (ISO 8601) Fecha de vencimiento de la cuota

Estados posibles de cuota (installment.status)

Estado Significado ¿Acción requerida?
pending Cuota creada pero no activa aún No — informativo
active Cuota activa, pendiente de pago No — informativo, indica que esta cuota es la siguiente a pagar
due Cuota vencida (en periodo de gracia) Posible — alertar al usuario
in_arrears Cuota en mora Sí — notificar al usuario, aplicar reglas de mora
paid Cuota pagada completamente Sí — registrar pago, actualizar estado interno
cancel Cuota cancelada Informativo

Para detectar pago de cuota: Filtrar por event == "installment_state_changed" AND data.installment.status == "paid".


2. order_state_changed

Se emite cuando cambia el estado general del crédito. Esto incluye el desembolso inicial y cuando el crédito se paga completamente.

{
  "event": "order_state_changed",
  "timestamp": "2026-03-12T14:30:45.000000Z",
  "data": {
    "customer_uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "order_uuid": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
    "order_status": "paid",
    "period_uuid": "550e8400-e29b-41d4-a716-446655440000",
    "period_name": "3 meses",
    "installment_count": 3,
    "external_reference_id": "EXT-12345"
  }
}

Campos del payload

Campo Tipo Descripción
data.customer_uuid UUID Identificador del cliente
data.order_uuid UUID Identificador del crédito
data.order_status string Nuevo estado del crédito
data.period_uuid UUID | null Identificador del periodo
data.period_name string Nombre legible del periodo ("3 meses", "6 quincenas")
data.installment_count integer Número total de cuotas
data.external_reference_id string | null ID de referencia externa (para créditos creados vía API)

Estados posibles de orden (order_status)

Estado Significado ¿Acción requerida?
active Crédito activo con cuotas pendientes Informativo
due Crédito con cuota(s) vencida(s) Posible — alertar
in_arrears Crédito en mora Sí — aplicar reglas de mora
paid Crédito completamente pagado Sí — cerrar crédito, liberar línea
cancel Crédito cancelado Sí — actualizar estado
processing_payment Pago en proceso Informativo

Para detectar crédito completamente pagado: Filtrar por event == "order_state_changed" AND data.order_status == "paid".


3. assigned_amount_created

Se emite cuando se asigna un monto de crédito disponible a un cliente.

{
  "event": "assigned_amount_created",
  "timestamp": "2026-03-12T14:30:45.000000Z",
  "data": {
    "customer_uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "document_type": "DPI",
    "document_value": "1234567890101",
    "type": "manual",
    "status": "active",
    "amount": 500000
  }
}
Campo Tipo Descripción
data.customer_uuid UUID Identificador del cliente
data.document_type string Tipo de documento (DPI, NIT, etc.)
data.document_value string Número de documento
data.type string Tipo de asignación: manual o automatic
data.status string Estado de la asignación
data.amount integer Monto asignado (en centavos)

4. assigned_amount_updated

Se emite cuando se actualiza un monto de crédito previamente asignado.

{
  "event": "assigned_amount_updated",
  "timestamp": "2026-03-12T14:30:45.000000Z",
  "data": {
    "customer_uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "document_type": "DPI",
    "document_value": "1234567890101",
    "type": "manual",
    "status": "active",
    "previous_status": "pending",
    "amount": 500000
  }
}

Incluye previous_status para rastrear la transición.


Secuencia de Eventos: Pago de Cuota

Cuando un pago se procesa exitosamente, los webhooks se emiten en este orden:

┌─────────────────────────────────────────────────────────────┐
│ Paso 1: Pago aplicado a cuota #N                            │
│                                                             │
│  → installment_state_changed                                │
│    installment.status = "paid"                              │
│    installment.number = N                                   │
│    installment.total_pending_amount = 0                     │
├─────────────────────────────────────────────────────────────┤
│ Paso 2: Siguiente cuota activada                            │
│                                                             │
│  → installment_state_changed                                │
│    installment.status = "active"                            │
│    installment.number = N+1                                 │
├─────────────────────────────────────────────────────────────┤
│ Paso 3: (Solo si N era la última cuota)                     │
│                                                             │
│  → order_state_changed                                      │
│    order_status = "paid"                                    │
│    (El crédito fue saldado completamente)                   │
└─────────────────────────────────────────────────────────────┘

Secuencia de Eventos: Ciclo de Vida Completo

Crédito desembolsado
  → order_state_changed (order_status: "active")

Cuota #1 vence
  → installment_state_changed (status: "due")
  → order_state_changed (order_status: "due")

Cuota #1 entra en mora
  → installment_state_changed (status: "in_arrears")
  → order_state_changed (order_status: "in_arrears")

Cuota #1 pagada
  → installment_state_changed (status: "paid", number: 1)
  → installment_state_changed (status: "active", number: 2)
  → order_state_changed (order_status: "active")

Cuota #2 pagada
  → installment_state_changed (status: "paid", number: 2)
  → installment_state_changed (status: "active", number: 3)

Cuota #3 (última) pagada
  → installment_state_changed (status: "paid", number: 3)
  → order_state_changed (order_status: "paid")

Nota sobre Montos

Todos los montos monetarios están expresados en centavos (integer).

Para obtener el valor en la unidad monetaria, dividir entre 100.

Ejemplo: amount: 150000 = Q1,500.00


Recomendaciones de Implementación

  1. Responder rápido — Tu endpoint debe responder con HTTP 2xx en menos de 3 segundos. Si necesitas procesamiento pesado, responde 200 inmediatamente y encola el trabajo.

  2. Idempotencia — Puedes recibir el mismo evento más de una vez (por reintentos). Usa la combinación order_uuid + installment.number + installment.status como clave de deduplicación.

  3. Verificar firma — Siempre valida el header Signature antes de procesar. Rechaza con 403 si no coincide.

  4. Enrutar por evento — Usa el campo event como discriminador:

    // Pseudocódigo
    switch (payload.event) {
      case 'installment_state_changed':
        if (payload.data.installment.status === 'paid') {
          handleInstallmentPaid(payload.data);
        }
        break;
      case 'order_state_changed':
        if (payload.data.order_status === 'paid') {
          handleCreditFullyPaid(payload.data);
        }
        break;
    }
  5. Manejar campos nulosperiod_uuid y external_reference_id pueden ser null para ciertos tipos de crédito. Tu código debe manejar estos casos.

  6. Logging — Registra todos los webhooks recibidos (payload + timestamp + signature) para facilitar debugging y auditoría.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment