Guide to Quoter Webhooks: Features and Best Practices
Quoter webhooks notify your application when a quote, person, or payment is created or updated. If you're building on Quoter for sales quoting, webhooks are how you react to these changes without polling.
This guide covers how Quoter webhooks work, the object types you subscribe to, how its MD5 hash-field verification works (and why it's weak), and the best practices for production.
What are Quoter webhooks?
Quoter webhooks are POSTed as application/x-www-form-urlencoded with three fields: hash, timestamp, and data (the data field is a JSON or XML string, chosen at setup). Verification is a weak MD5-based shared-secret scheme, not HMAC-SHA256 and not Standard Webhooks: you compute md5(HASH_KEY + timestamp + data) and compare it to the hash field. The hash key is optional, so a webhook can run with no verification at all, which is worth flagging in any integration.
Quoter webhook features
| Feature | Details |
|---|---|
| Delivery | application/x-www-form-urlencoded with hash, timestamp, data |
data field | A JSON or XML string, chosen at setup |
| Verification | md5(HASH_KEY + timestamp + data) compared to the hash form field |
| Hash key | Optional, so verification can be absent entirely |
| Signature location | A form field (hash), not an HTTP header |
| Triggers | Object type (Quote, Person, Payment) on create or update |
| Configuration | Settings > Integrations: target URL, Applies To, format, optional hash key |
| SDK | None |
Common events
Quoter has no dotted event names (there's no quote.published or quote.won). Instead, you subscribe an object type that fires on create or update:
| Object type | Fires when |
|---|---|
Quote | A quote is created or updated |
Person | A person is created or updated |
Payment | A payment is created or updated |
Determine what happened from the object type and the contents of the data field, not from an event name.
Setting up Quoter webhooks
Configure webhooks under Settings > Integrations: set the target URL, choose the object type under "Applies To", pick the data format (JSON or XML), and set an optional hash key. Set the hash key, otherwise deliveries arrive with no way to verify them.
Securing Quoter webhooks
Compute md5(HASH_KEY + timestamp + data) and compare it to the hash field. Read the data field string exactly as received, don't re-parse and re-serialize it before hashing, or the bytes change and the hash won't match. Optionally reject requests whose timestamp (GMT UNIX seconds) is older than about 300 seconds.
const crypto = require("crypto");
const HASH_KEY = process.env.QUOTER_HASH_KEY;
function verify(fields) {
if (!HASH_KEY) return false; // refuse unverified deliveries: set a hash key
// Hash the data string exactly as received (do not re-serialize)
const expected = crypto
.createHash("md5")
.update(`${HASH_KEY}${fields.timestamp}${fields.data}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(fields.hash || "");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return false;
// Optional replay guard: reject stale timestamps (GMT UNIX seconds)
if (Math.abs(Date.now() / 1000 - Number(fields.timestamp)) > 300) return false;
return true;
}
app.post("/webhook", express.urlencoded({ extended: false }), (req, res) => {
if (!verify(req.body)) return res.sendStatus(401);
res.sendStatus(200); // acknowledge fast
const data = JSON.parse(req.body.data); // if configured as JSON
processQueue.add(data); // branch on object type, async
});
The same check in Python:
import hashlib
import hmac
import os
import time
HASH_KEY = os.environ.get("QUOTER_HASH_KEY")
def verify(fields: dict) -> bool:
if not HASH_KEY:
return False # refuse unverified deliveries: set a hash key
expected = hashlib.md5(
f"{HASH_KEY}{fields['timestamp']}{fields['data']}".encode()
).hexdigest()
if not hmac.compare_digest(expected, fields.get("hash", "")):
return False
return abs(time.time() - int(fields["timestamp"])) <= 300 # optional replay guard
Quoter webhook limitations and pain points
Verification is weak, and optional
The Problem: The scheme is MD5 over HASH_KEY + timestamp + data, and the hash key is optional. Without a hash key, deliveries are unverified, and even with one, MD5 is a weak choice.
Why It Happens: Quoter uses an optional MD5 shared-secret scheme.
Workarounds:
- Always set a hash key, refuse deliveries when it's missing, and pair verification with HTTPS and a replay-window check.
How Hookdeck Can Help: Hookdeck can front the endpoint, enforce verification, and apply its own controls, so an optional weak scheme isn't your only line of defense.
The signature is a form field, not a header
The Problem: The hash lives in the form body, not an HTTP header, so code that reads a signature header finds nothing.
Why It Happens: Quoter posts the hash as a hash form field.
Workarounds:
- Read
hash,timestamp, anddatafrom the urlencoded body.
How Hookdeck Can Help: Hookdeck parses and verifies the payload at the edge regardless of where the signature sits.
Hash the data string as received
The Problem: The hash is over the data string exactly as sent. Parsing then re-serializing it (reordering keys, changing whitespace) changes the bytes and breaks the match.
Why It Happens: MD5 is over the literal data string.
Workarounds:
- Capture the raw
datafield value and hash it before any parsing.
How Hookdeck Can Help: Hookdeck verifies against the received bytes at the edge, so downstream parsing can't invalidate the check.
No dotted event names
The Problem: There's no quote.published or quote.won. You subscribe an object type that fires on create or update, so handlers keyed to specific event names have nothing to match.
Why It Happens: Quoter models triggers as object types, not typed events.
Workarounds:
- Subscribe the object type (Quote, Person, Payment) and infer the change from the
datafield.
How Hookdeck Can Help: Hookdeck's filters can route on fields inside data, giving you finer routing than the object type alone.
Best practices
Always set a hash key and verify
Set a hash key, compute md5(HASH_KEY + timestamp + data), compare to hash in constant time, and refuse deliveries with no key.
Hash the raw data string
Hash the data value exactly as received, before parsing.
Enforce a replay window
Reject deliveries whose timestamp is older than about 300 seconds.
Return 200 and process asynchronously
Acknowledge quickly and defer work to a queue. See why to process webhooks asynchronously.
Make Quoter webhooks production-ready
Hookdeck fronts your endpoint, verifies, deduplicates, and durably queues every event
Conclusion
Quoter webhooks are form-urlencoded with hash, timestamp, and data, verified by md5(HASH_KEY + timestamp + data) compared to the hash field, a weak scheme whose hash key is optional. Always set a hash key and refuse unverified deliveries, hash the raw data string, enforce a replay window, and subscribe object types rather than event names.
Hookdeck fronts your endpoint, verifies, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique events.
Get started with Hookdeck for free and handle Quoter webhooks reliably in minutes.