# 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:

```json
{
  "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:

```json
{
  "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

1. As an administrator, open admin mode, go to **System Settings** and select the **API Settings** tab.
2. In the **Webhooks** section, add a new webhook.
3. Enter a **Webhook Name** and the HTTPS **Webhook URL**.
4. Tick each event the webhook should receive.
5. Choose the authentication type: **Token**, **Basic** or **None**.
6. 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:

```javascript
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:

```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](/event-gateway) checks the `Authorization` header 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 `2xx` as 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 and `execution_date` when `event_info` is 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 `productsUpload` events by `event_info.request_id`.
* Use `approval_details` to tell approval steps apart.
* Filter DealRoom events on the `action_summary` key.

**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](/webhooks/guides/implement-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](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## 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](https://hookdeck.com) checks each DealHub delivery, stores it, and retries into your service on your schedule.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle DealHub webhooks reliably in minutes.