# QuickBooks Webhooks

## When to Use This Skill

* How do I receive QuickBooks Online webhooks?
* How do I verify the `intuit-signature` header with my Verifier Token?
* Why is my QuickBooks webhook signature verification failing?
* How do I handle the new CloudEvents payload (`qbo.invoice.created.v1`) and the legacy `eventNotifications` payload?
* How do I process QuickBooks webhooks for multiple companies (realm IDs) in one notification?

## Verification (core)

Intuit signs the **raw** request body with HMAC-SHA256 keyed on your app's **Verifier Token**, base64-encodes the digest, and sends it in the `intuit-signature` header. There is no timestamp in the signed content. Read the raw body **before** JSON parsing and compare timing-safe. Intuit's SDKs have no webhook-verification helper, so verify manually.

Node:

```javascript
const crypto = require('crypto');

function verifyQuickBooksSignature(rawBody, signature, verifierToken) {
  if (!signature) return false;
  const expected = crypto.createHmac('sha256', verifierToken).update(rawBody).digest('base64');
  try {
    return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  } catch {
    return false; // length mismatch = invalid
  }
}

```

Python:

```python
import hmac, hashlib, base64

def verify_quickbooks_signature(raw_body: bytes, signature: str, verifier_token: str) -> bool:
    if not signature:
        return False
    expected = base64.b64encode(hmac.new(verifier_token.encode(), raw_body, hashlib.sha256).digest())
    return hmac.compare_digest(signature.encode(), expected)

```

> **Respond with 200 within 3 seconds.** Intuit retries failures (10s, 20s, 30s, 5m, 20m, 2h, 4h, 6h, then every 6h) and may hold back later events until the first one is acknowledged. Queue the work and process it asynchronously.

> **For complete handlers with route wiring, event dispatch, and tests**, see:
> 
> * [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/quickbooks-webhooks/examples/express/)
> * [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/quickbooks-webhooks/examples/nextjs/)
> * [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/quickbooks-webhooks/examples/fastapi/)

## Payload Structure

QuickBooks webhooks use the **CloudEvents** format: a JSON **array** of events, which can cover several companies (realm IDs) in one delivery. Payloads are thin. Fetch the full record from the Accounting API using `intuitentityid` and `intuitaccountid`.

```json
[
  {
    "specversion": "1.0",
    "id": "88cd52aa-33b6-4351-9aa4-47572edbd068",
    "source": "intuit.dsnBgbseACLLRZNxo2dfc4evmEJdxde58xeeYcZliOU=",
    "type": "qbo.invoice.created.v1",
    "datacontenttype": "application/json",
    "time": "2026-09-10T21:31:25Z",
    "intuitentityid": "1234",
    "intuitaccountid": "310687",
    "data": {}
  }
]

```

Apps built before the 2026 migration got the **legacy** object shape `{"eventNotifications":[{"realmId", "dataChangeEvent":{"entities":[{"name","id","operation","lastUpdated"}]}}]}`. The examples normalize both shapes into one event list, and the same signature check covers both.

## Common Event Types

`type` is `qbo.{entitytype}.{event}.v1`, with the entity name in lowercase.

| Event type | Triggered When |
| --- | --- |
| `qbo.invoice.created.v1` | An invoice is created |
| `qbo.invoice.updated.v1` | An invoice is changed |
| `qbo.payment.created.v1` | A customer payment is recorded |
| `qbo.customer.created.v1` | A customer is created |
| `qbo.customer.updated.v1` | A customer is changed |
| `qbo.customer.merged.v1` | Two customers are merged (`data.deletedid` holds the removed ID) |
| `qbo.salesreceipt.updated.v1` | A sales receipt is changed |
| `qbo.employee.created.v1` | An employee is created |

Supported operations per entity are Create, Update, Delete, Merge, Void and Emailed, depending on the entity (see [overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/quickbooks-webhooks/references/overview.md)). The examples dispatch on the `{event}` segment (`created`, `updated`, `deleted`, `merged`, `voided`, `emailed`) so every entity is covered.

## Environment Variables

```bash
QUICKBOOKS_WEBHOOK_VERIFIER_TOKEN=your_verifier_token   # Developer dashboard > your app > Webhooks > Verifier Token

```

Development (sandbox) and Production each have **their own** Verifier Token. Use the one that matches the environment that sends to the endpoint.

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 quickbooks --path /webhooks/quickbooks

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/quickbooks-webhooks/references/overview.md) - Payload formats (CloudEvents and legacy), event types, supported entities
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/quickbooks-webhooks/references/setup.md) - Configure webhooks in the Intuit Developer dashboard, get the Verifier Token
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/quickbooks-webhooks/references/verification.md) - Signature verification details, gotchas, debugging