Webhooks
Créer un webhook
Dans l'app (Paramètres › API et webhooks) ou par l'API. Le secret de signature signing_secret n'est renvoyé qu'à la création : conservez-le.
curl
curl -X POST "https://app.replexa.eu/api/v2/webhooks" \
-H "Authorization: Bearer $REPLEXA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "CRM", "target_hook_url": "https://exemple.fr/webhooks/replexa", "event_type": "reply_received" }'event_type vaut un évènement ci-dessous ou all_events ; campaign limite le webhook à une campagne. POST /webhooks/{id}/test envoie un évènement d'essai signé, GET /webhook-events liste les livraisons et leurs réponses. Détail dans la référence.
Requête reçue
| En-tête | Valeur |
|---|---|
Content-Type | application/json |
User-Agent | Replexa-Webhooks/1.0 |
X-Replexa-Timestamp | Horodatage Unix de l'envoi, en secondes. |
X-Replexa-Signature | HMAC-SHA256 en hexadécimal de {timestamp}.{corps brut}, clé = secret du webhook. |
X-Replexa-Event-Id | Identifiant de l'évènement, identique à chaque nouvelle tentative (« test » pour un envoi d'essai). |
| En-têtes personnalisés | Ceux du champ headers du webhook (ex. un jeton pour votre pare-feu). Un en-tête personnalisé nommé Content-Type, User-Agent ou X-Replexa-…, quelle que soit la casse, est ignoré. |
json
{
"timestamp": "2026-10-19T09:12:44.512Z",
"event_type": "reply_received",
"workspace": "7c1e0b6a-5d0f-4a8e-9c61-2f3b4d5e6a70",
"campaign_id": "0193f0a2-7c1e-7d4b-9a51-3f2b8c6d1e90",
"campaign_name": "Relance salons octobre",
"is_first": true,
"lead_email": "jean.dupont@exemple.fr",
"email_account": "vous@exemple.fr",
"step": 1,
"variant": 1,
"firstName": "Jean",
"lastName": "Dupont",
"companyName": "Exemple SA",
"website": "https://exemple.fr",
"phone": null,
"personalization": null
}step et variant commencent à 1. Les évènements de statut (lead_interested…) et ai_draft_created ajoutent leurs propres champs ; GET /hooks/sample?event=… renvoie un exemple réel pour chaque type.
Vérifier la signature
- Lisez le corps brut, avant tout décodage JSON.
- Refusez un horodatage de plus de 5 minutes (protection contre le rejeu).
- Calculez
HMAC-SHA256(secret, timestamp + "." + corps)en hexadécimal et comparez-le en temps constant àX-Replexa-Signature.
Node.js (Express)
import crypto from "node:crypto";
import express from "express";
const SECRET = process.env.REPLEXA_WEBHOOK_SECRET; // signing_secret renvoyé à la création du webhook
const app = express();
// Corps brut obligatoire : la signature porte sur les octets reçus, pas sur un JSON re-sérialisé.
app.post("/webhooks/replexa", express.raw({ type: "application/json" }), (req, res) => {
const timestamp = req.get("x-replexa-timestamp") ?? "";
const signature = req.get("x-replexa-signature") ?? "";
const raw = req.body.toString("utf8");
// 1. Horodatage récent (5 minutes) : rejoue impossible.
if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.status(400).send("horodatage invalide");
}
// 2. HMAC-SHA256 de "timestamp.corps", comparé en temps constant.
const expected = crypto.createHmac("sha256", SECRET).update(`${timestamp}.${raw}`).digest("hex");
const a = Buffer.from(signature, "hex");
const b = Buffer.from(expected, "hex");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send("signature invalide");
}
const event = JSON.parse(raw);
res.sendStatus(200); // répondez vite (moins de 15 s), traitez ensuite
handle(event);
});Python (Flask)
import hashlib, hmac, json, os, time
from flask import Flask, abort, request
SECRET = os.environ["REPLEXA_WEBHOOK_SECRET"].encode()
app = Flask(__name__)
@app.post("/webhooks/replexa")
def replexa_webhook():
timestamp = request.headers.get("X-Replexa-Timestamp", "")
signature = request.headers.get("X-Replexa-Signature", "")
raw = request.get_data() # octets reçus, avant tout décodage JSON
# 1. Horodatage récent (5 minutes)
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
abort(400)
# 2. HMAC-SHA256 de "timestamp.corps", comparé en temps constant
expected = hmac.new(SECRET, timestamp.encode() + b"." + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401)
event = json.loads(raw)
handle(event)
return "", 200Livraison et nouvelles tentatives
- Une réponse 2xx en moins de 15 secondes vaut succès. Les redirections ne sont pas suivies.
- Sinon, cinq nouvelles tentatives : après 1, 5, 30, 120 puis 720 minutes. Le même évènement peut donc arriver plusieurs fois : dédoublonnez sur
X-Replexa-Event-Id. - Après 20 évènements abandonnés en 24 heures, le webhook est désactivé (
statusà -1). Réactivez-le avecPOST /webhooks/{id}/resumeune fois l'URL réparée. - Abonnements REST Hooks (Zapier, Make) : une réponse
410 Gonesupprime l'abonnement. - L'URL doit être publique, en HTTPS : une adresse privée ou locale est refusée à la création et à la modification du webhook (400), puis à chaque livraison.
- L'horodatage et la signature sont recalculés à chaque tentative.
Évènements
email_sentemail_openedemail_link_clickedreply_receivedauto_reply_receivedemail_bouncedlead_unsubscribedcampaign_completedaccount_errorlead_neutrallead_interestedlead_not_interestedlead_meeting_bookedlead_meeting_completedlead_closedlead_out_of_officelead_wrong_personlead_no_showai_draft_created
Plus custom_label_any_positive et custom_label_any_negative pour les labels personnalisés. GET /webhooks/event-types renvoie la liste avec leurs libellés.