Webhooks

Webhooks

Gemlead envoie une requête HTTPS à ton serveur dès qu'un lead est trouvé, devient chaud, répond ou réagit à ton message, ou dès que Sam AI a une réponse prête, en envoie une ou te passe une conversation. Elle est signée, pour que tu saches qu'elle vient de nous.

Configurer un endpoint

Ajoute un endpoint dans l'app, sur la page API, ou avec Créer un endpoint webhook. Donne-lui une URL HTTPS publique et les types d'événements voulus, et garde le secret qu'il renvoie pour vérifier les signatures.

  • HTTPS uniquement, pas d'identifiants dans l'URL, et aucune adresse privée, loopback ou link-local.
  • Jusqu'à 10 endpoints par compte.
  • filters.persona_ids limite les événements qu'un endpoint reçoit aux leads de certains personas, quel que soit leur type.
  • Vérifie ton serveur avec Envoyer un événement test avant l'arrivée des vrais événements.

Les webhooks sont inclus dans les plans Business et Enterprise. Si le compte passe au plan Free ou Starter, ses endpoints sont conservés mais n'envoient rien tant qu'il n'est pas revenu en Business ou Enterprise.

POST/v1/webhook_endpoints
curl -X POST https://gemlead.ai/api/v1/webhook_endpoints \
  -H "Authorization: Bearer $GEMLEAD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/gemlead",
    "events": ["lead.hot", "reply.received"],
    "description": "CRM sync",
    "filters": {
      "persona_ids": ["lvWBPnytpS"]
    }
  }'

Types d'événements

Tous les événements ont la même enveloppe. Leur data dépend de leur type.

TypeEnvoyé quand
lead.createdGemlead trouve et score un nouveau lead, quel que soit le résultat. L'événement part une seule fois par lead, avec son premier score, et seulement dans les 3 jours qui suivent sa découverte, donc un rescore d'un lead plus ancien ne l'envoie jamais. Un lead qui obtient 60 ou plus dès le départ envoie aussi lead.hot.
lead.hotLe score d'un lead atteint 60 ou plus pour la première fois. L'événement part une seule fois par lead, et un rescore ne le renvoie jamais.
reply.receivedUn lead t'envoie un nouveau message, en campagne ou non. L'événement part une fois par message de moins de 6 heures. Quand beaucoup de messages du lead arrivent d'un coup, seuls les 5 plus récents le déclenchent.
reaction.receivedUn lead réagit à un de tes messages avec un emoji. L'événement part une fois par emoji du lead sur un message.
sam.suggestion.readySam AI a une nouvelle réponse prête à envoyer, dans une conversation en mode draft. L'événement part une fois par suggestion de moins de 6 heures.
sam.reply.sentUne réponse de Sam AI part de ton compte X, toute seule en mode auto ou une fois que tu l'as validée dans l'app. L'événement part une fois par réponse de moins de 6 heures. Les messages que tu écris toi-même ne le déclenchent jamais, ils apparaissent dans Récupérer une conversation.
sam.handoffSam AI te passe une conversation quand il ne voit pas de bonne réponse sans toi, que le lead n'a envoyé qu'une photo ou un message vocal, ou que sa réponse n'a pas pu partir. En mode auto, il te laisse ensuite les prochains messages du lead. L'événement part une fois par passage de main, s'il date de moins de 6 heures.

L'objet événement

  • idstring
    Identifiant unique de l'événement, evt_ suivi de 24 caractères. Aussi dans le header Gemlead-Event-Id.
  • objectstring
    Toujours event.
  • typestring
    Le type d'événement.
    lead.createdlead.hotreply.receivedreaction.receivedsam.suggestion.readysam.reply.sentsam.handoff
  • created_atstring
    Date de l'événement, en ISO 8601.
  • account_idstring
    Le compte Gemlead concerné.
  • dataobject
    Le contenu de l'événement, qui dépend de son type.

lead.created

Gemlead trouve et score un nouveau lead, quel que soit le résultat. L'événement part une seule fois par lead, avec son premier score, et seulement dans les 3 jours qui suivent sa découverte, donc un rescore d'un lead plus ancien ne l'envoie jamais. Un lead qui obtient 60 ou plus dès le départ envoie aussi lead.hot.

Données

lead.created
{
  "id": "evt_8kQ2vXr4LmT9wPz1Hc6YbN3d",
  "object": "event",
  "type": "lead.created",
  "created_at": "2026-11-05T14:03:40.117Z",
  "account_id": "a9PzK2vRtM",
  "data": {
    "object": {
      "id": "k3VnQ8wZpR",
      "object": "lead",
      "profile": {
        "x_user_id": "1592047713",
        "username": "mark_picks",
        "name": "Mark Ellis",
        "bio": "Free NBA picks every day. Sports fan first.",
        "followers_count": 2150,
        "following_count": 940,
        "location": "Denver, CO",
        "website": "https://markpicks.net",
        "profile_image_url": "https://pbs.twimg.com/profile_images/1592047713/mark_400x400.jpg",
        "verified": false,
        "url": "https://x.com/mark_picks"
      },
      "score": 38,
      "score_reason": "Shares free picks for fun and sells nothing yet, no sign of a paying community.",
      "hot": false,
      "status": "scored",
      "persona": {
        "id": "lvWBPnytpS",
        "name": "Tipsters"
      },
      "campaign": null,
      "conversation_id": null,
      "locked_to": null,
      "found_at": "2026-11-05T14:02:11.482Z",
      "scored_at": "2026-11-05T14:03:40.117Z",
      "updated_at": "2026-11-05T14:03:40.117Z",
      "metadata": {}
    }
  }
}

lead.hot

Le score d'un lead atteint 60 ou plus pour la première fois. L'événement part une seule fois par lead, et un rescore ne le renvoie jamais.

Données

lead.hot
{
  "id": "evt_8kQ2vXr4LmT9wPz1Hc6YbN3d",
  "object": "event",
  "type": "lead.hot",
  "created_at": "2026-11-05T14:03:40.117Z",
  "account_id": "a9PzK2vRtM",
  "data": {
    "object": {
      "id": "msgOtJ8dvT",
      "object": "lead",
      "profile": {
        "x_user_id": "1460283925",
        "username": "jane_builds",
        "name": "Jane Doe",
        "bio": "Runs a paid trading community. Daily setups, no hype.",
        "followers_count": 12400,
        "following_count": 380,
        "location": "Austin, TX",
        "website": "https://janedoe.com",
        "profile_image_url": "https://pbs.twimg.com/profile_images/1460283925/jane_400x400.jpg",
        "verified": true,
        "url": "https://x.com/jane_builds"
      },
      "score": 86,
      "score_reason": "Runs a paid trading community and asks how to collect payments for it.",
      "hot": true,
      "status": "scored",
      "persona": {
        "id": "lvWBPnytpS",
        "name": "Tipsters"
      },
      "campaign": null,
      "conversation_id": null,
      "locked_to": null,
      "found_at": "2026-11-05T14:02:11.482Z",
      "scored_at": "2026-11-05T14:03:40.117Z",
      "updated_at": "2026-11-05T14:03:40.117Z",
      "metadata": {}
    }
  }
}

reply.received

Un lead t'envoie un nouveau message, en campagne ou non. L'événement part une fois par message de moins de 6 heures. Quand beaucoup de messages du lead arrivent d'un coup, seuls les 5 plus récents le déclenchent.

Données

  • leadobject
    Le lead qui a écrit.
  • messageobject
    Le nouveau message, avec direction à inbound.
  • campaignobject ou null
    La campagne du lead, avec son id, son name et son status.
  • conversation_idstring
    Utilise-le avec Récupérer une conversation pour avoir tout le fil.
reply.received
{
  "id": "evt_8kQ2vXr4LmT9wPz1Hc6YbN3d",
  "object": "event",
  "type": "reply.received",
  "created_at": "2026-11-05T14:03:40.117Z",
  "account_id": "a9PzK2vRtM",
  "data": {
    "lead": {
      "id": "msgOtJ8dvT",
      "object": "lead",
      "profile": {
        "x_user_id": "1460283925",
        "username": "jane_builds",
        "name": "Jane Doe",
        "bio": "Runs a paid trading community. Daily setups, no hype.",
        "followers_count": 12400,
        "following_count": 380,
        "location": "Austin, TX",
        "website": "https://janedoe.com",
        "profile_image_url": "https://pbs.twimg.com/profile_images/1460283925/jane_400x400.jpg",
        "verified": true,
        "url": "https://x.com/jane_builds"
      },
      "score": 86,
      "score_reason": "Runs a paid trading community and asks how to collect payments for it.",
      "hot": true,
      "status": "in_conversation",
      "persona": {
        "id": "lvWBPnytpS",
        "name": "Tipsters"
      },
      "campaign": {
        "id": "c7RkQ2wLxe",
        "name": "Tipsters warm intro",
        "status": "in_progress"
      },
      "conversation_id": "msgOtJ8dvT",
      "locked_to": null,
      "found_at": "2026-11-05T14:02:11.482Z",
      "scored_at": "2026-11-05T14:03:40.117Z",
      "updated_at": "2026-11-05T14:03:40.117Z",
      "metadata": {}
    },
    "message": {
      "id": "1985321409876543210",
      "object": "message",
      "direction": "inbound",
      "text": "Yes, I want to charge for my Discord. How does it work?",
      "created_at": "2026-11-06T08:12:44.000Z",
      "media": [],
      "reactions": [],
      "encrypted": false,
      "reply_to_message_id": null
    },
    "campaign": {
      "id": "c7RkQ2wLxe",
      "name": "Tipsters warm intro",
      "status": "in_progress"
    },
    "conversation_id": "msgOtJ8dvT"
  }
}

reaction.received

Un lead réagit à un de tes messages avec un emoji. L'événement part une fois par emoji du lead sur un message.

Données

  • leadobject
    Le lead qui a réagi.
  • messageobject
    Ton message auquel le lead a réagi, avec direction à outbound.
  • reactionobject
    La nouvelle réaction, avec direction à inbound.
reaction.received
{
  "id": "evt_8kQ2vXr4LmT9wPz1Hc6YbN3d",
  "object": "event",
  "type": "reaction.received",
  "created_at": "2026-11-05T14:03:40.117Z",
  "account_id": "a9PzK2vRtM",
  "data": {
    "lead": {
      "id": "msgOtJ8dvT",
      "object": "lead",
      "profile": {
        "x_user_id": "1460283925",
        "username": "jane_builds",
        "name": "Jane Doe",
        "bio": "Runs a paid trading community. Daily setups, no hype.",
        "followers_count": 12400,
        "following_count": 380,
        "location": "Austin, TX",
        "website": "https://janedoe.com",
        "profile_image_url": "https://pbs.twimg.com/profile_images/1460283925/jane_400x400.jpg",
        "verified": true,
        "url": "https://x.com/jane_builds"
      },
      "score": 86,
      "score_reason": "Runs a paid trading community and asks how to collect payments for it.",
      "hot": true,
      "status": "in_conversation",
      "persona": {
        "id": "lvWBPnytpS",
        "name": "Tipsters"
      },
      "campaign": {
        "id": "c7RkQ2wLxe",
        "name": "Tipsters warm intro",
        "status": "in_progress"
      },
      "conversation_id": "msgOtJ8dvT",
      "locked_to": null,
      "found_at": "2026-11-05T14:02:11.482Z",
      "scored_at": "2026-11-05T14:03:40.117Z",
      "updated_at": "2026-11-05T14:03:40.117Z",
      "metadata": {}
    },
    "message": {
      "id": "1985329981234567890",
      "object": "message",
      "direction": "outbound",
      "text": "Happy to show you. Do you sell picks monthly or per event?",
      "created_at": "2026-11-06T08:20:03.000Z",
      "media": [],
      "reactions": [
        {
          "emoji": "👍",
          "direction": "inbound",
          "created_at": "2026-11-06T08:24:51.000Z"
        }
      ],
      "encrypted": false,
      "reply_to_message_id": "1985321409876543210"
    },
    "reaction": {
      "emoji": "👍",
      "direction": "inbound",
      "created_at": "2026-11-06T08:24:51.000Z"
    }
  }
}

sam.suggestion.ready

Sam AI a une nouvelle réponse prête à envoyer, dans une conversation en mode draft. L'événement part une fois par suggestion de moins de 6 heures.

Données

  • leadobject
    Le lead auquel Sam AI répond.
  • conversation_idstring
    Utilise-le avec Récupérer le Sam AI d'une conversation pour relire la suggestion ou changer le mode.
  • campaignobject ou null
    La campagne du lead, avec son id, son name et son status.
  • suggestionobject
    La réponse que Sam AI a prête.
sam.suggestion.ready
{
  "id": "evt_8kQ2vXr4LmT9wPz1Hc6YbN3d",
  "object": "event",
  "type": "sam.suggestion.ready",
  "created_at": "2026-11-05T14:03:40.117Z",
  "account_id": "a9PzK2vRtM",
  "data": {
    "lead": {
      "id": "msgOtJ8dvT",
      "object": "lead",
      "profile": {
        "x_user_id": "1460283925",
        "username": "jane_builds",
        "name": "Jane Doe",
        "bio": "Runs a paid trading community. Daily setups, no hype.",
        "followers_count": 12400,
        "following_count": 380,
        "location": "Austin, TX",
        "website": "https://janedoe.com",
        "profile_image_url": "https://pbs.twimg.com/profile_images/1460283925/jane_400x400.jpg",
        "verified": true,
        "url": "https://x.com/jane_builds"
      },
      "score": 86,
      "score_reason": "Runs a paid trading community and asks how to collect payments for it.",
      "hot": true,
      "status": "in_conversation",
      "persona": {
        "id": "lvWBPnytpS",
        "name": "Tipsters"
      },
      "campaign": {
        "id": "c7RkQ2wLxe",
        "name": "Tipsters warm intro",
        "status": "in_progress"
      },
      "conversation_id": "msgOtJ8dvT",
      "locked_to": null,
      "found_at": "2026-11-05T14:02:11.482Z",
      "scored_at": "2026-11-05T14:03:40.117Z",
      "updated_at": "2026-11-05T14:03:40.117Z",
      "metadata": {}
    },
    "conversation_id": "msgOtJ8dvT",
    "campaign": {
      "id": "c7RkQ2wLxe",
      "name": "Tipsters warm intro",
      "status": "in_progress"
    },
    "suggestion": {
      "messages": [
        "It takes two minutes. You set a price, members pay, and they get their Discord role right away.",
        "Want the link to set it up?"
      ],
      "stage": "answer_question",
      "why": "She asked how it works, so the reply explains it in one line and offers the link.",
      "replying_to_message_id": "1985321409876543210",
      "created_at": "2026-11-06T08:13:20.000Z"
    }
  }
}

sam.reply.sent

Une réponse de Sam AI part de ton compte X, toute seule en mode auto ou une fois que tu l'as validée dans l'app. L'événement part une fois par réponse de moins de 6 heures. Les messages que tu écris toi-même ne le déclenchent jamais, ils apparaissent dans Récupérer une conversation.

Données

  • leadobject
    Le lead auquel Sam AI a répondu.
  • conversation_idstring
    Utilise-le avec Récupérer une conversation pour avoir tout le fil.
  • campaignobject ou null
    La campagne du lead, avec son id, son name et son status.
  • messagestableau de strings
    Les DM envoyés par Sam AI, dans l'ordre.
  • sent_bystring
    auto quand Sam AI l'a envoyée tout seul, approved quand tu l'as validée dans l'app.
    autoapproved
  • replying_to_message_idstring ou null
    Le message du lead auquel elle a répondu.
  • sent_atstring
    Date d'envoi, en ISO 8601.
sam.reply.sent
{
  "id": "evt_8kQ2vXr4LmT9wPz1Hc6YbN3d",
  "object": "event",
  "type": "sam.reply.sent",
  "created_at": "2026-11-05T14:03:40.117Z",
  "account_id": "a9PzK2vRtM",
  "data": {
    "lead": {
      "id": "msgOtJ8dvT",
      "object": "lead",
      "profile": {
        "x_user_id": "1460283925",
        "username": "jane_builds",
        "name": "Jane Doe",
        "bio": "Runs a paid trading community. Daily setups, no hype.",
        "followers_count": 12400,
        "following_count": 380,
        "location": "Austin, TX",
        "website": "https://janedoe.com",
        "profile_image_url": "https://pbs.twimg.com/profile_images/1460283925/jane_400x400.jpg",
        "verified": true,
        "url": "https://x.com/jane_builds"
      },
      "score": 86,
      "score_reason": "Runs a paid trading community and asks how to collect payments for it.",
      "hot": true,
      "status": "in_conversation",
      "persona": {
        "id": "lvWBPnytpS",
        "name": "Tipsters"
      },
      "campaign": {
        "id": "c7RkQ2wLxe",
        "name": "Tipsters warm intro",
        "status": "in_progress"
      },
      "conversation_id": "msgOtJ8dvT",
      "locked_to": null,
      "found_at": "2026-11-05T14:02:11.482Z",
      "scored_at": "2026-11-05T14:03:40.117Z",
      "updated_at": "2026-11-05T14:03:40.117Z",
      "metadata": {}
    },
    "conversation_id": "msgOtJ8dvT",
    "campaign": {
      "id": "c7RkQ2wLxe",
      "name": "Tipsters warm intro",
      "status": "in_progress"
    },
    "messages": [
      "It takes two minutes. You set a price, members pay, and they get their Discord role right away.",
      "Want the link to set it up?"
    ],
    "sent_by": "auto",
    "replying_to_message_id": "1985321409876543210",
    "sent_at": "2026-11-06T08:14:05.000Z"
  }
}

sam.handoff

Sam AI te passe une conversation quand il ne voit pas de bonne réponse sans toi, que le lead n'a envoyé qu'une photo ou un message vocal, ou que sa réponse n'a pas pu partir. En mode auto, il te laisse ensuite les prochains messages du lead. L'événement part une fois par passage de main, s'il date de moins de 6 heures.

Données

  • leadobject
    Le lead à qui répondre.
  • conversation_idstring
    Utilise-le avec Récupérer une conversation pour avoir tout le fil.
  • campaignobject ou null
    La campagne du lead, avec son id, son name et son status.
  • reasonstring ou null
    La raison pour laquelle Sam AI te la passe, telle qu'il l'a écrite.
sam.handoff
{
  "id": "evt_8kQ2vXr4LmT9wPz1Hc6YbN3d",
  "object": "event",
  "type": "sam.handoff",
  "created_at": "2026-11-05T14:03:40.117Z",
  "account_id": "a9PzK2vRtM",
  "data": {
    "lead": {
      "id": "msgOtJ8dvT",
      "object": "lead",
      "profile": {
        "x_user_id": "1460283925",
        "username": "jane_builds",
        "name": "Jane Doe",
        "bio": "Runs a paid trading community. Daily setups, no hype.",
        "followers_count": 12400,
        "following_count": 380,
        "location": "Austin, TX",
        "website": "https://janedoe.com",
        "profile_image_url": "https://pbs.twimg.com/profile_images/1460283925/jane_400x400.jpg",
        "verified": true,
        "url": "https://x.com/jane_builds"
      },
      "score": 86,
      "score_reason": "Runs a paid trading community and asks how to collect payments for it.",
      "hot": true,
      "status": "in_conversation",
      "persona": {
        "id": "lvWBPnytpS",
        "name": "Tipsters"
      },
      "campaign": {
        "id": "c7RkQ2wLxe",
        "name": "Tipsters warm intro",
        "status": "in_progress"
      },
      "conversation_id": "msgOtJ8dvT",
      "locked_to": null,
      "found_at": "2026-11-05T14:02:11.482Z",
      "scored_at": "2026-11-05T14:03:40.117Z",
      "updated_at": "2026-11-05T14:03:40.117Z",
      "metadata": {}
    },
    "conversation_id": "msgOtJ8dvT",
    "campaign": {
      "id": "c7RkQ2wLxe",
      "name": "Tipsters warm intro",
      "status": "in_progress"
    },
    "reason": "She wants a call before she signs up."
  }
}

Envoi

Gemlead envoie chaque événement en POST JSON et attend ta réponse pendant 10 secondes. Toute réponse 2xx compte comme un envoi réussi. Une redirection, tout autre statut, un timeout ou une erreur de connexion compte comme un échec, et l'événement est renvoyé.

HeaderValeur
Gemlead-SignatureLe timestamp et la signature, t=1762444800,v1=5f2b...
Gemlead-Event-IdL'id de l'événement, pour ignorer ceux que tu as déjà traités.
Gemlead-Event-TypeLe type de l'événement.
User-AgentGemlead-Webhooks/1.0
Requête
POST /webhooks/gemlead HTTP/1.1
Content-Type: application/json; charset=utf-8
User-Agent: Gemlead-Webhooks/1.0
Gemlead-Event-Id: evt_8kQ2vXr4LmT9wPz1Hc6YbN3d
Gemlead-Event-Type: lead.hot
Gemlead-Signature: t=1762444800,v1=58ca5e964b92d617c979d3f067cdc2035a1a1a9d3f848d7d927c2db09b8e9baa

Vérifier les signatures

N'importe qui peut envoyer une requête à ton URL. Vérifie que chacune vient bien de Gemlead avant de lui faire confiance.

  1. Lis le corps brut en octets, avant tout parsing JSON. Le parser puis le resérialiser modifie les octets.
  2. Découpe le header Gemlead-Signature sur les virgules. t est un timestamp Unix en secondes, v1 la signature.
  3. Calcule le HMAC SHA256 de t, d'un point et du corps brut, avec le secret complet de l'endpoint comme clé, whsec_ compris, et écris-le en hexadécimal.
  4. Compare-le à chaque v1 en temps constant.
  5. Refuse un t à plus de 5 minutes de ton horloge, pour qu'une vieille requête ne puisse pas être rejouée. Chaque nouvelle tentative est signée avec un nouveau t.

Réponds 400 quand la vérification échoue. Garde le secret dans une variable d'environnement comme GEMLEAD_WEBHOOK_SECRET.

const crypto = require("crypto");
const express = require("express");

// rawBody: the request body exactly as received, before any JSON parsing.
function verifyGemleadSignature(rawBody, header, secret, toleranceSeconds = 300) {
  if (typeof header !== "string") return false;
  let timestamp = null;
  const signatures = [];
  for (const part of header.split(",")) {
    const [key, value] = part.split("=", 2).map((s) => (s || "").trim());
    if (key === "t") timestamp = value;
    if (key === "v1" && value) signatures.push(value);
  }
  if (!/^\d+$/.test(timestamp || "") || signatures.length === 0) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest();
  return signatures.some((signature) => {
    const given = Buffer.from(signature, "hex");
    return given.length === expected.length && crypto.timingSafeEqual(given, expected);
  });
}

const app = express();

// The body must stay raw on this route.
app.post("/webhooks/gemlead", express.raw({ type: "application/json" }), (req, res) => {
  const ok = verifyGemleadSignature(
    req.body,
    req.get("Gemlead-Signature"),
    process.env.GEMLEAD_WEBHOOK_SECRET,
  );
  if (!ok) return res.status(400).send("Invalid signature");

  const event = JSON.parse(req.body.toString("utf8"));
  // Already handled? Skip it, retries send the same event.id again.
  if (event.type === "lead.created") {
    // event.data.object is the new lead, hot or not
  } else if (event.type === "lead.hot") {
    // event.data.object is the lead
  } else if (event.type === "reply.received") {
    // event.data.lead, event.data.message, event.data.campaign, event.data.conversation_id
  }
  res.sendStatus(200);
});

app.listen(3000);

Nouvelles tentatives

Quand un envoi échoue, Gemlead réessaie 7 fois, en attendant plus longtemps à chaque fois. Après la 8e tentative, environ 45 heures après la première, l'envoi est marqué en échec.

TentativeEnvoyée
1Tout de suite
21 minute après l'échec précédent
35 minutes après l'échec précédent
430 minutes après l'échec précédent
52 heures après l'échec précédent
66 heures après l'échec précédent
712 heures après l'échec précédent
824 heures après l'échec précédent

Un endpoint qui échoue 3 jours de suite, avec au moins 8 tentatives ratées, est désactivé. Son status passe à disabled, son disabled_reason à failing, et les envois encore en attente finissent en échec. Corrige ton serveur, puis réactive-le dans l'app ou avec Modifier un endpoint webhook, ce qui remet le compteur à zéro. Les événements test sont envoyés une seule fois et ne comptent jamais.

Lister les envois montre le code de statut et l'erreur de chaque tentative, pour voir ce que ton serveur a répondu.

Bonnes pratiques

  • Réponds par un 2xx en moins de 10 secondes, puis traite l'événement dans une file d'attente.
  • Chaque événement arrive au moins une fois. Utilise son id, aussi dans Gemlead-Event-Id, pour ignorer un événement déjà traité.
  • Les événements peuvent arriver dans le désordre. Compare created_at, ou récupère à nouveau le lead pour avoir son dernier état.
  • Vérifie chaque signature et refuse les vieux timestamps.
  • Quand tu renouvelles le secret, il est remplacé aussitôt. Déploie le nouveau juste après. Les envois qui échouent entre-temps sont renvoyés et signés avec lui.
  • Les événements et les envois sont conservés 30 jours.