PricelyDocs

Webhooks

Connectez Pricely à Zapier, Make ou votre propre code. Pricely envoie une requête POST JSON signée à votre URL lors des événements clés, et réessaie si votre point de terminaison est momentanément indisponible.

Business9 min de lecture

Un webhook est une URL que Pricely appelle lorsqu’un événement se produit dans votre espace de travail. Dirigez-le vers un scénario Zapier ou Make pour mettre à jour un CRM, publier dans un canal de discussion ou ajouter une ligne à un tableur, ou vers votre propre serveur. Chaque requête est signée, ce qui permet à votre code de prouver qu’elle provient bien de Pricely.

La fenêtre Webhook avec l’URL du point de terminaison, le secret de signature, la liste des événements et le bouton Envoyer un test
La fenêtre Webhook, ouverte depuis la page Automatisation.

Configurer un webhook

  1. Ouvrez Automatisation

    Choisissez Automatisations dans la barre latérale et repérez la carte Webhooks.

  2. Ajoutez votre point de terminaison

    Cliquez sur Configurer le webhook et collez votre URL du point de terminaison, par exemple une URL Zapier Catch Hook ou une URL Make Custom webhook. Il doit s’agir d’une adresse publique en https://.

  3. Copiez le secret de signature

    La fenêtre affiche un Secret de signature commençant par whsec_. Cliquez sur Copier et conservez-le avec la configuration de votre point de terminaison. Vous en aurez besoin pour vérifier les signatures.

  4. Envoyez un test

    Cliquez sur Envoyer un test. Pricely envoie un événement ping signé et affiche la réponse de votre point de terminaison, par exemple Livré. Votre point de terminaison a répondu 200.

  5. Enregistrez

    Cliquez sur Enregistrer le webhook. La carte affiche alors l’URL de destination, et le bouton devient Mettre à jour le webhook.

Chaque espace de travail dispose d’une seule URL de webhook. Pour la changer, ouvrez la même fenêtre et enregistrez une nouvelle URL. Pour ne plus envoyer d’événements, cliquez sur Supprimer.

Événements

ÉvénementEnvoyé lorsque
quote.acceptedUn devis est accepté : le client le signe via le lien du devis, ou un membre de votre équipe utilise Accepter dans l’application.
invoice.paidUne facture est entièrement payée, quel que soit le moyen : paiement par carte via Stripe, paiement que vous enregistrez vous-même, Marquer comme entièrement payée, action groupée ou avoir qui solde le reste à payer.
invoice.overdueUne facture impayée dépasse sa date d’échéance. L’événement est envoyé une fois par facture, lors de la vérification quotidienne de 07:00 UTC.
pingVous cliquez sur Envoyer un test.

Les factures créées déjà payées, par exemple lors d’un import, n’envoient pas invoice.paid. Un paiement partiel non plus : l’événement est déclenché lorsque le solde atteint zéro.

Format des requêtes

Chaque événement est une requête POST avec un corps JSON et les en-têtes suivants :

En-têteValeur
Content-Typeapplication/json
User-AgentPricely-Webhooks/1.0
X-Pricely-EventLe nom de l’événement, par exemple invoice.paid.
X-Pricely-DeliveryUn identifiant unique pour cette livraison, comme evt_3f9a…. Les nouvelles tentatives conservent le même identifiant.
X-Pricely-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>. Voir Vérifier les signatures.

Le corps contient toujours les mêmes quatre clés : id (l’identifiant de livraison, identique à X-Pricely-Delivery), event, data et timestamp.

En-têtes de la requête
POST /pricely-webhook HTTP/1.1
Content-Type: application/json
User-Agent: Pricely-Webhooks/1.0
X-Pricely-Event: invoice.paid
X-Pricely-Delivery: evt_3f9a6c0e1b7d4a52a8e2f1c9d0b4e7a6
X-Pricely-Signature: t=1793372645,v1=5d1c0f…
quote.accepted
{
  "id": "evt_3f9a6c0e1b7d4a52a8e2f1c9d0b4e7a6",
  "event": "quote.accepted",
  "data": {
    "id": "q_8f2c1a",
    "number": "QT-0042",
    "clientId": "c_71b0d2",
    "clientName": "Northwind Studio",
    "clientEmail": "ops@example.com",
    "total": 4800,
    "currency": "USD",
    "acceptedAt": "2026-10-30T15:04:05.000Z"
  },
  "timestamp": "2026-10-30T15:04:05.120Z"
}
invoice.paid
{
  "id": "evt_9b2e4d7a0c5f4e1b9a3d6c8e2f7a1b40",
  "event": "invoice.paid",
  "data": {
    "id": "inv_51c9e0",
    "number": "INV-0042",
    "clientId": "c_71b0d2",
    "clientName": "Northwind Studio",
    "clientEmail": "ops@example.com",
    "total": 4800,
    "amountPaid": 4800,
    "balanceDue": 0,
    "currency": "USD",
    "dueDate": "2026-11-13T12:00:00.000Z",
    "paidAt": "2026-10-30T15:04:05.000Z"
  },
  "timestamp": "2026-10-30T15:04:05.120Z"
}

invoice.overdue transporte les mêmes champs data que invoice.paid, avec le montant restant dû dans balanceDue et paidAt à null.

ChampTypeRemarques
idstringL’identifiant du devis ou de la facture dans Pricely.
numberstringLe numéro du document que voit votre client.
clientId, clientName, clientEmailstringChaînes vides lorsque le document n’est lié à aucun client ou que le client n’a pas d’e-mail.
total, amountPaid, balanceDuenumberMontants en unités principales, par exemple 4800 pour $4,800.00.
currencystringLe code de devise à trois lettres du document.
acceptedAt, dueDate, paidAt, timestampstring ou nullHorodatages ISO 8601 en UTC.

L’événement de test utilise les mêmes en-têtes et la même signature. Son corps ressemble à ceci :

ping
{
  "id": "evt_0c4d2a9e7f1b4c3d8e6a5b2f9d1c7e30",
  "event": "ping",
  "data": {
    "message": "Test event from Pricely",
    "orgId": "org_2f81c4"
  },
  "timestamp": "2026-10-30T15:04:05.000Z"
}

Vérifier les signatures

Vérifiez la signature avant de faire confiance à une requête. X-Pricely-Signature comporte deux parties : t, l’heure Unix en secondes de la signature, et v1, un HMAC-SHA256 hexadécimal de la chaîne t + . + le corps brut de la requête, calculé avec votre secret de signature.

  1. Lisez le corps brut

    Utilisez exactement les octets reçus. Analyser le JSON puis le resérialiser modifie les octets et invalide la signature.

  2. Découpez l’en-tête

    Découpez sur les virgules, puis chaque partie sur son premier = pour obtenir t et v1.

  3. Recalculez le HMAC

    Calculez le HMAC-SHA256 de t.body avec votre secret whsec_… et comparez-le à v1 avec une comparaison à temps constant.

  4. Vérifiez l’heure

    Rejetez les requêtes dont t s’écarte de plus de cinq minutes de l’horloge de votre serveur. Cela empêche le rejeu d’anciennes requêtes.

Node.js (Express)
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.PRICELY_WEBHOOK_SECRET; // whsec_…
const TOLERANCE_SECONDS = 300;

function verifyPricelySignature(header, rawBody, secret) {
  const parts = {};
  for (const piece of String(header || "").split(",")) {
    const i = piece.indexOf("=");
    if (i > 0) parts[piece.slice(0, i).trim()] = piece.slice(i + 1).trim();
  }
  const t = Number(parts.t);
  if (!Number.isFinite(t) || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest();
  const given = Buffer.from(parts.v1, "hex");
  return given.length === expected.length && crypto.timingSafeEqual(expected, given);
}

// express.raw keeps the exact bytes Pricely signed.
app.post("/pricely-webhook", express.raw({ type: "application/json" }), (req, res) => {
  const rawBody = req.body.toString("utf8");
  if (!verifyPricelySignature(req.get("X-Pricely-Signature"), rawBody, SECRET)) {
    return res.status(400).send("Invalid signature");
  }
  const delivery = JSON.parse(rawBody);
  res.sendStatus(200); // answer first, then do the work

  // Skip delivery.id if you've already handled it (retries reuse it).
  if (delivery.event === "invoice.paid") {
    console.log(`${delivery.data.number} paid: ${delivery.data.amountPaid} ${delivery.data.currency}`);
  }
});

app.listen(3000);
Python
import hashlib
import hmac
import time

TOLERANCE_SECONDS = 300

def verify_pricely_signature(header: str, raw_body: bytes, secret: str) -> bool:
    parts = {}
    for piece in (header or "").split(","):
        key, sep, value = piece.partition("=")
        if sep:
            parts[key.strip()] = value.strip()
    try:
        t = int(parts["t"])
    except (KeyError, ValueError):
        return False
    if abs(time.time() - t) > TOLERANCE_SECONDS or "v1" not in parts:
        return False

    signed = parts["t"].encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

# Flask: verify_pricely_signature(request.headers.get("X-Pricely-Signature"),
#                                 request.get_data(), SECRET)

Livraison et nouvelles tentatives

  • HTTPS uniquement. Pricely ne livre qu’aux URL publiques en https://. Les adresses locales et de réseau privé, comme localhost, *.local ou 192.168.x.x, sont bloquées.
  • Pas de redirections. Si votre URL redirige ailleurs, la livraison échoue. Utilisez l’URL finale.
  • Répondez par un 2xx en moins de 10 secondes. Tout statut 2xx confirme la réception. Répondez d’abord, et effectuez les traitements longs ensuite.
  • Nouvelles tentatives automatiques. Pricely effectue jusqu’à trois tentatives au total, en attendant 1 seconde puis 2 secondes entre elles. Chaque tentative est signée à nouveau avec un nouveau t.
Votre point de terminaison…Ce que fait Pricely
Répond 2xxLivré. Pas de nouvelle tentative.
Est injoignable ou met plus de 10 secondes à répondreNouvelle tentative.
Répond 408, 429 ou 5xxNouvelle tentative.
Répond tout autre 4xx, comme 400 ou 401Arrêt. L’échec est définitif.

Le bouton Envoyer un test effectue une seule tentative, sans nouvel essai, pour que vous voyiez le résultat immédiatement. La fenêtre affiche aussi la Dernière livraison : l’événement, s’il a été livré ou a échoué, le code de statut, l’heure et le nombre de tentatives.

Dépannage

ProblèmeCe qu’il faut vérifier
Utilisez une URL publique en https://. Les adresses locales et de réseau privé sont bloquées.L’URL pointe vers votre propre machine ou un réseau privé. Utilisez une URL HTTPS publique, ou un tunnel qui vous en fournit une.
Votre point de terminaison a répondu 401. (ou un autre code)Pricely a atteint votre point de terminaison, mais celui-ci a refusé la requête. Tout code hors de 200–299 est considéré comme un échec. Consultez les journaux de votre point de terminaison.
Impossible de joindre le point de terminaison : aucune réponse en 10 sVotre point de terminaison est trop lent. Répondez d’abord par un 2xx, puis effectuez le traitement.
Impossible de joindre le point de terminaison (toute autre raison)L’hôte n’a pas répondu, son certificat TLS n’est pas valide ou l’URL redirige. Vérifiez que l’URL s’ouvre dans un navigateur et qu’il s’agit bien de son adresse finale.
La signature ne correspond pasVérifiez sur le corps brut, pas sur un JSON resérialisé, et utilisez le secret actuellement affiché dans la fenêtre. Si vous avez cliqué sur Nouveau secret, enregistrez le webhook et mettez à jour votre point de terminaison.
Pas d’en-tête X-Pricely-SignatureLe webhook a été enregistré sans secret. Ouvrez la fenêtre et cliquez sur Enregistrer le webhook pour en stocker un.
Le même événement est arrivé deux foisUne nouvelle tentative après un délai dépassé ou une erreur 5xx. Dédoublonnez sur X-Pricely-Delivery.
Pas d’invoice.overdue pour une ancienne factureC’est normal. L’événement est envoyé une fois, lorsque la facture dépasse son échéance pour la première fois, pas pour les factures déjà en retard auparavant.

Offres

Les webhooks sont inclus dans les offres Business et Enterprise. Avec les autres offres, la carte Webhooks affiche un cadenas et un lien pour passer à l’offre supérieure.

Cette page vous a-t-elle été utile ? Quelque chose manque ou n’est pas clair ? Dites-le-nous et nous corrigerons la page.Écrire à support@pricely.me
Esc
↑↓ pour naviguer↵ pour ouvrirEsc pour fermer