Webhooks Vulko
Connectez Vulko à Zapier, Make, n8n ou votre propre code en temps réel. À chaque event critique, Vulko POST un payload JSON signé HMAC-SHA256 vers l'URL que vous déclarez.
Events disponibles
Chaque webhook peut souscrire à un ou plusieurs events. Convention de nommage : <resource>.<past-tense> — identique à Stripe / GitHub.
| event | Description |
|---|---|
| quote.created | Devis créé Émis dès qu'un devis est enregistré (statut DRAFT inclus). Trigger : Création manuelle, IA, ou import. |
| quote.signed | Devis signé électroniquement Émis quand un client signe via le lien public. La signature respecte le niveau eIDAS simple (art. 26). Trigger : POST /api/quote-sign/[token] (signature pad client). |
| quote.accepted | Devis accepté Émis à chaque passage du devis en statut ACCEPTED — signature électronique OU acceptation manuelle. Trigger : Signature OU bouton « Marquer accepté » côté artisan. |
| invoice.created | Facture créée Émis quand une facture est créée — conversion devis manuelle ou auto-conversion PME+. Trigger : convertQuoteToInvoiceAction (manual + auto). |
| invoice.paid | Facture payée Émis quand une facture passe au statut PAID — marquage manuel ou match bancaire CSV (plan PME+). Trigger : markInvoicePaidAction. |
| project.completed | Chantier terminé Émis à chaque passage du chantier en COMPLETED. Trigger : updateProjectStatusAction (status=COMPLETED). |
Format du payload
Chaque event est envoyé par HTTP POST avec un body JSON. Headers obligatoires :
Content-Type: application/jsonUser-Agent: Vulko-Webhook/1.0X-Vulko-Signature: t=<unix>,v1=<hex>— voir vérification ci-dessousX-Vulko-Webhook-Id: <uuid>— id du webhook côté Vulko (debug)
Exemple — event quote.signed :
{
"event": "quote.signed",
"timestamp": "2026-05-24T14:32:18.421Z",
"data": {
"id": "11111111-1111-1111-1111-111111111111",
"number": "DEV-2026-0042",
"status": "ACCEPTED",
"clientName": "Martin Dupont",
"clientEmail": "martin.dupont@example.fr",
"totalHT": 4200.00,
"totalTTC": 5040.00,
"tvaRate": 20,
"signedAt": "2026-05-24T14:32:18.421Z",
"acceptedAt": "2026-05-24T14:32:18.421Z",
"signedByName": "Martin Dupont",
"signedByEmail": "martin.dupont@example.fr"
}
}Vérification de signature
Le header X-Vulko-Signature contient un timestamp Unix et un HMAC-SHA256 du body. Vérifiez-le avant de traiter le payload — sinon n'importe qui peut forger un POST.
- Parsez le header au format
t=<unix>,v1=<hex> - Calculez
HMAC_SHA256(secret, "{t}.{rawBody}") - Comparez en timing-safe avec le
v1reçu - Rejetez si
|now - t| > 300s(anti-replay)
Exemple Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
const SECRET = process.env.VULKO_WEBHOOK_SECRET;
const TOLERANCE_SECONDS = 300;
export function verifyVulkoSignature(rawBody, header) {
// Header format : "t=<unix>,v1=<hex-sha256>"
const parts = header.split(",").map(p => p.trim());
const ts = parseInt(parts.find(p => p.startsWith("t="))?.slice(2) ?? "0", 10);
const received = parts.find(p => p.startsWith("v1="))?.slice(3) ?? "";
if (!ts || !received) return false;
if (Math.abs(Date.now() / 1000 - ts) > TOLERANCE_SECONDS) return false;
const expected = createHmac("sha256", SECRET)
.update(`${ts}.${rawBody}`)
.digest("hex");
if (received.length !== expected.length) return false;
return timingSafeEqual(
Buffer.from(received, "hex"),
Buffer.from(expected, "hex")
);
}Exemple Python
import hmac
import hashlib
import time
import os
SECRET = os.environ["VULKO_WEBHOOK_SECRET"].encode()
TOLERANCE_SECONDS = 300
def verify_vulko_signature(raw_body: bytes, header: str) -> bool:
parts = [p.strip() for p in header.split(",")]
ts_raw = next((p[2:] for p in parts if p.startswith("t=")), "")
received = next((p[3:] for p in parts if p.startswith("v1=")), "")
try:
ts = int(ts_raw)
except ValueError:
return False
if not received or abs(time.time() - ts) > TOLERANCE_SECONDS:
return False
expected = hmac.new(
SECRET, f"{ts}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(received, expected)Politique de retry
- 3 tentatives maximum avec backoff exponentiel (1s / 5s / 30s).
- Retry sur : 5xx, 429, timeouts (5s par tentative), erreurs réseau/DNS/TLS.
- Stop sur : 2xx (succès), 4xx hors 429 (votre code nous dit qu'on a fait n'importe quoi).
- Auto-désactivation après 10 échecs consécutifs. Vous devez réactiver manuellement le webhook depuis l'UI.
Setup Zapier (le plus rapide)
- Créez un nouveau Zap avec le trigger
Webhooks by Zapier → Catch Hook. - Copiez l'URL générée par Zapier (https://hooks.zapier.com/…).
- Côté Vulko, allez sur /settings/webhooks, cliquez Ajouter un webhook, collez l'URL et choisissez les events.
- Cliquez sur le bouton Tester (icône avion) — Zapier doit recevoir le payload
{event:"ping"}. - Continuez la configuration de votre Zap (Notion, Google Sheets, Pipedrive, Slack, etc.).
Limites
- 20 webhooks maximum par entreprise.
- Timeout 5 secondes par tentative HTTP.
- Payload tronqué à 500 caractères pour le log d'erreur (debug UI).
- Pas de garantie de livraison ordonnée — utilisez le champ
timestampdu payload si l'ordre vous importe. - Pas de garantie d'unicité — votre endpoint doit être idempotent (utilisez l'
id+eventcomme clé de dédup).