Servolution developers

Tutoriel 4 — Webhook événement → Slack

Objectif : recevoir une alerte Slack à chaque facture impayée > 1500 € depuis plus de 30 jours, pour relance immédiate.

Pré-requis

Approche 1 — Polling cron (simple)

Si vous n'avez pas encore d'infra serveur, un cron sur Make / n8n / Zapier suffit.

#!/usr/bin/env bash
# Lancer tous les jours à 09h00 via cron
COMPTENTRA_KEY="${COMPTENTRA_KEY:?}"
SLACK_URL="${SLACK_URL:?}"

# Factures impayées > 1500 € depuis > 30j
THIRTY_DAYS_AGO=$(date -d "30 days ago" +%Y-%m-%d)
curl -s "https://app.comptentra.servolution.fr/api/v1/factures?statut=impayee&min_amount_cents=150000&due_before=$THIRTY_DAYS_AGO" \
  -H "X-API-Key: $COMPTENTRA_KEY" | jq -c '.[]' | while read f; do
  num=$(echo "$f" | jq -r .numero)
  client=$(echo "$f" | jq -r .client_nom)
  total=$(echo "$f" | jq -r '.total_cents / 100')
  due=$(echo "$f" | jq -r .due_date)
  curl -s -X POST "$SLACK_URL" -H 'Content-Type: application/json' -d "{
    \"text\": \":warning: Facture impayée *$num* — $client : ${total} € (échéance $due)\"
  }"
done

Approche 2 — Webhook sortant Servolution (recommandé)

Configurez un endpoint webhook dans Comptentra → Settings → Webhooks :

Receiver minimal en Cloudflare Worker :

export default {
  async fetch(req, env) {
    const body = await req.text();
    const sig = req.headers.get("X-Servolution-Signature");
    const expected = await hmacSha256(env.WEBHOOK_SECRET, body);
    if (sig !== expected) return new Response("bad sig", { status: 401 });
    const evt = JSON.parse(body);
    if (evt.type === "invoice.overdue" && evt.data.total_cents > 150000) {
      await fetch(env.SLACK_URL, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          text: `:warning: Facture impayée *${evt.data.numero}* — ${evt.data.client_nom} : ${evt.data.total_cents/100} € (échéance ${evt.data.due_date})`
        })
      });
    }
    return new Response("ok");
  }
};

async function hmacSha256(key, body) {
  const enc = new TextEncoder();
  const k = await crypto.subtle.importKey("raw", enc.encode(key),
    { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
  const sig = await crypto.subtle.sign("HMAC", k, enc.encode(body));
  return [...new Uint8Array(sig)].map(b => b.toString(16).padStart(2, "0")).join("");
}

3. Bonnes pratiques