Pointage biométrique en temps réel dans votre ERP : Webhooks, middleware et 20 lignes de code
Envoyez les pointages biométriques vers Odoo, SAP, ERPNext ou votre SIRH maison en temps réel. Pas de polling, pas d'imports CSV. Un webhook, une fonction serverless et 20 lignes de code.
Introduction
Vos terminaux biométriques collectent les pointages. Votre ERP calcule la paie. Entre ces deux systèmes se trouve un gouffre qui coûte des heures de saisie manuelle chaque semaine. Certaines équipes IT exportent des CSV à minuit. D'autres lancent des crons qui interrogent les terminaux toutes les 5 minutes. Quelques-uns ont abandonné et embauché quelqu'un pour saisir les pointages à la main.
Tout cela est inutile. Avec un webhook et une fonction middleware de 20 lignes, chaque pointage arrive dans votre ERP au moment même où le doigt touche le lecteur.
Ce guide vous montre exactement comment — avec du code prêt pour la production pour Odoo, SAP, ERPNext et les SIRH maison.
Pourquoi les imports batch sabotent la précision de votre paie
L'approche traditionnelle : exporter les données de pointage depuis le logiciel du terminal, sauvegarder en CSV, uploader dans l'ERP, espérer que le mapping n'a rien cassé. Les équipes font ça quotidiennement, parfois deux fois par jour.
Les problèmes s'accumulent vite :
- Retard des données. Un pointage à 8h47 peut ne pas atteindre l'ERP avant le batch de minuit. Les calculs d'heures supplémentaires sont toujours en retard d'un jour.
- Enregistrements manquants. Le terminal se déconnecte pendant la fenêtre d'export ? Ces pointages disparaissent. Vous découvrez le trou quand la paie est déjà soumise.
- Erreurs manuelles. Quelqu'un mappe l'ID employé 1042 au mauvais enregistrement Odoo. Un mauvais mapping cascade en fiches de paie erronées.
- Pas de piste d'audit. Quand ce pointage est-il réellement arrivé dans l'ERP ? Personne ne le sait.
L'intégration en temps réel élimine ces quatre problèmes. Chaque pointage arrive dans l'ERP en quelques secondes, avec un horodatage vérifiable et une piste d'audit propre.
<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 temps réel</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 traditionnel</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">Logiciel 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">↓ (heures plus tard)</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">Upload manuel</text>
<text x="205" y="245" text-anchor="middle" fill="#64748b" font-size="14" font-family="system-ui">↓ (sujet aux erreurs)</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 retard</text>
<text x="205" y="350" text-anchor="middle" fill="#ef4444" font-size="14" font-family="system-ui">📉 Données manquantes</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 temps réel</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">Terminal biométrique</text>
<text x="600" y="175" text-anchor="middle" fill="#22d3ee" font-size="14" font-family="system-ui">↓ instantané</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">Votre ERP</text>
<text x="600" y="340" text-anchor="middle" fill="#34d399" font-size="14" font-family="system-ui">⚡ Livraison sub-seconde</text>
<text x="600" y="358" text-anchor="middle" fill="#34d399" font-size="14" font-family="system-ui">📊 Zéro perte de données</text>
</svg>
L'architecture : Terminal → PunchConnect → Middleware → ERP
Le modèle d'intégration est le même quel que soit votre ERP :
1. Le terminal biométrique enregistre un pointage (empreinte, visage, RFID)
2. PunchConnect reçoit l'événement et le pousse via webhook vers votre endpoint
3. Votre middleware (fonction serverless ou serveur léger) reçoit le webhook
4. Le middleware mappe et écrit l'enregistrement de présence dans l'API de votre ERP
Le middleware est là où la magie opère. C'est généralement 20-50 lignes de code — un simple handler HTTP qui transforme le payload webhook PunchConnect dans le format API de présence de votre ERP.
Le payload webhook que vous recevrez
Chaque événement de pointage arrive en JSON POST sur votre URL webhook configurée :
L'event_id est votre clé d'idempotence — utilisez-la pour dédupliquer en cas de rejeu.
<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">Flux d'intégration 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. Recevoir</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. Vérifier</text>
<text x="295" y="120" text-anchor="middle" fill="#94a3b8" font-size="14" font-family="system-ui">Signature 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. Mapper</text>
<text x="485" y="120" text-anchor="middle" fill="#94a3b8" font-size="14" font-family="system-ui">ID → employé 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. Écrire</text>
<text x="685" y="120" text-anchor="middle" fill="#94a3b8" font-size="14" font-family="system-ui">Créer dans l'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">En cas d'échec : 500 → PunchConnect réessaie</text>
<text x="390" y="242" text-anchor="middle" fill="#64748b" font-size="14" font-family="system-ui">Backoff exponentiel • 72h de rejeu • Au-moins-une-fois</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"}
Intégration #1 : Odoo (JSON-RPC)
Odoo utilise JSON-RPC pour son API. Voici un middleware complet — populaire auprès des intégrateurs en France, en Belgique et en Afrique francophone :
Déployez ça sur Railway, Scaleway Serverless ou OVH — ça gère toute l'intégration Odoo. Nos clients en France et en Côte d'Ivoire l'ont mis en place en moins d'une heure.
> Astuce : Mappez employee_id de PunchConnect au champ barcode de hr.employee dans Odoo. C'est le mapping le plus propre — pas besoin de champs personnalisés.
# odoo_middleware.py — PunchConnect → Odoo pointageimport xmlrpc.clientfrom flask import Flask, request, jsonifyapp = Flask(__name__)# Connexion OdooODOO_URL = "https://votre-odoo.com"ODOO_DB = "votre-base"ODOO_USER = "admin"ODOO_PASS = "votre-cle-api"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.json# Trouver l'employé Odoo par badgeemployee = 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": "employé non trouvé"}), 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
Intégration #2 : SAP (OData / RFC)
Les intégrations SAP suivent le même modèle mais utilisent l'API OData ou les appels RFC de SAP. La différence clé est l'authentification — SAP nécessite plus de configuration.
Pour SAP on-premise : Utilisez SAP Cloud Integration (CPI) comme couche middleware. CPI peut recevoir les webhooks PunchConnect via HTTPS et les router vers votre SAP on-premise via Cloud Connector.
> Conformité RGPD : Les données biométriques sont des données sensibles au sens de l'article 9 du RGPD. Assurez-vous que votre middleware ne stocke que les identifiants de pointage — jamais les gabarits biométriques. PunchConnect envoie uniquement l'ID employé et l'horodatage, pas les données biométriques brutes. Consultez la CNIL pour vos obligations spécifiques.
# sap_middleware.py — PunchConnect → SAP SuccessFactorsimport requestsfrom flask import Flask, request, jsonifyapp = Flask(__name__)SAP_BASE = "https://api.successfactors.com"SAP_COMPANY = "votreCompanyId"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
Intégration #3 : ERPNext (REST API)
ERPNext possède une API REST propre qui rend cette intégration directe :
> Astuce ERPNext : Utilisez le doctype Employee Checkin — il a une auto-pointage intégrée qui calcule les enregistrements de présence à partir des checkins. N'écrivez pas directement dans Attendance.
// erpnext_middleware.js — PunchConnect → ERPNextconst express = require("express");const axios = require("axios");const app = express();app.use(express.json());const ERPNEXT_URL = "https://votre-site.erpnext.com";const API_KEY = "votre-api-key";const API_SECRET = "votre-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: "employé non trouvé" });}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("Erreur ERPNext:", err.response?.data || err.message);res.status(500).json({ error: "échec écriture ERP" });}});app.listen(3000);
Intégration #4 : SIRH maison (base de données directe)
Si vous avez développé votre propre SIRH, l'intégration est encore plus simple. Pointez le webhook PunchConnect vers votre endpoint de présence :
# custom_middleware.py — PunchConnect → SIRH maisonfrom 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:# Idempotence : ignorer si event_id déjà traitécur.execute("SELECT 1 FROM attendance_events WHERE event_id = %s",(data["event_id"],))if cur.fetchone():return jsonify({"status": "doublon"}), 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
Gestion des cas limites
Terminaux hors ligne : Quand un terminal se déconnecte, il met les pointages en tampon localement. Une fois reconnecté, PunchConnect délivre les événements en ordre. Les horodatages sont ceux du pointage original, pas de la livraison.
Pointages doubles : Les employés pointent parfois deux fois par accident. Votre ERP doit gérer ça au niveau métier — par exemple, ignorer un second check-in dans les 60 secondes du premier.
Décalages de fuseau horaire : PunchConnect livre les horodatages en UTC. Convertissez dans votre fuseau local dans le middleware avant d'écrire dans l'ERP.
Direction manquante : Certains terminaux anciens ne signalent pas la direction du pointage. PunchConnect marque ceux-ci comme direction: "unknown". Votre middleware doit inférer la direction du contexte.
FAQ
À quelle vitesse les pointages arrivent-ils dans mon ERP ?
Sub-seconde à partir du moment où PunchConnect reçoit l'événement. La latence totale entre le doigt sur le lecteur et l'enregistrement ERP est typiquement sous 3 secondes.
Que se passe-t-il si mon middleware est en panne ?
PunchConnect réessaie avec backoff exponentiel pendant 72 heures. Quand votre middleware revient en ligne, il reçoit tous les événements en tampon dans l'ordre. Aucune donnée n'est perdue.
Ai-je besoin d'une IP fixe pour l'endpoint webhook ?
Non. PunchConnect livre les webhooks vers n'importe quelle URL HTTPS — fonctions serverless, plateformes cloud, ou tunnels comme ngrok pour le développement. Voir notre guide sur le pointage biométrique sans IP fixe.
Puis-je intégrer plusieurs ERP simultanément ?
Oui. Enregistrez plusieurs URL webhook dans PunchConnect et chacune recevra chaque événement indépendamment.
Articles connexes
Connecter ZKTeco à Odoo : L'Approche API Cloud (Sans VPN, Sans Logiciel Local)
10 min read
GuidePointage Biométrique Sans IP Fixe : Comment les API Cloud Libèrent Vos Déploiements
11 min read
ComparisonAlternative CAMS Biométrique : API Callback et Webhooks pour le Pointage en Temps Réel
10 min read
Comment gérer 50+ terminaux répartis sur plusieurs sites ?
Le middleware ne se soucie pas du nombre de terminaux — chaque pointage suit le même chemin webhook → middleware → ERP. Voir notre guide sur la gestion multi-site.
---
Prêt à connecter vos terminaux biométriques à votre ERP ? PunchConnect vous donne une API REST et des webhooks clés en main. Démarrez votre essai gratuit de 7 jours — sans carte bancaire. La plupart des équipes ont leur première intégration fonctionnelle en moins d'une heure.
*Déjà utilisateur de PunchConnect ? Consultez notre guide de configuration webhook pour la configuration avancée, ou découvrez comment AgriWise a intégré 24 000+ employés sur 50+ sites.*