Índice de la documentación

Webhooks

Los webhooks son notificaciones HTTP POST que Avirato Payments envia automaticamente a tu sistema cuando ocurre un evento relevante: un pago se completa, un reembolso se procesa, una captura se confirma, etc.

Por que son importantes

Los webhooks son la forma mas fiable de saber que una operacion se ha completado. A diferencia del redirect (urlOk/urlKo), que depende de que el cliente vuelva a tu pagina:

  • Los webhooks llegan siempre, incluso si el cliente cierra el navegador
  • Llegan unos segundos despues de que el procesador confirme la operacion
  • No dependen del comportamiento del usuario
Recomendacion: Usa los webhooks como fuente principal de confirmacion de operaciones. Usa el redirect y el polling como complementos.

Como configurar tu URL de webhooks

Desde Integraciones > Configuración > URLs de webhooks puedes registrar una URL pública por entorno (Live y Test). Cada URL puede activarse y desactivarse por separado. Recomendamos:

  • URL HTTPS (no HTTP) con certificado válido.
  • Endpoint dedicado a Avirato Payments (por ejemplo https://tu-dominio.com/avirato-payments/webhooks) que sepa procesar el formato común que se describe más abajo.
  • Necesitas tener una API key activa para el entorno antes de configurar su URL. Si no existe, el dashboard te lo indica con un mensaje claro y un código 422.
Importante: solo se envían los webhooks generados a partir del momento en que la URL está configurada y activa. Eventos previos no se reenvían retroactivamente.

Verificación de autenticidad (firma HMAC)

Todos los webhooks live van firmados con HMAC-SHA256 usando el mismo client_secret que se te muestra al crear (o rotar) la API key del entorno. Llegan estas dos cabeceras adicionales:

CabeceraValor
X-Avirato-TimestampTimestamp Unix (segundos) del momento del envío
X-Avirato-Signaturet={timestamp},v1={hmac} — donde hmac = hash_hmac('sha256', timestamp + "." + body, client_secret)

Ejemplo de verificación en PHP:

php
$timestamp = $_SERVER['HTTP_X_AVIRATO_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_AVIRATO_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');

if (!preg_match('/^t=(\d+),v1=([a-f0-9]+)$/', $signature, $m)) {
    http_response_code(400);
    exit;
}

$expected = hash_hmac('sha256', $timestamp . '.' . $body, $clientSecret);
if (!hash_equals($expected, $m[2])) {
    http_response_code(401);
    exit;
}

En Test, si tienes API key test creada, los webhooks de prueba se firman también con su secret. Si no, se envían sin firma para que puedas explorar el formato sin obligación de crear API key test todavía.

Política de reintentos

Cómo funciona en una línea

Cada vez que Avirato Payments te envia un webhook y tu endpoint no responde con HTTP 200, lo reintenta mas tarde. La espera entre cada fallo y el siguiente reintento crece: empieza en 10 minutos y termina en 48 horas. Tras un total de 10 intentos sin éxito Avirato Payments deja de reintentarlo automáticamente.

Cronograma completo

La tabla muestra, para un webhook que falla en cada intento, el momento exacto en el que tu endpoint recibe la siguiente llamada, contado desde que se produjo el evento original (autorización, refund, etc.).

IntentoQué esCuándo lo recibes (desde el evento)
1Envío inicialen el momento (≈ 0)
21.er reintento10 min después
32.º reintento30 min después
43.er reintento1 h 10 min después
54.º reintento2 h 40 min después
65.º reintento5 h 40 min después
76.º reintento11 h 40 min después
87.º reintento23 h 40 min después
98.º reintento1 día 23 h 40 min después
109.º (y último) reintento≈ 4 días después (95 h 40 min)
—a partir de aquí ya no se reintenta automáticamente

En total son 10 intentos (un envío inicial + 9 reintentos) repartidos en una ventana de unos 4 días. El reloj no se reinicia en cada intento: cuanto más tarde respondes correctamente, más se habrán espaciado los siguientes reintentos.

Cómo saber en qué intento vamos

En el dashboard, en Integraciones > Actividad > Webhooks enviados (entorno Live), cada fila del historial muestra:

  • Intentos: número de intentos consumidos hasta ahora (1 a 10).
  • Último intento: la fecha y hora del último envío.
  • HTTP: el código HTTP que devolvió tu endpoint en ese último envío.
  • Estado: Entregado (success), Pendiente (todavía con reintentos por delante).

Si el endpoint sigue cayendo tras los 10 intentos

Si tras los 10 intentos (≈ 4 días) tu endpoint no ha respondido correctamente ni una sola vez, Avirato Payments deja de reintentarlo automáticamente aunque más tarde repares tu sistema. En ese caso:

  1. Reactiva tu endpoint y verifica que ya responde correctamente con webhooks normales.
  2. Para los eventos que quedaron pendientes, abre Integraciones > Actividad > Webhooks enviados, identifica los webhooks con estado Pendiente y fuerza el reenvío manual con el botón Reenviar. Cada reenvío manual hace una llamada HTTP nueva a tu endpoint con el mismo payload.

Si desactivas o eliminas tu URL de webhooks: los eventos siguen encolándose en Avirato Payments. El cron sigue contando reintentos para cada uno, así que si reactivas la URL dentro de la ventana de ≈ 4 días los pendientes se entregarán automáticamente en el siguiente tick (siempre que aún queden intentos disponibles para ese webhook). Pasada esa ventana los eventos quedan abandonados y no se vuelven a enviar.

Comportamiento del reenvio manual

  • Si reenvias manualmente un webhook que estaba pendiente de reintentos y el reenvio tiene exito, los reintentos automaticos se detienen — el webhook ya se considera entregado.
  • Puedes reenviar un webhook que ya fue entregado correctamente. El dashboard te pedira confirmacion antes para evitar duplicados accidentales, pero el reenvio se ejecuta igualmente si lo confirmas.

Fuente de verdad del estado del pago (Live)

En el entorno Live, las notificaciones del procesador tienen la ultima palabra sobre el estado real del pago. Esto significa que un pago puede cambiar de estado despues de que tu sistema lo haya tratado como completado.

Casos tipicos en los que puede llegar un webhook posterior que revierte un exito previo:

  • Chargeback: el titular reclama el cargo a su banco
  • Fraude detectado a posteriori: el procesador identifica una transaccion fraudulenta dias despues
  • Reversion bancaria: el banco emisor revierte la operacion
  • Correccion del procesador: una notificacion previa contenia datos incompletos o incorrectos

Cuando recibimos uno de estos eventos en Live, actualizamos el estado del pago en nuestro sistema (por ejemplo, de AUTHORISED a FAILED) y te enviamos el webhook correspondiente. Tu integracion debe:

  • Aceptar webhooks asincronos que cambien el resultado de un pago previamente exitoso
  • Tratar los webhooks (no la respuesta inmediata ni el redirect) como la referencia para la conciliacion y el estado final
  • Volver a consultar la API (GET /payment/session/{sessionId}) cuando necesites el ultimo estado conocido
Test vs Live: en Test los webhooks los genera nuestro propio simulador y reflejan unicamente la simulacion que has lanzado. No hay chargebacks ni reverts en Test.

Formato general

Todos los webhooks llegan como POST a tu URL con este formato:

Cabeceras

CabeceraValor
Content-Typeapplication/json
webcodeEl webcode asociado al pago (ej: SHOP01)

Body

json
{
  "event_code": "AUTHORISATION",
  "data": {
    ...campos especificos del evento...
  }
}

Respuesta esperada

Tu endpoint debe responder con HTTP 200 y un body JSON:

json
{
  "status": "success"
}

Si tu endpoint no responde correctamente, el webhook se reintentara automaticamente con backoff exponencial.


Tipos de evento

AUTHORISATION — Pago completado (online o POS)

Se envia cuando un pago (online o POS) se autoriza correctamente o falla.

json
{
  "event_code": "AUTHORISATION",
  "data": {
    "amount": 15000,
    "currency": "EUR",
    "paymentReference": "SHOP01-PXT-17195000005678",
    "paymentStatus": "AUTHORISED",
    "description": "Pedido #12345 - Servicio premium",
    "customReference": "ORD-2026-12345",
    "paymentSource": "external_api"
  }
}
CampoSiempre presenteDescripcion
amountSiImporte en centimos
currencySiCodigo ISO 4217
paymentReferenceSiReferencia del pago
paymentStatusSiAUTHORISED si exitoso, FAILED si fallo
descriptionSiDescripcion del pago
customReferenceNoTu referencia interna (si la proporcionaste al crear la sesion/pago)
paymentSourceNoOrigen del pago (ej: external_api)
failedReasonNoMotivo del fallo (solo si el pago fallo)

Como identificar el tipo de pago por la referencia:

Infijo en la referenciaTipoEntorno
-PXT-Pago onlineLive
-PXTT-Pago onlineTest
-POSX-Pago POSLive
-POSXT-Pago POSTest

Ejemplo de pago POS exitoso:

json
{
  "event_code": "AUTHORISATION",
  "data": {
    "amount": 8500,
    "currency": "EUR",
    "paymentReference": "SHOP01-POSX-17195300001234",
    "paymentStatus": "AUTHORISED",
    "description": "Venta mostrador #205",
    "customReference": "VENTA-205-20260628",
    "paymentSource": "external_api"
  }
}

Ejemplo de pago fallido (rechazado):

json
{
  "event_code": "AUTHORISATION",
  "data": {
    "amount": 15000,
    "currency": "EUR",
    "paymentReference": "SHOP01-PXT-17195000005678",
    "paymentStatus": "FAILED",
    "description": "Pedido #12345",
    "failedReason": "Refused",
    "paymentSource": "external_api"
  }
}
Importante: Los webhooks se envian tanto para pagos exitosos como fallidos. Comprueba siempre el campo paymentStatus para determinar el resultado.

REFUND — Reembolso procesado

Se envia cuando un reembolso se completa (exito o fallo) por el procesador.

json
{
  "event_code": "REFUND",
  "data": {
    "amount": 5000,
    "currency": "EUR",
    "paymentReference": "SHOP01-PXT-17195000005678",
    "refundReason": "CUSTOMER REQUEST",
    "refundReference": "SHOP01-RFX-17195234567890",
    "status": "success",
    "customReference": "ORD-2026-12345",
    "paymentSource": "external_api"
  }
}
CampoSiempre presenteDescripcion
amountSiImporte reembolsado en centimos
currencySiCodigo ISO 4217
paymentReferenceSiReferencia del pago original
refundReasonSiMotivo del reembolso (CUSTOMER REQUEST, FRAUD, RETURN, DUPLICATE, OTHER)
refundReferenceSiReferencia de esta operacion de reembolso
statusSisuccess o failed
customReferenceNoTu referencia interna
paymentSourceNoOrigen del pago
failedReasonNoMotivo del fallo (solo si status: "failed")

Ejemplo de reembolso fallido:

json
{
  "event_code": "REFUND",
  "data": {
    "amount": 5000,
    "currency": "EUR",
    "paymentReference": "SHOP01-PXT-17195000005678",
    "refundReason": "CUSTOMER REQUEST",
    "refundReference": "SHOP01-RFX-17195234567890",
    "status": "failed",
    "failedReason": "Insufficient balance"
  }
}

PREAUTHORISATION — Resultado de captura, cancelacion o extension

Se envia cuando una operacion sobre una preautorizacion se completa. El campo action indica el tipo de operacion.

Captura completada

json
{
  "event_code": "PREAUTHORISATION",
  "data": {
    "paymentReference": "SHOP01-PXT-17195123456789",
    "action": "capture",
    "result": "success",
    "amount": 45000,
    "currency": "EUR",
    "customReference": "ORD-2026-67890",
    "paymentSource": "external_api"
  }
}

Cancelacion completada

json
{
  "event_code": "PREAUTHORISATION",
  "data": {
    "paymentReference": "SHOP01-PXT-17195123456789",
    "action": "cancellation",
    "result": "success",
    "customReference": "ORD-2026-67890",
    "paymentSource": "external_api"
  }
}

Extension completada

json
{
  "event_code": "PREAUTHORISATION",
  "data": {
    "paymentReference": "SHOP01-PXT-17195123456789",
    "action": "extend",
    "result": "success",
    "expirationDate": "2026-08-15T00:00:00Z",
    "customReference": "ORD-2026-67890",
    "paymentSource": "external_api"
  }
}
CampoSiempre presenteDescripcion
paymentReferenceSiReferencia del pago preautorizado
actionSicapture, cancellation o extend
resultSisuccess o failure
amountSolo captureImporte capturado en centimos
currencySolo captureCodigo ISO 4217
expirationDateSolo extend exitosoNueva fecha de expiracion
customReferenceNoTu referencia interna
paymentSourceNoOrigen del pago
failedReasonNoMotivo del fallo (solo si result: "failure")

Ejemplo de captura fallida (importe excede el autorizado):

json
{
  "event_code": "PREAUTHORISATION",
  "data": {
    "paymentReference": "SHOP01-PXT-17195123456789",
    "action": "capture",
    "result": "failure",
    "failedReason": "Payment already captured, cannot capture again"
  }
}

CHARGEBACK — Contracargo

Se envia cuando un titular de tarjeta inicia un contracargo contra un pago. Los contracargos son procesados por el emisor de la tarjeta y pueden tardar dias o semanas en resolverse.

json
{
  "event_code": "CHARGEBACK",
  "data": {
    "paymentReference": "SHOP01-PXT-17195123456789",
    "chargebackReference": "SHOP01-CHB-17195123456789",
    "action": "chargeback",
    "result": "success",
    "amount": 15000,
    "currency": "EUR",
    "reason": "Fraudulent transaction",
    "chargebackReasonCode": "10.4",
    "chargebackSchemeCode": "VISA_10_4",
    "defensePeriodEndsAt": "2026-06-15",
    "defendable": true,
    "disputeStatus": "Undefended",
    "paymentSource": "external_api"
  }
}
CampoSiempre presenteDescripcion
paymentReferenceSiReferencia del pago original
chargebackReferenceSiReferencia unica del contracargo
actionSiSiempre chargeback
resultSisuccess o failure
amountSiImporte en centimos
currencySiMoneda ISO 4217
reasonSiMotivo del contracargo
chargebackReasonCodeNoCodigo de motivo del esquema de tarjeta
chargebackSchemeCodeNoCodigo del esquema (Visa, Mastercard, etc.)
defensePeriodEndsAtNoFecha limite para defender el contracargo
defendableNotrue si puedes disputar el contracargo
disputeStatusNoEstado de la disputa (Undefended, Defended, etc.)
failedReasonNoMotivo si result es failure
paymentSourceNoOrigen del pago (external_api, etc.)

NOTIFICATION_OF_CHARGEBACK — Aviso previo de contracargo

Aviso anticipado de que un contracargo puede estar en camino. No todos los procesadores lo envian. Te permite prepararte antes de que el contracargo formal se procese.

json
{
  "event_code": "NOTIFICATION_OF_CHARGEBACK",
  "data": {
    "paymentReference": "SHOP01-PXT-17195123456789",
    "chargebackReference": "SHOP01-CHB-17195123456789",
    "action": "notification_of_chargeback",
    "result": "success",
    "amount": 15000,
    "currency": "EUR",
    "reason": "Cardholder disputes transaction",
    "chargebackReasonCode": "10.4",
    "chargebackSchemeCode": "VISA_10_4",
    "defensePeriodEndsAt": "2026-06-15",
    "defendable": true,
    "disputeStatus": "Undefended",
    "autoDefended": false,
    "arn": "74027600000012345678901",
    "paymentSource": "external_api"
  }
}

Contiene los mismos campos que CHARGEBACK mas dos campos adicionales opcionales:

CampoDescripcion
autoDefendedtrue si el procesador defendio automaticamente el contracargo
arnAcquirer Reference Number — identificador de la transaccion del adquirente

Pagos recurrentes

Los pagos recurrentes (recurso subscription en la API) envian sus propios eventos. Todos comparten estos campos en data:

CampoDescripcion
subscriptionIdId del pago recurrente (WEBCODE-SB-xxxxxxxx)
statusEstado tras el evento: active, paused, error, pendingNewCard o cancelled
planCodeTu codigo de plan (puede ser null)
environmentSolo en Test: siempre "test"
  • Los de Test solo van a tu URL Test y llevan environment: "test".
  • En Live recibes tambien los eventos de los pagos recurrentes creados desde el panel para tu webcode, no solo los creados por API.
  • Usa subscriptionId + event_code + paymentReference (si viene) como clave de deduplicacion.

SUBSCRIPTION_CREATED — Primer pago correcto

El cliente ha pagado el primer importe y se ha guardado su tarjeta. El pago recurrente queda activo.

json
{
  "event_code": "SUBSCRIPTION_CREATED",
  "data": {
    "subscriptionId": "SHOP01-SB-3f9a1c2e",
    "status": "active",
    "planCode": "GOLD",
    "paymentReference": "SHOP01-PYS-1792069000123",
    "pspReference": "T7KQ2N9X4M8P6R3Z",
    "amount": 990,
    "currency": "EUR"
  }
}

SUBSCRIPTION_RENEWED — Cobro periodico correcto

json
{
  "event_code": "SUBSCRIPTION_RENEWED",
  "data": {
    "subscriptionId": "SHOP01-SB-3f9a1c2e",
    "status": "active",
    "planCode": "GOLD",
    "paymentReference": "SHOP01-PYSS-1794912000567-1",
    "pspReference": "T8LR3P0Y5N9Q7S4A",
    "amount": 2990,
    "currency": "EUR",
    "paymentsCount": 2,
    "lastChargeDate": "2026-11-05 06:00:12",
    "nextChargeDate": "2026-12-05 06:00:00"
  }
}

SUBSCRIPTION_FAILED — Cobro rechazado

Se envia cuando falla el primer pago (el pago recurrente queda en error) o un cobro periodico que todavia tiene reintentos (sigue active). En este segundo caso incluye retryCount, maxRetries y nextRetryAt.

json
{
  "event_code": "SUBSCRIPTION_FAILED",
  "data": {
    "subscriptionId": "SHOP01-SB-3f9a1c2e",
    "status": "active",
    "planCode": "GOLD",
    "paymentReference": "SHOP01-PYSS-1794912000567-1",
    "amount": 2990,
    "currency": "EUR",
    "error": "Not enough balance",
    "errorCode": "NOT_ENOUGH_BALANCE",
    "retryCount": 1,
    "maxRetries": 3,
    "nextRetryAt": "2026-11-05 11:00:12"
  }
}

SUBSCRIPTION_ERROR — Reintentos agotados (pausado)

El ultimo reintento tambien ha fallado. El pago recurrente pasa a paused con statusReason: payment_failed_max_retries y no se cobra mas hasta que lo reactives (con la tarjeta actual o pidiendo una nueva con update-card). En este caso no llega SUBSCRIPTION_FAILED.

json
{
  "event_code": "SUBSCRIPTION_ERROR",
  "data": {
    "subscriptionId": "SHOP01-SB-3f9a1c2e",
    "status": "paused",
    "planCode": "GOLD",
    "statusReason": "payment_failed_max_retries",
    "requiresAction": "reactivate",
    "lastError": "Expired card",
    "lastErrorCode": "CARD_EXPIRED"
  }
}

Pausa, reactivacion, cancelacion y cambio de tarjeta

event_codeCuandoCampos extra
SUBSCRIPTION_PAUSEDLo pausas (API o panel)pausedAt
SUBSCRIPTION_REACTIVATEDLo reactivas con la tarjeta actualreactivatedAt, nextChargeDate
SUBSCRIPTION_CANCELLEDLo cancelas o llega su endAtcancelledAt
SUBSCRIPTION_CARD_UPDATEDEl cliente paga con su nueva tarjeta (link de update-card). Solo Live; en Test llega SUBSCRIPTION_REACTIVATEDcardLast4, cardBrand, cardExpiryMonth, cardExpiryYear
json
{
  "event_code": "SUBSCRIPTION_CANCELLED",
  "data": {
    "subscriptionId": "SHOP01-SB-3f9a1c2e",
    "status": "cancelled",
    "planCode": "GOLD",
    "cancelledAt": "2026-12-20 10:15:03"
  }
}

Ejemplo en Test (mismo formato + environment):

json
{
  "event_code": "SUBSCRIPTION_REACTIVATED",
  "data": {
    "subscriptionId": "SHOP01-SB-a07c55d1",
    "status": "active",
    "planCode": null,
    "reactivatedAt": "2026-10-16 09:30:00",
    "nextChargeDate": "2026-10-16 09:30:00",
    "environment": "test"
  }
}

Webhooks en entorno Test

En el entorno Test, los webhooks no provienen del procesador de pagos real. Son generados automaticamente por el backend de Avirato Payments inmediatamente despues de completar la operacion simulada.

Diferencias con Live

AspectoLiveTest
Quien enviaEl procesador de pagosEl backend de Avirato Payments
Cuando llegaSegundos a minutos despues de la operacion1-3 segundos despues de la respuesta HTTP
FormatoIdenticoIdentico
CamposIdenticosIdenticos
Reintentos automaticosSi (backoff exponencial, hasta ≈ 4 dias)No: el simulador hace un unico envio
Reenvio manual desde el dashboardSiSi
Criterio de exito HTTPHTTP 200HTTP 200

Lo que debes saber

  • El formato es exactamente el mismo: Puedes desarrollar tu logica de procesamiento de webhooks en Test y funcionara en Live sin cambios
  • Llegan para pagos exitosos Y fallidos: Tanto en Live como en Test, recibiras webhooks cuando un pago se rechaza o una operacion falla
  • URLs independientes por entorno: Los webhooks de Test se envian a la URL configurada para el entorno Test, y los de Live a la URL de Live. Configura ambas desde el panel de webhooks del dashboard
  • Las referencias usan sufijos Test: Las paymentReference y operationReference contendran los infijos de test (-PXTT-, -RFXT-, etc.)
  • Criterio de exito simple: Tanto en Live como en Test, basta con que tu endpoint responda HTTP 200 para que el webhook se marque como Entregado. Cualquier otro codigo de estado se considera fallo y activara los reintentos (en Live) o quedara como Fallido en el dashboard (en Test).
  • Reenvio manual en Test y Live: Desde el panel de webhooks puedes reenviar manualmente cualquier fila. Tanto en Test como en Live, el reenvio utiliza la URL actualmente configurada para ese entorno, no la URL que se uso en el envio original. Si reenvias un webhook que ya aparece como Entregado, el dashboard te pedira confirmacion antes para evitar duplicados accidentales.

Ejemplo de webhook de test (pago online exitoso)

json
{
  "event_code": "AUTHORISATION",
  "data": {
    "amount": 15000,
    "currency": "EUR",
    "paymentReference": "SHOP01-PXTT-17195000005678",
    "paymentStatus": "AUTHORISED",
    "description": "Pedido test #12345",
    "customReference": "TEST-ORD-12345",
    "paymentSource": "external_api"
  }
}

Ejemplo de webhook de test (POS rechazado)

json
{
  "event_code": "AUTHORISATION",
  "data": {
    "amount": 10077,
    "currency": "EUR",
    "paymentReference": "SHOP01-POSXT-17195300001234",
    "paymentStatus": "FAILED",
    "description": "Venta test #100",
    "failedReason": "Refused",
    "paymentSource": "external_api"
  }
}

Referencia rapida

Operacion`event_code``action` (si aplica)Como identificar External API
Pago online completadoAUTHORISATION—paymentReference contiene -PXT- (Live) o -PXTT- (Test)
Pago POS completadoAUTHORISATION—paymentReference contiene -POSX- (Live) o -POSXT- (Test)
Reembolso procesadoREFUND—refundReference contiene -RFX- (Live) o -RFXT- (Test)
Captura procesadaPREAUTHORISATIONcaptureOperacion sobre pago External API
Cancelacion procesadaPREAUTHORISATIONcancellationOperacion sobre pago External API
Extension procesadaPREAUTHORISATIONextendOperacion sobre pago External API
Contracargo recibidoCHARGEBACK—Contracargo iniciado por el titular de la tarjeta
Notificacion de contracargoNOTIFICATION_OF_CHARGEBACK—Aviso previo de un posible contracargo
Pago recurrente: primer pago / cobro correctoSUBSCRIPTION_CREATED / SUBSCRIPTION_RENEWED—subscriptionId contiene -SB-; Test lleva environment: "test"
Pago recurrente: cobro rechazado / reintentos agotadosSUBSCRIPTION_FAILED / SUBSCRIPTION_ERROR—Mira status y retryCount
Pago recurrente: pausa, reactivacion, cancelacion, nueva tarjetaSUBSCRIPTION_PAUSED, _REACTIVATED, _CANCELLED, _CARD_UPDATED—subscriptionId contiene -SB-

Buenas practicas

Idempotencia en tu endpoint

Tu endpoint de webhook puede recibir la misma notificacion mas de una vez (por reintentos o duplicados del procesador). Asegurate de que procesar el mismo webhook dos veces no cause problemas: usa paymentReference o refundReference como clave de deduplicacion.

Respuesta rapida

Responde al webhook lo antes posible (idealmente en menos de 5 segundos). Si necesitas hacer procesamiento pesado, encola el trabajo y responde inmediatamente con HTTP 200.

No dependas de un solo canal

Usa los webhooks como canal principal, pero implementa tambien:

  1. Verificacion post-redirect: Cuando el cliente vuelva a urlOk, consulta GET /payment/session/{id} para confirmar
  2. Polling de seguridad: Un proceso periodico que consulte sesiones en estado pending antiguas para detectar pagos completados que no hayas procesado