Webhooks

kontrol.ar recibe webhooks de pagos, facturación, ingesta de mails y conectores, y valida cada uno por firma HMAC-SHA256 antes de procesarlo.

Webhooks que recibe kontrol.ar

  • MercadoPago (IPN) en /api/payments/webhook: altas y bajas de suscripción (topic preapproval) y cada cobro recurrente del abono (topic subscription_authorized_payment).
  • Facturación (ERP) en /api/billing/invoice-webhook: eventos invoice.issued / invoice.failed del proveedor de factura electrónica.
  • Ingesta de emails en /api/ingest/webhook: mails entrantes reenviados por Resend (formato Svix) a tu dirección @ingest.kontrol.ar.
  • Conectores tipo webhook en /api/webhooks/receive/[connectorId]: un sistema externo empuja su snapshot a un conector tuyo.

Todos corren en Edge Runtime, colocados en gru1 (São Paulo) para quedar al lado de la base.

Verificación de firma (MercadoPago)

El patrón canónico es HMAC-SHA256. En MercadoPago, la firma llega en el header x-signature, que trae dos partes separadas por coma: ts (timestamp) y v1 (firma). Con el header x-request-id y el id del recurso se arma el manifest exacto:

id:<data.id>;request-id:<x-request-id>;ts:<ts>;

Ese manifest se firma con HMAC-SHA256 usando MP_WEBHOOK_SECRET, se pasa a hex y se compara contra v1 con timingSafeEqualStr (comparación en tiempo constante, sin early-exit, para no filtrar la posición del mismatch). Si no coincide, la request muere con 401 antes de tocar nada.

El webhook de facturación usa el mismo HMAC-SHA256 (header x-erp-signature o x-signature, firma en base64) con un webhook_secret por conexión; fail-closed: sin secret configurado no procesa. La ingesta valida la firma Svix (svix-id, svix-timestamp, svix-signature) sobre msgId.timestamp.payload.

Idempotencia

MercadoPago puede reenviar el mismo evento varias veces. Para no procesarlo dos veces, cada evento se registra en la tabla webhook_events con una clave única (preapproval:<id>, authpay:<id>). Antes de procesar se consulta esa tabla: si la clave ya existe, se responde { duplicate: true } y se corta.

Buenas prácticas

  • Validá la firma antes de leer o confiar en el body: sin firma válida, 401.
  • Nunca confíes en un payload sin firmar.
  • Respondé rápido y con 200; el trabajo pesado se dispara aparte.
  • Usá webhook_events (u otra clave única) para que un reintento no duplique efectos.