Gareth Wilson Gareth Wilson

Guide to Quoter Webhooks: Features and Best Practices

Published


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

FeatureDetails
Deliveryapplication/x-www-form-urlencoded with hash, timestamp, data
data fieldA JSON or XML string, chosen at setup
Verificationmd5(HASH_KEY + timestamp + data) compared to the hash form field
Hash keyOptional, so verification can be absent entirely
Signature locationA form field (hash), not an HTTP header
TriggersObject type (Quote, Person, Payment) on create or update
ConfigurationSettings > Integrations: target URL, Applies To, format, optional hash key
SDKNone

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 typeFires when
QuoteA quote is created or updated
PersonA person is created or updated
PaymentA 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, and data from 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 data field 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 data field.

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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.