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.
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.
| 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 |
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.
$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...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);
});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)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 |
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"
}
}
}| 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 |
| 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"ANDdata.installment.status == "paid".
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"
}
}| 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) |
| 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"ANDdata.order_status == "paid".
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) |
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_statuspara rastrear la transición.
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) │
└─────────────────────────────────────────────────────────────┘
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")
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
-
Responder rápido — Tu endpoint debe responder con HTTP
2xxen menos de 3 segundos. Si necesitas procesamiento pesado, responde200inmediatamente y encola el trabajo. -
Idempotencia — Puedes recibir el mismo evento más de una vez (por reintentos). Usa la combinación
order_uuid+installment.number+installment.statuscomo clave de deduplicación. -
Verificar firma — Siempre valida el header
Signatureantes de procesar. Rechaza con403si no coincide. -
Enrutar por evento — Usa el campo
eventcomo 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; }
-
Manejar campos nulos —
period_uuidyexternal_reference_idpueden sernullpara ciertos tipos de crédito. Tu código debe manejar estos casos. -
Logging — Registra todos los webhooks recibidos (payload + timestamp + signature) para facilitar debugging y auditoría.