ROKIConnect

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 de ROKI no está documentada. Asumí que puede haber reintentos y que el orden de entrega no está garantizado.

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 no hay un endpoint de listado para conciliación masiva.