Agent skill
QuickBooks Webhooks Skill
Receive and verify QuickBooks Online (Intuit) webhooks. Use when setting up QuickBooks webhook handlers, debugging intuit-signature verification with the Verifier Token, migrating from the legacy eventNotifications payload to the CloudEvents format, or handling accounting events like qbo.invoice.created.v1, qbo.customer.updated.v1, and qbo.customer.merged.v1.
Install this skill
npx skills add hookdeck/webhook-skills --skill quickbooks-webhooks
When to Use This Skill
- How do I receive QuickBooks Online webhooks?
- How do I verify the
intuit-signatureheader 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 legacyeventNotificationspayload? - 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:
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:
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:
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.
[
{
"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). The examples dispatch on the {event} segment (created, updated, deleted, merged, voided, emailed) so every entity is covered.
Environment Variables
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
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 quickbooks --path /webhooks/quickbooks
Reference Materials
- references/overview.md - Payload formats (CloudEvents and legacy), event types, supported entities
- references/setup.md - Configure webhooks in the Intuit Developer dashboard, get the Verifier Token
- references/verification.md - Signature verification details, gotchas, debugging