Guide to DealHub Webhooks: Features and Best Practices
DealHub is a CPQ (configure, price, quote) platform with DealRoom, a digital sales room where buyers review and sign quotes. Its webhooks report what happens to quotes, versions and users, and what buyers and sellers do inside a DealRoom. Instead of polling the API for quote status, your system hears from DealHub when a quote is submitted, approved, published or signed.
This guide covers how DealHub webhooks are configured and authenticated, the events and payloads they carry, the gaps in DealHub's documentation, and the best practices for running a DealHub receiver in production.
What are DealHub webhooks?
A DealHub webhook is an HTTPS endpoint that an administrator registers in DealHub and subscribes to a list of events. When one of those events happens, DealHub sends an HTTP POST with a JSON payload. The event field in the body names the event.
DealHub doesn't sign its webhooks. Instead, you choose how DealHub authenticates itself to your endpoint: a bearer token, Basic credentials, or nothing. Whatever you choose is sent in the Authorization header of every request, and your receiver checks it.
DealHub webhook features
| Feature | Details |
|---|---|
| Configuration | Admin mode > System Settings > API Settings > Webhooks |
| Endpoint | HTTPS only |
| Subscription model | A checkbox per event |
| Method and format | POST with a JSON body |
| Authentication | Token (Authorization: Bearer <TOKEN>), Basic (Authorization: Basic <CREDENTIALS>) or None |
| Signature | None |
| Event identifier | event field in the body |
| Delivery metadata | event_info with webhook_id, event_id, api_version and, for async requests, request_id |
| Timestamps | execution_date in GMT, formatted yyyy-mm-dd hh:mm:ss |
| Custom data | Answers to up to five playbook questions in additional_data, configured per Version |
| Retries | Not documented |
| Source IPs | Not documented |
Common events
Quote events
| Event | Fires when |
|---|---|
draftQuote | A quote is created and saved as a draft, or a draft is modified |
primaryQuote | A user or the system marks a quote as the primary quote |
quotePendingApproval | A quote that needs approval is submitted, and again as each approver approves their part |
quoteReady | A quote is ready to share: submitted with no approval needed, or fully approved |
quoteRejected | An approver rejects the quote |
quoteRecalled | A user recalls (cancels) the quote |
quotePublished | The quote is published to a DealRoom |
quoteUnpublished | A user unpublishes the DealRoom |
quoteWon | The quote is signed, in DealRoom or externally |
Two details matter when you build a state machine on these. If a user creates a quote and submits it in one motion, DealHub sends only quotePendingApproval, not draftQuote. And when Quote B becomes primary in place of Quote A, DealHub sends primaryQuote for Quote B only: "the consuming system must infer" that Quote A lost the flag.
A quoteWon payload looks like this:
{
"event": "quoteWon",
"status": "Won",
"external_opportunity_id": "786865656565",
"external_quote_id": "",
"external_customer_id": "65545425413",
"dealhub_opportunity_id": "2323232565685498",
"dealhub_quote_id": "6563232565685498",
"primary_quote": true,
"execution_date": "2021-10-22 16:39:08",
"executed_by": "john@mydomain.com",
"impersonated_by": "",
"additional_data": [
{ "q1": "some text" },
{ "q2": "red;green;blue" },
{ "q3": "" }
]
}
DealHub's suggested pattern is to take dealhub_quote_id from the webhook and call the Get Quote API for line items and answers, rather than expecting the payload to carry everything.
Version, user and system events
| Event | Fires when |
|---|---|
version | An administrator creates, activates or reactivates a version. action is created, activated or reactivated |
user | A user is created, modified or deleted. action is created, modified or deleted |
productsUpload | An SFTP or Open API product upload completes. upload_status is success or error |
crmImport | A CRM Import request (buyer accounts and contacts) completes, with total, succeeded, failed and skipped counts |
Failure WebHook v1 | An asynchronous API request, such as duplicating or activating a version, fails |
An SFTP upload loads data into every draft Version on the account, so productsUpload is "published once per affected draft Version". Expect several events for one upload. The documented Failure WebHook v1 body contains only error_description and error_code, with no event field.
DealRoom events
All DealRoom activity arrives as one event, DEALROOM_EVENT. The key inside action_summary says what happened, and its value is a readable message:
{
"event": "DEALROOM_EVENT",
"DealRoom_URL": "https://...",
"Dealhub_Opportunity_id": "OPP-123",
"dealhub_quote_id": "QUOTE-456",
"Event_source": "DealRoom",
"executed_by": "John Doe",
"execution_date": "2024-02-06 16:41:29",
"action_summary": {
"DEAL_BOX_FILE_ADDED": "John Doe added contract_final.pdf to the DealBox"
}
}
DealHub lists 37 keys across DealBox files, deal management (DEAL_VIEW, DEAL_SHARE, DEAL_SIGN, DEAL_CO_SIGN), document management, online redlining and revisions. Its own advice is to filter on the key and act only on business-critical ones such as DEAL_SIGN, REDLINE_FULLY_ACCEPTED and REVISION_SIGNED. Note the mixed casing: Dealhub_Opportunity_id and Impersonated_By here, dealhub_opportunity_id and impersonated_by in quote events.
Setting up DealHub webhooks
- As an administrator, open admin mode, go to System Settings and select the API Settings tab.
- In the Webhooks section, add a new webhook.
- Enter a Webhook Name and the HTTPS Webhook URL.
- Tick each event the webhook should receive.
- Choose the authentication type: Token, Basic or None.
- Save the configuration.
To include playbook answers, open Version Settings for a version, go to Advanced Settings > Open API, and pick up to five questions in Webhook data mapping. Their answers appear in additional_data for events raised in that version's context. Multi-select answers are separated by semicolons.
Securing DealHub webhooks
Choose Token or Basic and check the Authorization header on every request. In Token mode, DealHub sends:
Authorization: Bearer <TOKEN_SHARED_BY_ENDPOINT_PROVIDER>
In Basic mode, it sends Authorization: Basic <CREDENTIALS>. DealHub doesn't say how it builds CREDENTIALS. Standard HTTP Basic authentication base64-encodes username:password, and that is what the code below assumes, so confirm it against your first real request.
Compare the whole header value in constant time:
const crypto = require('crypto');
const express = require('express');
function expectedAuthorization() {
if (process.env.DEALHUB_WEBHOOK_TOKEN) {
return `Bearer ${process.env.DEALHUB_WEBHOOK_TOKEN}`;
}
const { DEALHUB_WEBHOOK_USERNAME: user, DEALHUB_WEBHOOK_PASSWORD: pass } = process.env;
if (user && pass) {
return `Basic ${Buffer.from(`${user}:${pass}`, 'utf8').toString('base64')}`;
}
return null;
}
function verifyDealhubAuthorization(header, expected) {
if (typeof header !== 'string' || !expected) return false;
const received = Buffer.from(header, 'utf8');
const wanted = Buffer.from(expected, 'utf8');
return received.length === wanted.length && crypto.timingSafeEqual(received, wanted);
}
const app = express();
app.post('/webhooks/dealhub', express.raw({ type: '*/*' }), (req, res) => {
const expected = expectedAuthorization();
if (!expected) return res.sendStatus(500); // fail closed
if (!verifyDealhubAuthorization(req.headers['authorization'], expected)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
// Deduplicate on event.event_info?.event_id, route on event.event, then process asynchronously.
res.sendStatus(200);
});
The same check in Python:
import base64
import hmac
import os
def expected_authorization():
token = os.environ.get("DEALHUB_WEBHOOK_TOKEN")
if token:
return f"Bearer {token}"
user = os.environ.get("DEALHUB_WEBHOOK_USERNAME")
password = os.environ.get("DEALHUB_WEBHOOK_PASSWORD")
if user and password:
return "Basic " + base64.b64encode(f"{user}:{password}".encode()).decode()
return None
def verify_dealhub_authorization(header, expected) -> bool:
if not header or not expected:
return False
return hmac.compare_digest(header.encode(), expected.encode())
Use a long random token. A bearer token is a shared secret sent with every request, so it only protects you over HTTPS, and anyone who sees one request can replay it.
None suits an endpoint that carries its own Shared Access Signature in the URL. Otherwise it leaves the endpoint open to anyone who learns the URL.
Make DealHub webhooks production-ready. Hookdeck Event Gateway checks the
Authorizationheader on every DealHub delivery, durably queues each quote and DealRoom event, and lets you replay any of them into your service.
DealHub webhook limitations and pain points
There is no signature
The Problem: You can confirm a request carried the right credentials, but not that DealHub sent it or that the body wasn't changed.
Why It Happens: DealHub authenticates with a static Authorization header and doesn't sign the body. The header is the same on every request.
Workarounds:
- Use Token or Basic, never None, unless your URL carries its own signature.
- Use a long random token and rotate it if it might have leaked.
- Treat the webhook as a notification and fetch the quote with the Get Quote API before acting on money or signatures.
How Hookdeck Can Help: Hookdeck's DealHub source type compares the whole Authorization header value, so you enter it exactly as DealHub sends it, Bearer or Basic included. Verification is optional on the source, so leave it off if you chose None in DealHub.
Delivery guarantees aren't documented
The Problem: Your endpoint is down while a quote is signed, and you can't tell whether quoteWon will be sent again.
Why It Happens: DealHub's webhook pages describe events and payloads but give no retry schedule, response timeout or failure handling.
Workarounds:
- Return a
2xxas soon as the event is stored. - Reconcile quote status with the API on a schedule, so a missed event is caught.
How Hookdeck Can Help: Hookdeck accepts each DealHub delivery as soon as it arrives and stores it, so an outage on your side doesn't lose events. You can retry or replay stored events into your service once it recovers.
The documented examples don't match the description
The Problem: You plan to deduplicate on event_info.event_id, but the field isn't in any example payload.
Why It Happens: DealHub says "all payloads include a standard event_info object", yet none of its quote, version, user or DealRoom examples show it, and the Failure WebHook v1 example has only two fields. DealRoom events also use different key casing from quote events.
Workarounds:
- Capture a real delivery for each event you handle before relying on any field.
- Fall back to a key built from
event, the entity ID andexecution_datewhenevent_infois missing. - Read keys case-insensitively where DealHub's casing differs.
How Hookdeck Can Help: Hookdeck's event log shows the full body and headers of every delivery, so you can see exactly what DealHub sends before writing your handler.
One action can produce several events
The Problem: A single product upload or approval flow triggers more events than you expected.
Why It Happens: productsUpload fires once per affected draft Version, and quotePendingApproval fires again as each approver approves. DEALROOM_EVENT fires for views, downloads and comments as well as signatures.
Workarounds:
- Group
productsUploadevents byevent_info.request_id. - Use
approval_detailsto tell approval steps apart. - Filter DealRoom events on the
action_summarykey.
How Hookdeck Can Help: Hookdeck filters can drop DealRoom activity you don't need, such as DEAL_VIEW, before it reaches your service.
Best practices
Compare the whole header in constant time
Check the full Authorization value, including the scheme, with crypto.timingSafeEqual or hmac.compare_digest, and check the lengths first in Node. Fail closed if the expected value isn't configured.
Deduplicate deliveries
Store event_info.event_id when it's present and skip events you've already handled. See our guide to webhook idempotency.
Treat optional fields as optional
DealHub omits fields with no value rather than sending empty ones. Use optional access for impersonated_by, external_quote_id, additional_data and anything else marked "can be omitted".
Parse dates as GMT
execution_date has no time zone marker. Parse it as GMT explicitly, or your timestamps will shift by your server's offset.
Acknowledge first, process after
Store the event, return 2xx, then call the Get Quote API or update your CRM. See why to process webhooks asynchronously.
Conclusion
DealHub webhooks carry quote, version, user, DealRoom and system events as JSON, authenticated by a static Authorization header rather than a signature. Choose Token or Basic, compare the whole header in constant time, capture real payloads before trusting fields the examples leave out, and reconcile with the API because DealHub doesn't document retries.
Hookdeck Event Gateway checks each DealHub delivery, stores it, and retries into your service on your schedule.
Get started with Hookdeck for free and handle DealHub webhooks reliably in minutes.