ROKIConnect
Conectores Empezar API SDK Migrar MCP Portal EN

Webhooks

Son la confirmación autoritativa de un cobro. Sin ellos, una integración depende de que el cliente vuelva al sitio - cosa que no siempre pasa.

14.1 Registro (un paso manual del comercio)

En https://aura.roki.systems/merchant/connect/webhooks:

  1. Elegí el entorno con el interruptor Sandbox (prueba) / En vivo (producción) - tiene que coincidir con la llave sk_ que usa la integración. Este es el error más común: registrar el endpoint en producción mientras desarrollás con una llave de prueba, y no recibir nada.
  2. Pegá la URL pública HTTPS de tu endpoint.
  3. Guardar endpoint.
  4. Copiá el secreto de firma que muestra el portal y guardalo en la configuración de tu proyecto.

Se pueden registrar URLs distintas para sandbox y producción, cada una con su propio secreto.

El portal muestra "Entregas recientes" (los últimos 10 intentos de entrega) - la herramienta de diagnóstico cuando los eventos no están llegando.

14.2 Eventos

Evento Cuándo
payment.approved El cliente pagó con éxito. Observado dentro de ~2 segundos del pago.
payment.failed Tarjeta declinada o error en el pago.
payment.expired El enlace venció antes de que se completara el checkout. Observado ~5 minutos después de expires_at, no en el instante: el vencimiento se barre por tarea programada.
payment.voided Se anuló un pago aprobado.
payment.refunded Reembolso total.
payment.partially_refunded Reembolso parcial.
payment_method.saved El cliente marcó "guardar mi tarjeta"; lleva el pm_* opaco (23.2).

Los últimos tres también llegan cuando la acción se hace desde el portal.

14.3 Estructura del evento

Reproducida de una entrega real recibida y verificada el 2026-08-14, no del ejemplo publicado. Dos cosas en la documentación oficial están mal y se corrigen acá.

Encabezados

Encabezado Ejemplo Para qué sirve
ROKI-Signature t=1786747980,v1=f90f1c... Verificala. Ver 14.4.
ROKI-Webhook-Event-Id 9a7a9698-e270-422a-ba8f-365e944248aa El id del evento, también en el encabezado. Deduplicá con esto sin parsear el cuerpo.
ROKI-Webhook-Event-Type payment.approved Enrutá sin parsear el cuerpo.
User-Agent GuzzleHttp/7 El cliente propio de ROKI. No filtres por eso.

Cuerpo

{
  "id": "9a7a9698-e270-422a-ba8f-365e944248aa",
  "type": "payment.approved",
  "created_at": "2026-08-14 16:52:58",
  "data": {
    "id": 1049,
    "status": "paid",
    "amount": 10,
    "subtotal": 10,
    "total": 10,
    "sales_tax_amount": 0,
    "service_fee_amount": 0,
    "currency": "340",
    "currency_iso": "HNL",
    "external_reference": "demo-hosted-mstjlm2w",
    "name": "Prueba modo 1 - guardar tarjeta",
    "description": null,
    "metadata": [],
    "customer": { "name": null, "email": "demo@roki.la", "phone": null, "identity_number": "0801199000000" },
    "lock_customer_fields": false,
    "reusable": false,
    "checkout_url": "https://aura.roki.systems/pay/link/6ixekbmlwf8e",
    "transaction_id": "91b73300-38b3-48eb-b9d0-72941896b26e",
    "refunded_amount": 0,
    "refund_status": "none",
    "expires_at": "2026-08-14 17:22:35",
    "created_at": "2026-08-14 16:52:36",
    "paid_at": "2026-08-14 16:52:56"
  }
}

Correcciones al ejemplo publicado:

Los eventos de reembolso incluyen además refund_amount, refunded_at y refund_reason dentro de data; refunded_amount y refund_status están presentes en todo pago pagado.

14.4 Verificación de firma (obligatoria)

Encabezado ROKI-Signature: t={timestamp},v1={hmac_sha256_hex}.

El HMAC se calcula sobre timestamp + "." + raw_body usando el secreto de firma:

expected = HMAC-SHA256(timestamp + "." + exact_raw_body, signing_secret)
valid    = constant_time_compare(expected, v1)

Tres reglas que rompen la verificación cuando se ignoran:

  1. Usá el cuerpo crudo, byte por byte. Si tu framework parsea el JSON y vos lo volvés a serializar, la firma nunca va a coincidir: cambian los espacios, el orden de las claves o el escapado.
  2. Comparación en tiempo constante (hash_equals, crypto.timingSafeEqual, hmac.compare_digest). Nunca ==.
  3. Firma inválida -> respondé 400. Válida -> respondé 200 rápido y procesá de forma asíncrona; no hagas trabajo pesado antes de responder.

14.5 Procesamiento idempotente

El mismo evento puede llegar más de una vez. Guardá el id del evento y descartá los repetidos, o armá el procesamiento para que sea idempotente por naturaleza (marcar como pagada una orden que ya estaba pagada no debe cobrar dos veces ni mandar dos correos).

La deduplicación más barata es sobre el encabezado ROKI-Webhook-Event-Id: es el mismo id que aparece en el cuerpo, así que un repetido se puede descartar antes incluso de parsear el payload.

La política de reintentos, confirmada por el equipo del API el 25/08/2026:

Tiempo de espera por intento 15 segundos
Intentos en total 5
Espera entre intentos 1 minuto, después 5 minutos, después 15 minutos, después 1 hora
Después del quinto no se manda nada más
Identificador del evento en los reintentos el mismo, siempre

Tres consecuencias que conviene tener en cuenta al diseñar:

  1. Contestá dentro de los 15 segundos. Verificá la firma, guardá el id del evento y devolvé 200. El trabajo de verdad hacelo después, fuera de la petición. Un manejador que despacha el pedido ahí mismo —mandando correos, llamando a la bodega— se pasa del tiempo cuando hay carga, y ROKI reenvía un evento que ya procesaste.
  2. La ventana completa de reintentos es de una hora y veintiún minutos. Pasado eso, una entrega que nunca llegó se perdió, y lo único que la recupera es tu propio barrido. Eso es lo que hace que el respaldo de 14.6 sea obligatorio y no un lujo.
  3. El id del evento no cambia entre intentos, así que descartar por id es exactamente la guarda correcta: un reintento es indistinguible del original salvo porque llega después.

El orden de entrega sigue sin estar garantizado. Un evento que necesitó cuatro reintentos llega después de otros que salieron a la primera, así que un manejador no puede asumir que el último evento recibido es el estado más reciente. La sección 14.4 explica cómo decidir por tipo de evento y no por orden de llegada.

Rotar el secreto de firma: no hay solapamiento. El secreto nuevo reemplaza al viejo de inmediato; el viejo deja de verificar en el momento en que rotás. Actualizá tu servidor primero, o en el mismo movimiento. Un hueco entre las dos cosas es un hueco donde toda entrega falla la verificación de firma y quema sus cinco intentos. Si no podés desplegar y rotar a la vez, aceptá los dos secretos en tu manejador mientras dure el despliegue: verificá contra el nuevo y, si no valida, contra el viejo; después quitá el respaldo.

14.6 Respaldo: consulta directa

Los webhooks se pueden perder (servidor caído, deploy, red). Implementá un respaldo: si una orden sigue en pending después de X minutos, llamá a GET /payments/{id}. Acá esto importa más de lo habitual porque el redireccionamiento no es prueba de pago. Para conciliación masiva, GET /payments acepta un rango de fechas from/to.