/
Documentation

Events & Webhooks

Subscribe to real-time events from SyncBooks and receive webhook notifications when data changes — even when your add-on isn't open.

How Events Work

When something happens in SyncBooks (invoice created, payment received, etc.), an event is dispatched. If your add-on is subscribed, SyncBooks sends a POST request to your webhook URL with the event data.

text
User creates invoice in SyncBooks
        ↓
SyncBooks fires "invoice.created" event
        ↓
Finds all addon subscriptions for this event + org
        ↓
Sends POST to each subscribed webhookUrl
        ↓
Your server receives the payload and processes it

Subscribing to Events

subscribe.ts
import { events } from '@syncbooks/addon-sdk'

// Subscribe to specific events
await events.subscribe('invoice.created', 'https://myapp.com/webhooks/syncbooks')
await events.subscribe('invoice.paid', 'https://myapp.com/webhooks/syncbooks')
await events.subscribe('payment.received', 'https://myapp.com/webhooks/syncbooks')
await events.subscribe('customer.created', 'https://myapp.com/webhooks/syncbooks')

// List your current subscriptions
const { data: subs } = await events.subscriptions()
console.log(subs)
// [
//   { event: 'invoice.created', webhookUrl: '...', isActive: true },
//   { event: 'invoice.paid', webhookUrl: '...', isActive: true },
// ]

// Unsubscribe from an event
await events.unsubscribe('customer.created')

// Get the full event catalog (all available events)
// This is unauthenticated — useful for documentation
// GET /api/addon/v1/events/catalog

When to subscribe

Typically you subscribe during your addon's first setup (e.g., in a settings page). Subscriptions persist until you unsubscribe or they get disabled due to delivery failures.

Webhook Payload Format

When an event fires, your webhook URL receives a POST with this payload:

Webhook Payload
// POST https://myapp.com/webhooks/syncbooks
// Headers:
//   Content-Type: application/json
//   X-SyncBooks-Signature: sha256=a1b2c3d4e5f6...
//   X-SyncBooks-Event: invoice.paid
//   X-SyncBooks-App: com.mycompany.myapp
//   X-SyncBooks-Delivery-Attempt: 1

{
  "event": "invoice.paid",
  "data": {
    "invoiceId": "507f1f77bcf86cd799439011",
    "invoiceNumber": "INV-0042",
    "customerId": "507f1f77bcf86cd799439012",
    "customerName": "Acme Corporation",
    "totalAmount": 5825,
    "paidAmount": 5825,
    "paymentMethod": "bank_transfer"
  },
  "organizationId": "507f1f77bcf86cd799439013",
  "timestamp": "2026-08-13T10:30:00.000Z",
  "deliveryId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

Handling Webhooks (Server-Side)

You need a server endpoint to receive webhooks. Here's a complete example:

webhook-server.ts
import express from 'express'
import crypto from 'crypto'

const app = express()
app.use(express.json())

// Your addon's app ID (used as signing key)
const APP_ID = 'com.mycompany.myapp'
const SIGNING_KEY = `syncbooks_addon_${APP_ID}`

app.post('/webhooks/syncbooks', (req, res) => {
  // ─── Step 1: Verify Signature ────────────────────────────────
  const signature = req.headers['x-syncbooks-signature'] as string
  const event = req.headers['x-syncbooks-event'] as string
  
  if (!signature) {
    return res.status(401).json({ error: 'Missing signature' })
  }
  
  const expectedSig = 'sha256=' + crypto
    .createHmac('sha256', SIGNING_KEY)
    .update(JSON.stringify(req.body))
    .digest('hex')
  
  if (signature !== expectedSig) {
    console.error('Invalid webhook signature!')
    return res.status(401).json({ error: 'Invalid signature' })
  }

  // ─── Step 2: Process the Event ───────────────────────────────
  const { event: eventName, data, organizationId, timestamp } = req.body
  
  console.log(`[${timestamp}] Received: ${eventName} for org ${organizationId}`)

  switch (eventName) {
    case 'invoice.created':
      handleNewInvoice(data, organizationId)
      break
    case 'invoice.paid':
      handleInvoicePaid(data, organizationId)
      break
    case 'payment.received':
      handlePayment(data, organizationId)
      break
    case 'customer.created':
      handleNewCustomer(data, organizationId)
      break
    default:
      console.log('Unhandled event:', eventName)
  }

  // ─── Step 3: Respond Quickly (within 10 seconds) ─────────────
  res.status(200).json({ received: true })
})

// ─── Event Handlers ──────────────────────────────────────────
function handleInvoicePaid(data: any, orgId: string) {
  console.log(`Invoice ${data.invoiceNumber} paid! Amount: ${data.totalAmount}`)
  // Send thank-you email, update your CRM, sync to external system, etc.
}

function handleNewInvoice(data: any, orgId: string) {
  console.log(`New invoice ${data.invoiceNumber} created`)
}

function handlePayment(data: any, orgId: string) {
  console.log(`Payment received: ${data.amount}`)
}

function handleNewCustomer(data: any, orgId: string) {
  console.log(`New customer: ${data.name}`)
}

app.listen(3001, () => console.log('Webhook server running on :3001'))

Verifying Signatures (SDK Helper)

The SDK includes a helper for signature verification:

import { verifyWebhookSignature } from '@syncbooks/addon-sdk'

app.post('/webhooks/syncbooks', async (req, res) => {
  const signature = (req.headers['x-syncbooks-signature'] as string).replace('sha256=', '')
  const appId = req.headers['x-syncbooks-app'] as string
  const signingKey = `syncbooks_addon_${appId}`
  
  const isValid = await verifyWebhookSignature(
    JSON.stringify(req.body),
    signature,
    signingKey
  )
  
  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' })
  }
  
  // Process event...
  res.status(200).json({ received: true })
})

Always verify signatures

Without signature verification, anyone could send fake events to your webhook URL. Always verify before processing.

Retry Policy

If your webhook returns a non-2xx status code, SyncBooks retries delivery:

AttemptDelayWhen
1ImmediateRight when event fires
2+1 minuteIf attempt 1 failed
3+5 minutesIf attempt 2 failed

After 3 failed attempts, the subscription is automatically disabled. You'll need to re-subscribe by calling events.subscribe() again.

Keep your webhook fast

Respond within 10 seconds. If you need to do heavy processing, acknowledge immediately (return 200) and process asynchronously using a job queue.

Available Events (35 total)

See the full list in the Events Catalog reference.

Most commonly used:

invoice.createdinvoice.paidinvoice.overduecustomer.createdpayment.receivedexpense.approvedproduct.low_stockstore.order.createdticket.created

Next Steps