Webhooks
Verbinden Sie Pricely mit Zapier, Make oder Ihrem eigenen Code. Pricely sendet bei wichtigen Ereignissen einen signierten JSON-POST an Ihre URL und wiederholt ihn, wenn Ihr Endpunkt kurz nicht erreichbar ist.
Ein Webhook ist eine URL, die Pricely aufruft, wenn in Ihrem Workspace etwas passiert. Richten Sie ihn auf ein Zapier- oder Make-Szenario, um ein CRM zu aktualisieren, in einem Chat-Kanal zu posten oder eine Zeile in einer Tabelle anzulegen – oder auf Ihren eigenen Server. Jede Anfrage ist signiert, sodass Ihr Code nachweisen kann, dass sie von Pricely stammt.

Webhook einrichten
- Automatisierung öffnen
Wählen Sie Automatisierungen in der Seitenleiste und suchen Sie die Karte Webhooks.
- Endpunkt hinzufügen
Klicken Sie auf Webhook konfigurieren und fügen Sie Ihre Endpunkt-URL ein, zum Beispiel eine Catch Hook-URL von Zapier oder eine Custom webhook-URL von Make. Es muss eine öffentliche
https://-Adresse sein. - Signatur-Secret kopieren
Der Dialog zeigt ein Signatur-Secret, das mit
whsec_beginnt. Klicken Sie auf Kopieren und speichern Sie es in der Konfiguration Ihres Endpunkts. Sie brauchen es, um Signaturen zu prüfen. - Test senden
Klicken Sie auf Test senden. Pricely sendet ein signiertes
ping-Ereignis und zeigt, was Ihr Endpunkt geantwortet hat, zum Beispiel Zugestellt. Ihr Endpunkt hat mit 200 geantwortet. - Speichern
Klicken Sie auf Webhook speichern. Die Karte zeigt danach die Ziel-URL, und die Schaltfläche heißt Webhook aktualisieren.
Jeder Workspace hat eine Webhook-URL. Um sie zu ändern, öffnen Sie denselben Dialog und speichern eine neue URL. Um keine Ereignisse mehr zu senden, klicken Sie auf Entfernen.
Ereignisse
| Ereignis | Gesendet, wenn |
|---|---|
quote.accepted | Ein Angebot angenommen wird: Der Kunde unterschreibt es über den Angebotslink, oder jemand aus Ihrem Team nutzt Annehmen in der App. |
invoice.paid | Eine Rechnung auf beliebigem Weg vollständig bezahlt wird: per Kartenzahlung über Stripe, durch eine selbst erfasste Zahlung, Als vollständig bezahlt markieren, eine Sammelaktion oder eine Gutschrift, die den offenen Betrag ausgleicht. |
invoice.overdue | Eine unbezahlte Rechnung ihr Fälligkeitsdatum überschreitet. Wird pro Rechnung einmal gesendet, durch die tägliche Prüfung um 07:00 UTC. |
ping | Sie auf Test senden klicken. |
Rechnungen, die bereits bezahlt angelegt werden, etwa durch einen Import, senden kein invoice.paid. Auch eine Teilzahlung sendet es nicht; das Ereignis wird ausgelöst, wenn der offene Betrag null erreicht.
Format der Anfrage
Jedes Ereignis ist ein POST mit JSON-Body und diesen Headern:
| Header | Wert |
|---|---|
Content-Type | application/json |
User-Agent | Pricely-Webhooks/1.0 |
X-Pricely-Event | Der Name des Ereignisses, zum Beispiel invoice.paid. |
X-Pricely-Delivery | Eine eindeutige ID für diese Zustellung, etwa evt_3f9a…. Wiederholungen behalten dieselbe ID. |
X-Pricely-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256>. Siehe Signaturen prüfen. |
Der Body hat immer dieselben vier Schlüssel: id (die Zustell-ID, identisch mit X-Pricely-Delivery), event, data und 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 enthält dieselben data-Felder wie invoice.paid, mit dem noch offenen Betrag in balanceDue und paidAt auf null.
| Feld | Typ | Hinweise |
|---|---|---|
id | string | Die ID des Angebots oder der Rechnung in Pricely. |
number | string | Die Dokumentnummer, die Ihr Kunde sieht. |
clientId, clientName, clientEmail | string | Leere Strings, wenn dem Dokument kein Kunde zugeordnet ist oder der Kunde keine E-Mail-Adresse hat. |
total, amountPaid, balanceDue | number | Beträge in der Haupteinheit, zum Beispiel 4800 für $4,800.00. |
currency | string | Der dreistellige Währungscode des Dokuments. |
acceptedAt, dueDate, paidAt, timestamp | string oder null | ISO-8601-Zeitangaben in UTC. |
Das Testereignis verwendet dieselben Header und dieselbe Signatur. Sein Body sieht so aus:
{
"id": "evt_0c4d2a9e7f1b4c3d8e6a5b2f9d1c7e30",
"event": "ping",
"data": {
"message": "Test event from Pricely",
"orgId": "org_2f81c4"
},
"timestamp": "2026-10-30T15:04:05.000Z"
}
Signaturen prüfen
Prüfen Sie die Signatur, bevor Sie einer Anfrage vertrauen. X-Pricely-Signature besteht aus zwei Teilen: t, der Unix-Zeit in Sekunden, zu der die Anfrage signiert wurde, und v1, einem hexadezimalen HMAC-SHA256 der Zeichenfolge t + . + roher Request-Body, mit Ihrem Signatur-Secret als Schlüssel.
- Rohen Body lesen
Verwenden Sie genau die empfangenen Bytes. Wenn Sie das JSON parsen und neu serialisieren, ändern sich die Bytes, und die Signatur stimmt nicht mehr.
- Header zerlegen
Teilen Sie ihn an den Kommas und jeden Teil am ersten
=, umtundv1zu erhalten. - HMAC neu berechnen
Berechnen Sie den HMAC-SHA256 von
t.bodymit Ihremwhsec_…-Secret und vergleichen Sie ihn in konstanter Zeit mitv1. - Zeit prüfen
Lehnen Sie Anfragen ab, bei denen
tmehr als fünf Minuten von der Uhr Ihres Servers abweicht. So lassen sich alte Anfragen nicht erneut einspielen.
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)
Zustellung und Wiederholungen
- Nur HTTPS. Pricely stellt nur an öffentliche
https://-URLs zu. Lokale und private Netzwerkadressen wielocalhost,*.localoder192.168.x.xsind gesperrt. - Keine Weiterleitungen. Leitet Ihre URL woandershin weiter, schlägt die Zustellung fehl. Verwenden Sie die endgültige URL.
- Antworten Sie innerhalb von 10 Sekunden mit 2xx. Jeder
2xx-Status bestätigt den Empfang. Antworten Sie zuerst und erledigen Sie langsame Arbeit danach. - Automatische Wiederholungen. Pricely unternimmt insgesamt bis zu drei Versuche und wartet dazwischen 1 Sekunde, dann 2 Sekunden. Jeder Versuch wird mit einem neuen
terneut signiert.
| Ihr Endpunkt … | Was Pricely tut |
|---|---|
antwortet mit 2xx | Zugestellt. Keine Wiederholung. |
| ist nicht erreichbar oder braucht länger als 10 Sekunden | Wiederholt. |
antwortet mit 408, 429 oder 5xx | Wiederholt. |
antwortet mit einem anderen 4xx, etwa 400 oder 401 | Bricht ab. Der Fehler ist endgültig. |
Die Schaltfläche Test senden unternimmt einen einzigen Versuch ohne Wiederholung, damit Sie das Ergebnis sofort sehen. Der Dialog zeigt außerdem die Letzte Zustellung: das Ereignis, ob es zugestellt wurde oder fehlschlug, den Statuscode, den Zeitpunkt und die Anzahl der Versuche.
Fehlerbehebung
| Problem | Was Sie prüfen sollten |
|---|---|
| Verwenden Sie eine öffentliche https://-URL. Lokale und private Netzwerkadressen sind gesperrt. | Die URL zeigt auf Ihren eigenen Rechner oder ein privates Netzwerk. Verwenden Sie eine öffentliche HTTPS-URL oder einen Tunnel, der Ihnen eine bereitstellt. |
| Ihr Endpunkt hat mit 401 geantwortet. (oder einem anderen Code) | Pricely hat Ihren Endpunkt erreicht, aber er hat die Anfrage abgelehnt. Alles außerhalb von 200–299 gilt als Fehler. Prüfen Sie die Logs Ihres Endpunkts. |
| Endpunkt nicht erreichbar: Keine Antwort innerhalb von 10 s | Ihr Endpunkt ist zu langsam. Antworten Sie zuerst mit 2xx und erledigen Sie dann die Arbeit. |
| Endpunkt nicht erreichbar (aus einem anderen Grund) | Der Host hat nicht geantwortet, sein TLS-Zertifikat ist ungültig, oder die URL leitet weiter. Prüfen Sie, ob sich die URL im Browser öffnet und ihre endgültige Adresse ist. |
| Die Signatur stimmt nicht | Prüfen Sie gegen den rohen Body, nicht gegen neu serialisiertes JSON, und verwenden Sie das Secret, das aktuell im Dialog angezeigt wird. Haben Sie auf „Neues Secret“ geklickt, speichern Sie den Webhook und aktualisieren Sie Ihren Endpunkt. |
Kein X-Pricely-Signature-Header | Der Webhook wurde ohne Secret gespeichert. Öffnen Sie den Dialog und klicken Sie auf „Webhook speichern“, um eines zu hinterlegen. |
| Dasselbe Ereignis kam zweimal an | Eine Wiederholung nach einer Zeitüberschreitung oder einem 5xx. Deduplizieren Sie anhand von X-Pricely-Delivery. |
Kein invoice.overdue für eine alte Rechnung | Das ist so gewollt. Das Ereignis wird einmal gesendet, wenn die Rechnung ihr Fälligkeitsdatum erstmals überschreitet – nicht für Rechnungen, die schon vorher überfällig waren. |
Tarife
Webhooks sind in Business und Enterprise enthalten. In anderen Tarifen zeigt die Karte „Webhooks“ ein Schloss-Symbol und einen Upgrade-Link.