Agent skill
Quoter Webhooks Skill
Receive and verify Quoter webhooks. Use when setting up Quoter webhook handlers, debugging the MD5 hash verification, or handling Quote, Person, and Payment create/update events posted as x-www-form-urlencoded.
Install this skill
npx skills add hookdeck/webhook-skills --skill quoter-webhooks
When to Use This Skill
- Setting up Quoter webhook handlers
- Debugging Quoter hash verification failures
- Understanding Quoter object types (Quote, Person, Payment) and create vs update
- Parsing the
application/x-www-form-urlencodedhash/timestamp/datapayload
⚠️ Security Warning: Weak Verification Scheme
Quoter does not use HMAC-SHA256, and it is not Standard Webhooks. It uses a legacy MD5 shared-secret hash, and the hash key is optional — a Quoter webhook can be configured with no verification at all.
- The signature is a form field named
hash, not an HTTP header. - Always set a hash key in Quoter (Settings → Integrations). Without one, anyone who learns your endpoint URL can forge requests.
- MD5 is cryptographically broken. Treat this as a low-assurance check and pair it with a network-level control (IP allowlist, a shared secret in the URL path, or fronting the endpoint with Hookdeck).
Verification (core)
Quoter POSTs application/x-www-form-urlencoded with three fields: hash, timestamp, and data. The data field is the JSON (or XML) payload as a string. Verify by computing md5(HASH_KEY + timestamp + data) and comparing to hash. Hash the data string exactly as received — never re-serialize the parsed JSON, or the hash won't match.
Node:
const crypto = require('crypto');
// timestamp and data come from the parsed form body (already URL-decoded).
function verifyQuoter(hashKey, timestamp, data, receivedHash) {
if (!hashKey || !receivedHash) return false; // no hash key => reject (verification disabled)
const expected = crypto
.createHash('md5')
.update(hashKey + timestamp + data) // data is the raw JSON/XML string, unmodified
.digest('hex');
// Reject stale requests: timestamp is GMT UNIX seconds
const fresh = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) <= 300;
try {
return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(receivedHash));
} catch {
return false; // length mismatch = invalid
}
}
Python:
import hashlib, hmac, time
def verify_quoter(hash_key, timestamp, data, received_hash):
if not hash_key or not received_hash: # no hash key => reject (verification disabled)
return False
expected = hashlib.md5(f"{hash_key}{timestamp}{data}".encode("utf-8")).hexdigest()
fresh = abs(int(time.time()) - int(timestamp)) <= 300
return fresh and hmac.compare_digest(expected, received_hash)
For complete handlers with form parsing, event dispatch, and tests, see:
Events: Object Types, Not Event Names
Quoter has no dotted event names (there is no quote.published / quote.won / quote.lost). Instead you subscribe an object type in Settings → Integrations via the "Applies To" option, and it fires whenever an object of that type is created or updated.
| Object Type ("Applies To") | Fires When | Common Use Cases |
|---|---|---|
Quote | A quote is created or updated | Sync quotes to CRM/ERP, trigger fulfillment |
Person | A person (contact) is created or updated | Keep contacts in sync, enrich CRM records |
Payment | A payment is created or updated | Reconcile payments, update invoices |
The object type is not included in the payload or an HTTP header — each integration is configured for a single object type and fires on both create and update. Because the request itself does not identify the object type, configure a distinct target URL per object type and add a hint your handler can read, e.g. https://your-app.com/webhooks/quoter?object=quote. The examples dispatch on this object query parameter. Since the same object fires on create and update, process idempotently keyed on the record's id.
Environment Variables
# Shared secret ("Hash Key") configured in Quoter → Settings → Integrations.
# Optional in Quoter, but REQUIRED by these examples — always set one.
QUOTER_HASH_KEY=your_hash_key_here
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 quoter --path /webhooks/quoter
Use the printed URL (append ?object=quote, ?object=person, or ?object=payment) as the target URL in Quoter → Settings → Integrations.
Reference Materials
- references/overview.md - Quoter webhook concepts, object types, payload
- references/setup.md - Settings → Integrations configuration
- references/verification.md - MD5 hash verification details and gotchas