Asistencia biométrica en tiempo real en tu ERP: Webhooks, middleware y 20 líneas de código
Envía los fichajes biométricos a Odoo, SAP, ERPNext o tu HRMS propio en tiempo real. Sin polling, sin imports CSV. Un webhook, una función serverless y 20 líneas de código.
Introduction
Tus dispositivos biométricos capturan fichajes. Tu ERP calcula la nómina. Entre esos dos sistemas hay un vacío que le cuesta a las empresas horas de captura manual cada semana. Algunos equipos de TI exportan CSV a medianoche. Otros corren crons que consultan los relojes checadores cada 5 minutos. Algunos ya se rindieron y contrataron a alguien para teclear los registros de asistencia a mano.
Nada de eso es necesario. Con un webhook y una función middleware de 20 líneas, cada fichaje llega a tu ERP en el momento en que el dedo toca el lector.
Esta guía te muestra exactamente cómo — con código listo para producción para Odoo, SAP, ERPNext y sistemas HRMS propios.
Por qué los imports batch están arruinando la precisión de tu nómina
El enfoque tradicional: exportar datos de asistencia del software del dispositivo, guardar como CSV, subir al ERP, rezar que el mapeo no haya roto nada. Los equipos hacen esto a diario, a veces dos veces al día.
Los problemas se acumulan rápido:
- Retraso de datos. Un fichaje a las 8:47 AM puede no llegar al ERP hasta el batch de medianoche. Los cálculos de horas extra siempre van un día atrasados.
- Registros perdidos. ¿El dispositivo se desconecta durante la ventana de exportación? Esos fichajes desaparecen. Descubres el hueco cuando la nómina ya se procesó.
- Errores manuales. Alguien mapea el ID de empleado 1042 al registro equivocado en Odoo. Un mal mapeo se cascadea en nóminas incorrectas.
- Sin pista de auditoría. ¿Cuándo llegó realmente ese fichaje al ERP? Nadie lo sabe.
La integración en tiempo real elimina los cuatro problemas. Cada fichaje llega al ERP en segundos, con una marca temporal verificable y una pista de auditoría limpia.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 380" fill="none" style="width:100%;max-width:800px;">
<text x="400" y="30" text-anchor="middle" fill="#94a3b8" font-size="17" font-weight="bold" font-family="system-ui">Import batch vs Webhook tiempo real</text>
<!-- Batch side -->
<text x="200" y="65" text-anchor="middle" fill="#ef4444" font-size="15" font-weight="bold" font-family="system-ui">❌ Import batch tradicional</text>
<rect x="40" y="80" width="320" height="280" rx="12" stroke="#ef4444" stroke-width="1.5" fill="none" stroke-dasharray="5,4"/>
<rect x="60" y="100" width="130" height="40" rx="8" stroke="#64748b" stroke-width="1.5" fill="none"/>
<text x="125" y="125" text-anchor="middle" fill="#94a3b8" font-size="14" font-family="system-ui">Software terminal</text>
<text x="200" y="125" text-anchor="middle" fill="#64748b" font-size="14" font-family="system-ui">→</text>
<rect x="220" y="100" width="120" height="40" rx="8" stroke="#64748b" stroke-width="1.5" fill="none"/>
<text x="280" y="125" text-anchor="middle" fill="#94a3b8" font-size="14" font-family="system-ui">Export CSV</text>
<text x="280" y="165" text-anchor="middle" fill="#64748b" font-size="14" font-family="system-ui">↓ (horas después)</text>
<rect x="130" y="180" width="150" height="40" rx="8" stroke="#64748b" stroke-width="1.5" fill="none"/>
<text x="205" y="205" text-anchor="middle" fill="#94a3b8" font-size="14" font-family="system-ui">Carga manual</text>
<text x="205" y="245" text-anchor="middle" fill="#64748b" font-size="14" font-family="system-ui">↓ (propenso a errores)</text>
<rect x="155" y="260" width="100" height="40" rx="8" stroke="#ef4444" stroke-width="1.5" fill="none"/>
<text x="205" y="285" text-anchor="middle" fill="#ef4444" font-size="14" font-family="system-ui">ERP</text>
<text x="205" y="330" text-anchor="middle" fill="#ef4444" font-size="14" font-family="system-ui">⏱ 4-24h de retraso</text>
<text x="205" y="350" text-anchor="middle" fill="#ef4444" font-size="14" font-family="system-ui">📉 Registros perdidos</text>
<!-- Real-time side -->
<text x="600" y="65" text-anchor="middle" fill="#22d3ee" font-size="15" font-weight="bold" font-family="system-ui">✅ Webhook tiempo real</text>
<rect x="440" y="80" width="320" height="280" rx="12" stroke="#22d3ee" stroke-width="1.5" fill="none"/>
<rect x="490" y="110" width="130" height="40" rx="8" stroke="#22d3ee" stroke-width="1.5" fill="none"/>
<text x="555" y="135" text-anchor="middle" fill="#22d3ee" font-size="14" font-family="system-ui">Reloj checador</text>
<text x="600" y="175" text-anchor="middle" fill="#22d3ee" font-size="14" font-family="system-ui">↓ instantáneo</text>
<rect x="490" y="190" width="130" height="40" rx="8" stroke="#a78bfa" stroke-width="1.5" fill="none"/>
<text x="555" y="215" text-anchor="middle" fill="#a78bfa" font-size="14" font-family="system-ui">PunchConnect</text>
<text x="600" y="255" text-anchor="middle" fill="#22d3ee" font-size="14" font-family="system-ui">↓ webhook (< 1s)</text>
<rect x="490" y="270" width="130" height="40" rx="8" stroke="#34d399" stroke-width="1.5" fill="none"/>
<text x="555" y="295" text-anchor="middle" fill="#34d399" font-size="14" font-family="system-ui">Tu ERP</text>
<text x="600" y="340" text-anchor="middle" fill="#34d399" font-size="14" font-family="system-ui">⚡ Entrega sub-segundo</text>
<text x="600" y="358" text-anchor="middle" fill="#34d399" font-size="14" font-family="system-ui">📊 Cero pérdida de datos</text>
</svg>
La arquitectura: Dispositivo → PunchConnect → Middleware → ERP
El patrón de integración es el mismo sin importar qué ERP uses:
1. El dispositivo biométrico registra un fichaje (huella, rostro, RFID)
2. PunchConnect recibe el evento y lo envía como webhook a tu endpoint
3. Tu middleware (función serverless o servidor ligero) recibe el webhook
4. El middleware mapea y escribe el registro de asistencia en la API de tu ERP
El middleware es donde ocurre la magia. Típicamente son 20-50 líneas de código — un handler HTTP simple que transforma el payload del webhook PunchConnect al formato de asistencia de tu ERP.
El payload webhook que recibirás
Cada evento de fichaje llega como JSON POST a tu URL webhook configurada:
El event_id es tu clave de idempotencia — úsalo para deduplicar en caso de reintentos.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 300" fill="none" style="width:100%;max-width:800px;">
<text x="400" y="30" text-anchor="middle" fill="#94a3b8" font-size="17" font-weight="bold" font-family="system-ui">Flujo de integración middleware</text>
<rect x="30" y="70" width="150" height="70" rx="10" stroke="#22d3ee" stroke-width="2" fill="none"/>
<text x="105" y="98" text-anchor="middle" fill="#22d3ee" font-size="15" font-weight="bold" font-family="system-ui">1. Recibir</text>
<text x="105" y="120" text-anchor="middle" fill="#94a3b8" font-size="14" font-family="system-ui">POST /webhook</text>
<line x1="180" y1="105" x2="220" y2="105" stroke="#64748b" stroke-width="1.5"/>
<rect x="220" y="70" width="150" height="70" rx="10" stroke="#a78bfa" stroke-width="2" fill="none"/>
<text x="295" y="98" text-anchor="middle" fill="#a78bfa" font-size="15" font-weight="bold" font-family="system-ui">2. Verificar</text>
<text x="295" y="120" text-anchor="middle" fill="#94a3b8" font-size="14" font-family="system-ui">Firma HMAC</text>
<line x1="370" y1="105" x2="410" y2="105" stroke="#64748b" stroke-width="1.5"/>
<rect x="410" y="70" width="150" height="70" rx="10" stroke="#f59e0b" stroke-width="2" fill="none"/>
<text x="485" y="98" text-anchor="middle" fill="#f59e0b" font-size="15" font-weight="bold" font-family="system-ui">3. Mapear</text>
<text x="485" y="120" text-anchor="middle" fill="#94a3b8" font-size="14" font-family="system-ui">ID → empleado ERP</text>
<line x1="560" y1="105" x2="600" y2="105" stroke="#64748b" stroke-width="1.5"/>
<rect x="600" y="70" width="170" height="70" rx="10" stroke="#34d399" stroke-width="2" fill="none"/>
<text x="685" y="98" text-anchor="middle" fill="#34d399" font-size="15" font-weight="bold" font-family="system-ui">4. Escribir</text>
<text x="685" y="120" text-anchor="middle" fill="#94a3b8" font-size="14" font-family="system-ui">Crear en el ERP</text>
<rect x="220" y="190" width="340" height="80" rx="10" stroke="#64748b" stroke-width="1.5" fill="none" stroke-dasharray="5,4"/>
<text x="390" y="218" text-anchor="middle" fill="#94a3b8" font-size="15" font-weight="bold" font-family="system-ui">Si falla: 500 → PunchConnect reintenta</text>
<text x="390" y="242" text-anchor="middle" fill="#64748b" font-size="14" font-family="system-ui">Backoff exponencial • 72h de reintentos • Al-menos-una-vez</text>
<line x1="485" y1="140" x2="390" y2="190" stroke="#64748b" stroke-width="1" stroke-dasharray="4,3"/>
</svg>
{"event": "attendance.punch","device_serial": "CZKE2241600045","employee_id": "1042","timestamp": "2026-03-28T08:47:12Z","direction": "in","method": "fingerprint","event_id": "evt_a8f3k2x9m1"}
Integración #1: Odoo (JSON-RPC)
Odoo usa JSON-RPC para su API. Aquí un middleware completo — muy popular entre integradores en México, Colombia y España:
Despliega esto en Railway, DigitalOcean o cualquier plataforma serverless. Nuestros clientes en México y Colombia lo han puesto en marcha en menos de una hora.
> Tip: Mapea employee_id de PunchConnect al campo barcode de hr.employee en Odoo. Es el mapeo más limpio — sin campos personalizados.
# odoo_middleware.py — PunchConnect → Odoo asistenciaimport xmlrpc.clientfrom flask import Flask, request, jsonifyapp = Flask(__name__)ODOO_URL = "https://tu-odoo.com"ODOO_DB = "tu-base"ODOO_USER = "admin"ODOO_PASS = "tu-api-key"models = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/object")uid = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/common").authenticate(ODOO_DB, ODOO_USER, ODOO_PASS, {})@app.route("/webhook", methods=["POST"])def handle_punch():data = request.jsonemployee = models.execute_kw(ODOO_DB, uid, ODOO_PASS,"hr.employee", "search_read",[[["barcode", "=", data["employee_id"]]]],{"fields": ["id"], "limit": 1})if not employee:return jsonify({"error": "empleado no encontrado"}), 404if data["direction"] == "in":models.execute_kw(ODOO_DB, uid, ODOO_PASS,"hr.attendance", "create",[{"employee_id": employee[0]["id"],"check_in": data["timestamp"]}])else:open_att = models.execute_kw(ODOO_DB, uid, ODOO_PASS,"hr.attendance", "search",[[["employee_id", "=", employee[0]["id"]],["check_out", "=", False]]],{"limit": 1})if open_att:models.execute_kw(ODOO_DB, uid, ODOO_PASS,"hr.attendance", "write",[open_att, {"check_out": data["timestamp"]}])return jsonify({"status": "ok"}), 200
Integración #2: SAP (OData / RFC)
Las integraciones SAP siguen el mismo patrón pero usan la API OData o llamadas RFC de SAP:
Para SAP on-premise: Usa SAP Cloud Integration (CPI) como capa middleware. CPI puede recibir webhooks PunchConnect vía HTTPS y routearlos a tu SAP on-premise a través de Cloud Connector.
> Cumplimiento NOM-035 (México): La NOM-035 requiere registro de jornada laboral. Con PunchConnect, cada fichaje queda registrado con marca temporal exacta, método de verificación y dispositivo — documentación lista para auditoría. En Colombia, el artículo 161 del Código Sustantivo del Trabajo tiene requisitos similares.
# sap_middleware.py — PunchConnect → SAP SuccessFactorsimport requestsfrom flask import Flask, request, jsonifyapp = Flask(__name__)SAP_BASE = "https://api.successfactors.com"SAP_COMPANY = "tuCompanyId"SAP_USER = "api-user"SAP_PASS = "api-password"@app.route("/webhook", methods=["POST"])def handle_punch():data = request.jsonpayload = {"userId": data["employee_id"],"eventDate": data["timestamp"][:10],"eventTime": data["timestamp"][11:19].replace(":", ""),"eventType": "P10" if data["direction"] == "in" else "P20","terminalId": data["device_serial"]}response = requests.post(f"{SAP_BASE}/odata/v2/EmployeeTime",json=payload,auth=(f"{SAP_USER}@{SAP_COMPANY}", SAP_PASS),headers={"Content-Type": "application/json"})if response.status_code in (200, 201):return jsonify({"status": "ok"}), 200return jsonify({"error": response.text}), 500
Integración #3: ERPNext (REST API)
ERPNext tiene una API REST limpia que hace esta integración directa:
> Tip ERPNext: Usa el doctype Employee Checkin — tiene auto-asistencia integrada que calcula registros de Attendance a partir de los checkins. No escribas directamente en Attendance.
// erpnext_middleware.js — PunchConnect → ERPNextconst express = require("express");const axios = require("axios");const app = express();app.use(express.json());const ERPNEXT_URL = "https://tu-sitio.erpnext.com";const API_KEY = "tu-api-key";const API_SECRET = "tu-api-secret";const erpnext = axios.create({baseURL: ERPNEXT_URL,headers: {Authorization: `token ${API_KEY}:${API_SECRET}`,"Content-Type": "application/json",},});app.post("/webhook", async (req, res) => {const { employee_id, timestamp, direction, event_id } = req.body;try {const { data: empList } = await erpnext.get(`/api/resource/Employee?filters=[["attendance_device_id","=","${employee_id}"]]&fields=["name"]`);if (!empList.data.length) {return res.status(404).json({ error: "empleado no encontrado" });}const employeeName = empList.data[0].name;await erpnext.post("/api/resource/Employee Checkin", {employee: employeeName,time: timestamp,device_id: event_id,log_type: direction === "in" ? "IN" : "OUT",});res.json({ status: "ok" });} catch (err) {console.error("Error ERPNext:", err.response?.data || err.message);res.status(500).json({ error: "fallo escritura ERP" });}});app.listen(3000);
Integración #4: HRMS propio (base de datos directa)
Si desarrollaste tu propio HRMS, la integración es todavía más simple:
# custom_middleware.py — PunchConnect → HRMS propiofrom flask import Flask, request, jsonifyimport psycopg2import osapp = Flask(__name__)conn = psycopg2.connect(os.environ["DATABASE_URL"])@app.route("/webhook", methods=["POST"])def handle_punch():data = request.jsonwith conn.cursor() as cur:cur.execute("SELECT 1 FROM attendance_events WHERE event_id = %s",(data["event_id"],))if cur.fetchone():return jsonify({"status": "duplicado"}), 200cur.execute("""INSERT INTO attendance_events(event_id, employee_id, punch_time, direction, device_serial, method)VALUES (%s, %s, %s, %s, %s, %s)""",(data["event_id"], data["employee_id"],data["timestamp"], data["direction"],data["device_serial"], data["method"]))conn.commit()return jsonify({"status": "ok"}), 200
Manejo de casos límite
Dispositivos offline: Cuando un dispositivo se desconecta, almacena los fichajes localmente. Una vez reconectado, PunchConnect entrega los eventos en orden. Las marcas temporales son del fichaje original, no de la entrega.
Fichajes dobles: Los empleados a veces fichan dos veces por accidente. Tu ERP debe manejar esto a nivel de lógica de negocio — por ejemplo, ignorar un segundo check-in dentro de 60 segundos del primero.
Desfases de zona horaria: PunchConnect entrega marcas temporales en UTC. Convierte a tu zona horaria local en el middleware antes de escribir al ERP.
Dirección faltante: Algunos dispositivos antiguos no reportan la dirección del fichaje. PunchConnect marca estos como direction: "unknown". Tu middleware debe inferir la dirección del contexto.
FAQ
¿Qué tan rápido llegan los fichajes a mi ERP?
Sub-segundo desde que PunchConnect recibe el evento. La latencia total del dedo en el lector al registro en el ERP es típicamente menos de 3 segundos.
¿Qué pasa si mi middleware está caído?
PunchConnect reintenta con backoff exponencial durante 72 horas. Cuando tu middleware vuelve a estar online, recibe todos los eventos en buffer en orden. Ningún dato se pierde.
¿Necesito una IP fija para el endpoint webhook?
No. PunchConnect entrega webhooks a cualquier URL HTTPS — funciones serverless, plataformas cloud o túneles como ngrok para desarrollo. Ver nuestra guía sobre asistencia biométrica sin IP fija.
¿Puedo integrar con múltiples ERPs simultáneamente?
Sí. Registra múltiples URLs webhook en PunchConnect y cada una recibirá cada evento independientemente.
¿Cómo manejo 50+ dispositivos en múltiples sedes?
El middleware no le importa cuántos dispositivos tengas — cada fichaje sigue el mismo camino webhook → middleware → ERP. Ver nuestra guía de gestión multi-sede.
---
¿Listo para conectar tus dispositivos biométricos a tu ERP? PunchConnect te da una API REST y webhooks listos para usar. Inicia tu prueba gratuita de 7 días — sin tarjeta de crédito. La mayoría de los equipos tienen su primera integración funcionando en menos de una hora.
*¿Ya usas PunchConnect? Consulta nuestra guía de configuración webhook para configuración avanzada, o ve cómo AgriWise integró 24,000+ empleados en 50+ sedes.*
Artículos relacionados
Conectar ZKTeco a Odoo: Integración Cloud vía API REST (Sin VPN ni Software Local)
10 min read
GuideAsistencia Biométrica sin IP Fija: Solución Cloud para ZKTeco en LATAM
11 min read
ComparisonAlternativa a CAMS Biométrico con API Callback: Webhook en Tiempo Real para Control de Asistencia
10 min read