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 (topicpreapproval) y cada cobro recurrente del abono (topicsubscription_authorized_payment). - Facturación (ERP) en
/api/billing/invoice-webhook: eventosinvoice.issued/invoice.faileddel 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.