Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions apps/docs/content/docs/en/integrations/quickbooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2919,3 +2919,215 @@ Download a QuickBooks file attachment through its short-lived URL
| `attachmentId` | string | Downloaded QuickBooks attachment ID |



## Triggers

A **Trigger** is a block that starts a workflow when an event happens in this service.

### QuickBooks Bill Events

Trigger when selected Bill events occur in QuickBooks

#### Configuration

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `triggerCredentials` | string | Yes | QuickBooks Account |
| `eventTypes` | string | Yes | Event Types |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `eventId` | string | Intuit webhook event ID |
| `eventType` | string | Full Intuit CloudEvent type |
| `entityType` | string | QuickBooks entity type |
| `action` | string | QuickBooks webhook action |
| `entityId` | string | QuickBooks entity ID |
| `realmId` | string | QuickBooks company realm ID |
| `eventTime` | string | Event timestamp |
| `specVersion` | string | CloudEvents specification version |
| `source` | string | Intuit event source |
| `contentType` | string | Event content type, when provided |
| `data` | json | Optional event data supplied by Intuit |


---

### QuickBooks Bill Payment Events

Trigger when selected Bill Payment events occur in QuickBooks

#### Configuration

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `triggerCredentials` | string | Yes | QuickBooks Account |
| `eventTypes` | string | Yes | Event Types |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `eventId` | string | Intuit webhook event ID |
| `eventType` | string | Full Intuit CloudEvent type |
| `entityType` | string | QuickBooks entity type |
| `action` | string | QuickBooks webhook action |
| `entityId` | string | QuickBooks entity ID |
| `realmId` | string | QuickBooks company realm ID |
| `eventTime` | string | Event timestamp |
| `specVersion` | string | CloudEvents specification version |
| `source` | string | Intuit event source |
| `contentType` | string | Event content type, when provided |
| `data` | json | Optional event data supplied by Intuit |


---

### QuickBooks Customer Events

Trigger when selected Customer events occur in QuickBooks

#### Configuration

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `triggerCredentials` | string | Yes | QuickBooks Account |
| `eventTypes` | string | Yes | Event Types |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `eventId` | string | Intuit webhook event ID |
| `eventType` | string | Full Intuit CloudEvent type |
| `entityType` | string | QuickBooks entity type |
| `action` | string | QuickBooks webhook action |
| `entityId` | string | QuickBooks entity ID |
| `realmId` | string | QuickBooks company realm ID |
| `eventTime` | string | Event timestamp |
| `specVersion` | string | CloudEvents specification version |
| `source` | string | Intuit event source |
| `contentType` | string | Event content type, when provided |
| `data` | json | Optional event data supplied by Intuit |


---

### QuickBooks Invoice Events

Trigger when selected Invoice events occur in QuickBooks

#### Configuration

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `triggerCredentials` | string | Yes | QuickBooks Account |
| `eventTypes` | string | Yes | Event Types |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `eventId` | string | Intuit webhook event ID |
| `eventType` | string | Full Intuit CloudEvent type |
| `entityType` | string | QuickBooks entity type |
| `action` | string | QuickBooks webhook action |
| `entityId` | string | QuickBooks entity ID |
| `realmId` | string | QuickBooks company realm ID |
| `eventTime` | string | Event timestamp |
| `specVersion` | string | CloudEvents specification version |
| `source` | string | Intuit event source |
| `contentType` | string | Event content type, when provided |
| `data` | json | Optional event data supplied by Intuit |


---

### QuickBooks Payment Events

Trigger when selected Payment events occur in QuickBooks

#### Configuration

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `triggerCredentials` | string | Yes | QuickBooks Account |
| `eventTypes` | string | Yes | Event Types |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `eventId` | string | Intuit webhook event ID |
| `eventType` | string | Full Intuit CloudEvent type |
| `entityType` | string | QuickBooks entity type |
| `action` | string | QuickBooks webhook action |
| `entityId` | string | QuickBooks entity ID |
| `realmId` | string | QuickBooks company realm ID |
| `eventTime` | string | Event timestamp |
| `specVersion` | string | CloudEvents specification version |
| `source` | string | Intuit event source |
| `contentType` | string | Event content type, when provided |
| `data` | json | Optional event data supplied by Intuit |


---

### QuickBooks Purchase Order Events

Trigger when selected Purchase Order events occur in QuickBooks

#### Configuration

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `triggerCredentials` | string | Yes | QuickBooks Account |
| `eventTypes` | string | Yes | Event Types |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `eventId` | string | Intuit webhook event ID |
| `eventType` | string | Full Intuit CloudEvent type |
| `entityType` | string | QuickBooks entity type |
| `action` | string | QuickBooks webhook action |
| `entityId` | string | QuickBooks entity ID |
| `realmId` | string | QuickBooks company realm ID |
| `eventTime` | string | Event timestamp |
| `specVersion` | string | CloudEvents specification version |
| `source` | string | Intuit event source |
| `contentType` | string | Event content type, when provided |
| `data` | json | Optional event data supplied by Intuit |


---

### QuickBooks Vendor Events

Trigger when selected Vendor events occur in QuickBooks

#### Configuration

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `triggerCredentials` | string | Yes | QuickBooks Account |
| `eventTypes` | string | Yes | Event Types |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `eventId` | string | Intuit webhook event ID |
| `eventType` | string | Full Intuit CloudEvent type |
| `entityType` | string | QuickBooks entity type |
| `action` | string | QuickBooks webhook action |
| `entityId` | string | QuickBooks entity ID |
| `realmId` | string | QuickBooks company realm ID |
| `eventTime` | string | Event timestamp |
| `specVersion` | string | CloudEvents specification version |
| `source` | string | Intuit event source |
| `contentType` | string | Event content type, when provided |
| `data` | json | Optional event data supplied by Intuit |

1 change: 1 addition & 0 deletions apps/sim/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ CRON_SECRET=your_cron_secret # Use `openssl rand -hex 32` to generate. Authentic
# QUICKBOOKS_CLIENT_ID=
# QUICKBOOKS_CLIENT_SECRET=
# QUICKBOOKS_ENV=sandbox # Required when QuickBooks is configured: sandbox or production
# QUICKBOOKS_WEBHOOK_VERIFIER_TOKEN= # Verifier token from the Intuit webhook configuration

# Azure Blob Storage takes precedence over S3 if both are configured
# AZURE_ACCOUNT_NAME= # Azure storage account name
Expand Down
100 changes: 100 additions & 0 deletions apps/sim/app/api/webhooks/quickbooks/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/** @vitest-environment node */

import crypto from 'node:crypto'
import { requestUtilsMockFns, resetEnvMock, setEnv } from '@sim/testing'
import { NextRequest } from 'next/server'
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'

const { mockEnqueue, mockRelease } = vi.hoisted(() => ({
mockEnqueue: vi.fn(),
mockRelease: vi.fn(),
}))

vi.mock('@/background/quickbooks-webhook-ingress', () => ({
enqueueQuickBooksWebhookIngress: mockEnqueue,
}))
vi.mock('@/lib/core/admission/gate', () => ({
admissionRejectedResponse: vi.fn(() => new Response(null, { status: 503 })),
tryAdmit: vi.fn(() => ({ release: mockRelease })),
}))
vi.mock('@/lib/core/utils/with-route-handler', () => ({
withRouteHandler:
(handler: (request: NextRequest) => Promise<Response>) => (request: NextRequest) =>
handler(request),
}))

import { POST } from '@/app/api/webhooks/quickbooks/route'

const validEvent = {
specversion: '1.0',
id: 'event-1',
source: 'quickbooks-online',
type: 'qbo.invoice.created.v1',
time: '2026-08-03T12:00:00Z',
intuitentityid: '123',
intuitaccountid: '456',
}

function request(body: string, signature?: string): NextRequest {
return new NextRequest('http://localhost/api/webhooks/quickbooks', {
method: 'POST',
headers: {
'content-type': 'application/json',
...(signature ? { 'intuit-signature': signature } : {}),
},
body,
})
}

function signedRequest(value: unknown): NextRequest {
const body = JSON.stringify(value)
const signature = crypto.createHmac('sha256', 'verifier').update(body).digest('base64')
return request(body, signature)
}

describe('QuickBooks webhook ingress route', () => {
beforeEach(() => {
vi.clearAllMocks()
setEnv({ QUICKBOOKS_WEBHOOK_VERIFIER_TOKEN: 'verifier' })
requestUtilsMockFns.mockGenerateRequestId.mockReturnValue('request-1')
mockEnqueue.mockResolvedValue('job-1')
})
afterAll(() => {
resetEnvMock()
requestUtilsMockFns.mockGenerateRequestId.mockReset()
})

it('accepts a signed multi-company batch only after durable enqueue', async () => {
const response = await POST(
signedRequest([validEvent, { ...validEvent, id: 'event-2', intuitaccountid: '789' }])
)
expect(response.status).toBe(200)
expect(mockEnqueue).toHaveBeenCalledWith(
expect.objectContaining({
events: [validEvent, { ...validEvent, id: 'event-2', intuitaccountid: '789' }],
requestId: 'request-1',
})
)
expect(mockRelease).toHaveBeenCalledOnce()
})

it('rejects missing signatures and malformed signed payloads before enqueue', async () => {
expect((await POST(request(JSON.stringify([validEvent])))).status).toBe(401)
expect((await POST(signedRequest({ invalid: true }))).status).toBe(400)
expect(mockEnqueue).not.toHaveBeenCalled()
})

it('rejects batches over the 1,000 event bound', async () => {
const events = Array.from({ length: 1001 }, (_, index) => ({
...validEvent,
id: `event-${index}`,
}))
expect((await POST(signedRequest(events))).status).toBe(400)
expect(mockEnqueue).not.toHaveBeenCalled()
})

it('returns 503 when durable acceptance fails', async () => {
mockEnqueue.mockRejectedValue(new Error('queue unavailable'))
expect((await POST(signedRequest([validEvent]))).status).toBe(503)
})
})
Loading