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énement | Quand |
|---|---|
deploy.ready | Un déploiement est terminé et son aperçu fonctionne |
deploy.published | Un déploiement est en ligne (aussi après un retour arrière) |
form.submission | Un visiteur a envoyé un formulaire (les indésirables ne sont pas transmis) |
test | Envoyé 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
- Lisez le corps brut en octets, avant toute lecture JSON.
- 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èssha256=. - Comparez-le à
x-liftpad-signatureen temps constant. - 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
- Répondez par un statut 2xx en moins de 10 secondes. Les redirections ne sont pas suivies.
- Chaque événement est envoyé une fois, sans nouvel essai. Après 50 échecs de suite, la notification se met en pause; le tableau de bord montre le dernier statut.
- Les envois indésirables ne sont pas transmis. Les fichiers joints sont listés par nom; téléchargez-les avec l'API.
- Essayez avec le bouton Send test du tableau de bord (événement test).