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.
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.

Configurer un webhook
- Ouvrez Automatisation
Choisissez Automatisations dans la barre latérale et repérez la carte Webhooks.
- 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://. - 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. - Envoyez un test
Cliquez sur Envoyer un test. Pricely envoie un événement
pingsigné et affiche la réponse de votre point de terminaison, par exemple Livré. Votre point de terminaison a répondu 200. - 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énement | Envoyé lorsque |
|---|---|
quote.accepted | Un devis est accepté : le client le signe via le lien du devis, ou un membre de votre équipe utilise Accepter dans l’application. |
invoice.paid | Une 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.overdue | Une 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. |
ping | Vous 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ête | Valeur |
|---|---|
Content-Type | application/json |
User-Agent | Pricely-Webhooks/1.0 |
X-Pricely-Event | Le nom de l’événement, par exemple invoice.paid. |
X-Pricely-Delivery | Un identifiant unique pour cette livraison, comme evt_3f9a…. Les nouvelles tentatives conservent le même identifiant. |
X-Pricely-Signature | t=<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.
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…
{
"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"
}
{
"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.
| Champ | Type | Remarques |
|---|---|---|
id | string | L’identifiant du devis ou de la facture dans Pricely. |
number | string | Le numéro du document que voit votre client. |
clientId, clientName, clientEmail | string | Chaînes vides lorsque le document n’est lié à aucun client ou que le client n’a pas d’e-mail. |
total, amountPaid, balanceDue | number | Montants en unités principales, par exemple 4800 pour $4,800.00. |
currency | string | Le code de devise à trois lettres du document. |
acceptedAt, dueDate, paidAt, timestamp | string ou null | Horodatages 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 :
{
"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.
- Lisez le corps brut
Utilisez exactement les octets reçus. Analyser le JSON puis le resérialiser modifie les octets et invalide la signature.
- Découpez l’en-tête
Découpez sur les virgules, puis chaque partie sur son premier
=pour obtenirtetv1. - Recalculez le HMAC
Calculez le HMAC-SHA256 de
t.bodyavec votre secretwhsec_…et comparez-le àv1avec une comparaison à temps constant. - Vérifiez l’heure
Rejetez les requêtes dont
ts’écarte de plus de cinq minutes de l’horloge de votre serveur. Cela empêche le rejeu d’anciennes requêtes.
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);
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é, commelocalhost,*.localou192.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
2xxconfirme 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 2xx | Livré. Pas de nouvelle tentative. |
| Est injoignable ou met plus de 10 secondes à répondre | Nouvelle tentative. |
Répond 408, 429 ou 5xx | Nouvelle tentative. |
Répond tout autre 4xx, comme 400 ou 401 | Arrê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ème | Ce 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 s | Votre 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 pas | Vé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-Signature | Le 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 fois | Une 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 facture | C’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.