Aller au contenu principal

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.

eventDescription
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/json
  • User-Agent: Vulko-Webhook/1.0
  • X-Vulko-Signature: t=<unix>,v1=<hex> — voir vérification ci-dessous
  • X-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.

  1. Parsez le header au format t=<unix>,v1=<hex>
  2. Calculez HMAC_SHA256(secret, "{t}.{rawBody}")
  3. Comparez en timing-safe avec le v1 reçu
  4. 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)

  1. Créez un nouveau Zap avec le trigger Webhooks by Zapier → Catch Hook.
  2. Copiez l'URL générée par Zapier (https://hooks.zapier.com/…).
  3. Côté Vulko, allez sur /settings/webhooks, cliquez Ajouter un webhook, collez l'URL et choisissez les events.
  4. Cliquez sur le bouton Tester (icône avion) — Zapier doit recevoir le payload {event:"ping"}.
  5. 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 timestamp du payload si l'ordre vous importe.
  • Pas de garantie d'unicité — votre endpoint doit être idempotent (utilisez l'id + event comme clé de dédup).

Prêt à simplifier votre activité ?

Testez VULKO gratuitement pendant 14 jours. Sans CB, sans engagement.

Commencer l'essai gratuit