Saltar al contenido

Guías

Webhooks

Cuando un pago se completa, falla, se devuelve o recibe un contracargo, e-Payouts hace un POST JSON a la URL de notificación del módulo (tu panel de comercio → Sitios → módulo). Es la única confirmación válida de un pago.

Cuándo se envía

Se envía un aviso automáticamente en estos casos, siempre con el mismo formato:

event / statusCuándoQué hacer
payment.completed
completed
El cliente pagó y el procesador lo confirmó.Si sandbox es false y amount y currency coinciden con tu pedido, marcalo como pagado y entregalo.
payment.failed
failed
El pago fue rechazado o no se completó (p. ej. el banco lo rechazó o el cliente no confirmó en la app).No entregues el pedido. Si querés, ofrecele volver a pagar: creá un pago nuevo (podés usar el mismo ucode; tendrá otro code).
payment.refunded
refunded · completed
Se hizo una devolución del pago (pedida por vos o por soporte). status es refunded si se devolvió el total, o completed si fue parcial. El detalle va en adjustment.Registrá la devolución en tu pedido (adjustment.amount). El monto ya se descontó de tu saldo.
payment.charged_back
charged_back · completed
El cliente reclamó el cobro a su banco y el contracargo quedó procesado. status es charged_back si cubrió el total, o completed si fue parcial.Tratalo como una devolución no pedida: si todavía no entregaste el pedido, no lo entregues.

No se envían avisos por los estados intermedios (created, pending), ni cuando se pide una devolución: solo cuando queda hecha. Las devoluciones y los contracargos también se consultan con GET /v2/adjustments, y te avisamos por correo.

Reenvío manual (tu panel de comercio → Pagos → detalle): repite el último aviso del pago tal cual se envió (mismo event y status). Si el pago nunca tuvo un aviso, envía uno con su estado actual, que puede ser cualquiera de los de la API (p. ej. payment.pending o payment.refunded).

Campos

El cuerpo es un objeto JSON (Content-Type: application/json) con estos campos, siempre en este orden:

CampoTipoDescripción
eventstringQué pasó: payment.completed, payment.failed, payment.refunded o payment.charged_back. También llega en la cabecera X-Epayouts-Event.
sandboxbooleantrue en pagos de prueba: nunca entregues un pedido real por un aviso con sandbox true.
codestringCódigo del pago (UUID): el mismo que devuelve la API. Con él podés consultar el detalle: GET /v2/payments/{code}.
ucodestring | nullTu código de pedido, tal como llegó al paymentbox; null si no mandaste ninguno.
methodstringMétodo con que se inició el pago (el código que usa tu integración): procesá el aviso con este.
effective_methodstringMétodo que efectivamente lo cobró. Distinto de method solo si ese método tiene un ruteo (p. ej. se inició como ppal y se cobró con 16cards); es informativo.
amountstringMonto que pediste, con dos decimales y punto ("49.90"). Comparalo con tu pedido.
currencystringMoneda que pediste (ISO 4217).
statusstringEstado del pago en el momento del aviso: completed, failed, refunded o charged_back (en devoluciones y contracargos parciales sigue completed).
created_atstringCuándo se creó el pago (ISO 8601, UTC). No es la hora del aviso.
charged_amountstringSolo si el método elegido no acepta tu moneda (p. ej. MB WAY y Multibanco cobran en EUR): lo que se le cobró al cliente, con la tasa del momento. No lo uses para validar el pedido.
charged_currencystringMoneda de charged_amount (solo junto con charged_amount).
adjustmentobjectSolo en payment.refunded y payment.charged_back: id (el de GET /v2/adjustments), type (refund | chargeback), amount y currency (lo devuelto o reclamado, en la moneda en que se cobró) y reason (motivo o null).

Decidí qué hacer según event, no según status: en una devolución o un contracargo parcial, status sigue en completed. El aviso no trae datos del cliente, comisión ni liquidación: si los necesitás, consultá el pago por code.

Ejemplos

Pago completado

JSON
{
  "event": "payment.completed",
  "sandbox": false,
  "code":        "3f6b2d10-9a4c-4e21-8b7f-5c0d1e2f3a4b",
  "ucode": "PEDIDO-1042",
  "method": "sofort",
  "effective_method": "sofort",
  "amount": "49.90",
  "currency": "EUR",
  "status": "completed",
  "created_at": "2026-09-18T14:33:22+00:00"
}

Pago fallido

JSON
{
  "event": "payment.failed",
  "sandbox": false,
  "code":        "5e7a9c1b-3d5f-4a2c-8e4b-6d8f0a2c4e6a",
  "ucode": "PEDIDO-1044",
  "method": "ppal",
  "effective_method": "ppal",
  "amount": "35.00",
  "currency": "EUR",
  "status": "failed",
  "created_at": "2026-09-29T09:12:05+00:00"
}

Pago completado cobrado en otra moneda

Pedido en USD pagado con MB WAY, que cobra en EUR: amount y currency son los de tu pedido; charged_amount y charged_currency, lo que pagó el cliente.

JSON
{
  "event": "payment.completed",
  "sandbox": false,
  "code":        "8a2c4e6f-1b3d-4f5a-9c7e-0d2f4a6b8c1e",
  "ucode": "PEDIDO-1043",
  "method": "mbway",
  "effective_method": "mbway",
  "amount": "12.00",
  "currency": "USD",
  "status": "completed",
  "created_at": "2026-09-30T14:35:10+00:00",
  "charged_amount": "10.65",
  "charged_currency": "EUR"
}

Pago de un método con ruteo

Tu integración usa ppal, pero ese método está ruteado y el pago se cobró con 16cards: procesalo como ppal.

JSON
{
  "event": "payment.completed",
  "sandbox": false,
  "code":        "c2e4a6b8-0d1f-4e3a-9b5c-7d9e1f3a5b7c",
  "ucode": "PEDIDO-1045",
  "method": "ppal",
  "effective_method": "16cards",
  "amount": "20.00",
  "currency": "EUR",
  "status": "completed",
  "created_at": "2026-09-30T11:02:48+00:00"
}

Devolución total

JSON
{
  "event": "payment.refunded",
  "sandbox": false,
  "code":        "3f6b2d10-9a4c-4e21-8b7f-5c0d1e2f3a4b",
  "ucode": "PEDIDO-1042",
  "method": "sofort",
  "effective_method": "sofort",
  "amount": "49.90",
  "currency": "EUR",
  "status": "refunded",
  "created_at": "2026-09-18T14:33:22+00:00",
  "adjustment": {
    "id": "c4d5e6f7-0a1b-4c2d-8e3f-4a5b6c7d8e9f",
    "type": "refund",
    "amount": "49.90",
    "currency": "EUR",
    "reason": "El cliente canceló el pedido"
  }
}

Contracargo parcial

El banco del cliente reclamó 30,00 € de un pago de 80,00 €: el pago sigue completed.

JSON
{
  "event": "payment.charged_back",
  "sandbox": false,
  "code":        "9b8a7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
  "ucode": "PEDIDO-0988",
  "method": "16cards",
  "effective_method": "16cards",
  "amount": "80.00",
  "currency": "EUR",
  "status": "completed",
  "created_at": "2026-09-02T18:20:11+00:00",
  "adjustment": {
    "id": "d1e2f3a4-5b6c-4d7e-8f9a-0b1c2d3e4f5a",
    "type": "chargeback",
    "amount": "30.00",
    "currency": "EUR",
    "reason": null
  }
}

Los avisos de pagos de prueba tienen el mismo formato, con "sandbox": true: ver ejemplo en Modo prueba →

Cabeceras

HTTP
POST /tu-url-de-notificacion HTTP/1.1
Content-Type: application/json
X-Epayouts-Event: payment.completed
X-Epayouts-Delivery: 1234
X-Epayouts-Signature: t=1790380000,v1=5d41402abc4b2a76b9719d911017c592…
X-Epayouts-Sandbox: 1        (solo pagos de prueba)
  • X-Epayouts-Delivery — número del aviso; es el mismo en cada reintento del mismo aviso.
  • X-Epayouts-Signature — t = segundos Unix del envío; v1 = firma (ver abajo). Cambia en cada intento.

Verificar la firma

v1 = HMAC-SHA256("<t>.<cuerpo crudo>", clave de firma de webhooks). La clave está en tu panel de comercio → API y es distinta del secreto de la API. Rechazá avisos con t de más de 5 minutos.

PHP
<?php
$raw = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_EPAYOUTS_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $parts);   // t=…&v1=…
$expected = hash_hmac('sha256', ($parts['t'] ?? '') . '.' . $raw, getenv('EPAYOUTS_WEBHOOK_KEY'));

if (!hash_equals($expected, $parts['v1'] ?? '') || abs(time() - (int) ($parts['t'] ?? 0)) > 300) {
    http_response_code(400);
    exit;
}
$event = json_decode($raw, true);
if ($event['sandbox'] === false) {
    switch ($event['event']) {   // usá event, no status: en devoluciones parciales status sigue en completed
        case 'payment.completed':    // marcar el pedido $event['ucode'] como pagado (si ya lo estaba, no hagas nada)
            break;
        case 'payment.failed':       // dejar el pedido sin pagar
            break;
        case 'payment.refunded':     // registrar la devolución: $event['adjustment']['amount']
        case 'payment.charged_back': // registrar el contracargo: $event['adjustment']['amount']
            break;
    }
}
http_response_code(200);

Reintentos

Respondé HTTP 2xx en menos de 10 segundos. Si no, reintentamos con espera creciente (por defecto 1, 2, 4, 8… minutos, hasta 6 horas entre intentos) hasta un máximo de intentos; al agotarse se abre un caso con soporte. Un mismo aviso puede llegar más de una vez: antes de procesarlo, fijate por code y status si ya lo procesaste, así no marcás dos veces el mismo pedido.

La URL de notificación tiene que ser https y pública. Podés pedir un reenvío desde tu panel de comercio → Pagos → detalle.