ReplexaDocs

Webhooks

Replexa envoie chaque évènement (réponse reçue, lead intéressé, email envoyé…) en POST JSON vers votre URL, signé avec le secret du webhook.

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êteValeur
Content-Typeapplication/json
User-AgentReplexa-Webhooks/1.0
X-Replexa-TimestampHorodatage Unix de l'envoi, en secondes.
X-Replexa-SignatureHMAC-SHA256 en hexadécimal de {timestamp}.{corps brut}, clé = secret du webhook.
X-Replexa-Event-IdIdentifiant de l'évènement, identique à chaque nouvelle tentative (« test » pour un envoi d'essai).
En-têtes personnalisésCeux 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

  1. Lisez le corps brut, avant tout décodage JSON.
  2. Refusez un horodatage de plus de 5 minutes (protection contre le rejeu).
  3. 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 "", 200

Livraison 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 avec POST /webhooks/{id}/resume une fois l'URL réparée.
  • Abonnements REST Hooks (Zapier, Make) : une réponse 410 Gone supprime 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.