Webhooks

Un site peut avertir votre serveur quand un déploiement est prêt, quand il est mis en ligne et quand un visiteur envoie un formulaire. Chaque requête est signée pour que vous puissiez vérifier qu'elle vient de Liftpad.

Ajouter un webhook

Dans le tableau de bord, ouvrez le site, puis Notifications : choisissez Webhook (ou Slack), collez une adresse https publique et choisissez les événements. Le secret de signature (lpwh_...) n'est montré qu'une fois : gardez-le dans les secrets de votre serveur. Les notifications se modifient seulement dans le tableau de bord, jamais avec un jeton d'API : un jeton divulgué ne peut pas ajouter un webhook qui transmettrait les prochains envois. Un jeton read peut quand même lire et exporter les envois déjà reçus : gardez chaque jeton secret, les jetons read aussi.

Événements

ÉvénementQuand
deploy.readyUn déploiement est terminé et son aperçu fonctionne
deploy.publishedUn déploiement est en ligne (aussi après un retour arrière)
form.submissionUn visiteur a envoyé un formulaire (les indésirables ne sont pas transmis)
testEnvoyé par le bouton d'essai (testHook)

La requête

Liftpad envoie un POST avec un corps JSON et ces en-têtes :

content-type: application/json
user-agent: Liftpad-Notifications/1
x-liftpad-event: deploy.published
x-liftpad-delivery: 0a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d
x-liftpad-timestamp: 1793977500
x-liftpad-signature: sha256=<hex HMAC-SHA256 of "<timestamp>.<raw body>">
{
  "event": "deploy.published",
  "delivery": "0a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
  "at": "2026-11-05T15:05:00.000Z",
  "site": {
    "name": "my-site",
    "url": "https://www.example.com"
  },
  "deploy": {
    "id": "k2j4h6g8f0d2",
    "title": "First deploy"
  }
}

Vérifier la signature

  1. Lisez le corps brut en octets, avant toute lecture JSON.
  2. Calculez un HMAC-SHA256 avec votre secret sur <x-liftpad-timestamp>.<corps brut> (l'horodatage, un point, puis le corps) et écrivez-le en hexadécimal minuscule, après sha256=.
  3. Comparez-le à x-liftpad-signature en temps constant.
  4. Refusez les horodatages à plus de 5 minutes de votre horloge (rejeu). Utilisez x-liftpad-delivery pour ignorer un envoi déjà traité.

Node.js

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyLiftpad(rawBody, headers, secret, toleranceSeconds = 300) {
  const timestamp = String(headers["x-liftpad-timestamp"] ?? "");
  const signature = String(headers["x-liftpad-signature"] ?? "");
  if (!/^\d+$/.test(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) return false;
  const expected = "sha256=" + createHmac("sha256", secret).update(timestamp + ".").update(rawBody).digest("hex");
  const a = Buffer.from(signature);
  const b = Buffer.from(expected);
  return a.length === b.length && timingSafeEqual(a, b);
}
import { createServer } from "node:http";

createServer((req, res) => {
  const chunks = [];
  req.on("data", (chunk) => chunks.push(chunk));
  req.on("end", () => {
    const raw = Buffer.concat(chunks);
    if (!verifyLiftpad(raw, req.headers, process.env.LIFTPAD_WEBHOOK_SECRET)) {
      res.writeHead(401).end();
      return;
    }
    const event = JSON.parse(raw);
    console.log(event.event, event.site.name);
    res.writeHead(204).end();
  });
}).listen(3000);

Python

import hashlib
import hmac
import time


def verify_liftpad(raw_body: bytes, headers, secret: str, tolerance_seconds: int = 300) -> bool:
    timestamp = headers.get("x-liftpad-timestamp", "")
    signature = headers.get("x-liftpad-signature", "")
    if not timestamp.isdigit():
        return False
    if abs(time.time() - int(timestamp)) > tolerance_seconds:
        return False
    mac = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256)
    return hmac.compare_digest(signature, "sha256=" + mac.hexdigest())

Avec Flask : verify_liftpad(request.get_data(), request.headers, secret) (les en-têtes de Flask ignorent la casse).

Livraison