Webhooks

Webhooks allow your application to receive real-time HTTP notifications when events occur in InCRM. Instead of polling the API, you register a URL and InCRM pushes event data to it.

Setup

Create a Webhook

curl -X POST https://api.incrm.app/api/v1/webhook \
  -H "X-Api-Key: incrm_your-key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/incrm",
    "events": ["client.created", "invoice.created", "invoice.updated"],
    "description": "Main integration webhook"
  }'

Response:

{
  "success": true,
  "data": {
    "id": "6507a1b2c3d4e5f6a7b8c9d0",
    "url": "https://your-app.com/webhooks/incrm",
    "events": ["client.created", "invoice.created", "invoice.updated"],
    "description": "Main integration webhook",
    "secret": "whsec_dGhpcyBpcyBhIHRlc3Qgc2VjcmV0...",
    "secretPrefix": "whsec_dGhpcyB",
    "isActive": true,
    "createdAt": "2025-01-15T10:00:00.000Z"
  }
}

Important: The secret field is only returned once at creation time. Store it securely — you will need it to verify webhook signatures.

Update a Webhook

curl -X PUT "https://api.incrm.app/api/v1/webhook/6507a1b2c3d4e5f6a7b8c9d0" \
  -H "X-Api-Key: incrm_your-key" \
  -H "Content-Type: application/json" \
  -d '{
    "events": ["client.created", "client.updated", "invoice.created"],
    "isActive": true
  }'

Delete (Deactivate) a Webhook

curl -X DELETE "https://api.incrm.app/api/v1/webhook/6507a1b2c3d4e5f6a7b8c9d0" \
  -H "X-Api-Key: incrm_your-key"

This sets isActive: false rather than permanently deleting the webhook.

Event Types

Events follow the pattern {entity}.{action}. The entity name uses camelCase with a lowercase first character.

Available Events

Event Trigger
client.created New client created
client.updated Client record updated
client.deleted Client deleted
invoice.created New invoice created
invoice.updated Invoice updated
invoice.deleted Invoice deleted
quote.created New quote created
quote.updated Quote updated
quote.deleted Quote deleted
product.created New product created
product.updated Product updated
product.deleted Product deleted
service.created New service created
service.updated Service updated
service.deleted Service deleted
employee.created New employee added
employee.updated Employee updated
employee.deleted Employee removed

Any entity managed through the generic CRUD system emits these events automatically.

Payload Format

When an event occurs, InCRM sends a POST request to your webhook URL with the following structure:

Headers:

Header Description
Content-Type application/json
X-InCRM-Signature sha256=<hex-signature>
X-InCRM-Event Event name (e.g., client.created)
X-InCRM-Delivery-Id Unique delivery ID for idempotency

Body:

{
  "event": "client.created",
  "data": {
    "id": "6507a1b2c3d4e5f6a7b8c9d0",
    "firstName": "John",
    "lastName": "Doe",
    "email": "john@example.com",
    "businessId": "6507a1b2c3d4e5f6a7b8c9d1",
    "createdAt": "2025-01-15T10:30:00.000Z"
  },
  "businessId": "6507a1b2c3d4e5f6a7b8c9d1",
  "timestamp": "2025-01-15T10:30:00.123Z"
}

Signature Verification

Every webhook delivery includes a cryptographic signature so you can verify that the payload is authentic and has not been tampered with.

Algorithm

The signature is computed as:

HMAC-SHA256(JSON.stringify(payload), SHA256(webhook_secret))

Where:

Verification Steps

  1. Read the raw request body as a string (do not parse then re-serialize).
  2. Compute SHA-256(your_webhook_secret) to get the signing key.
  3. Compute HMAC-SHA256(raw_body, signing_key) to get the expected signature.
  4. Compare the expected signature against the value in X-InCRM-Signature (after stripping the sha256= prefix).
  5. Use constant-time comparison to prevent timing attacks.

Node.js Example

const crypto = require('crypto')

function verifyWebhookSignature(rawBody, secret, signatureHeader) {
  const signingKey = crypto.createHash('sha256').update(secret).digest('hex')

  const expectedSignature = crypto
    .createHmac('sha256', signingKey)
    .update(rawBody)
    .digest('hex')

  const receivedSignature = signatureHeader.replace('sha256=', '')

  return crypto.timingSafeEqual(
    Buffer.from(expectedSignature, 'hex'),
    Buffer.from(receivedSignature, 'hex'),
  )
}

// Express.js middleware example
app.post('/webhooks/incrm', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-incrm-signature']
  const isValid = verifyWebhookSignature(req.body.toString(), WEBHOOK_SECRET, signature)

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' })
  }

  const event = JSON.parse(req.body.toString())
  console.log(`Received ${event.event}:`, event.data)

  res.status(200).json({ received: true })
})

TypeScript Example

import { createHash, createHmac, timingSafeEqual } from 'crypto'

function verifyWebhookSignature(
  rawBody: string,
  secret: string,
  signatureHeader: string,
): boolean {
  const signingKey = createHash('sha256').update(secret).digest('hex')
  const expectedSignature = createHmac('sha256', signingKey)
    .update(rawBody)
    .digest('hex')
  const receivedSignature = signatureHeader.replace('sha256=', '')

  return timingSafeEqual(
    Buffer.from(expectedSignature, 'hex'),
    Buffer.from(receivedSignature, 'hex'),
  )
}

Retry Behavior

If your endpoint does not return a 2xx response within 10 seconds, the delivery is marked for retry.

Retry Schedule

Attempt Delay after failure
1st retry ~60 seconds
2nd retry ~5 minutes (300 seconds)
3rd retry Permanently failed

Retries are processed every 30 seconds. Up to 10 pending retries are processed per cycle.

Delivery Statuses

Status Description
PENDING Created but not yet delivered (e.g., test deliveries)
SUCCESS Delivered successfully (2xx response)
RETRYING Failed, scheduled for retry
FAILED All retry attempts exhausted

Delivery Logs

View delivery history for a webhook:

curl "https://api.incrm.app/api/v1/webhook/6507a1b2c3d4e5f6a7b8c9d0/deliveries?page=1&perPage=20" \
  -H "X-Api-Key: incrm_your-key"

Response:

{
  "success": true,
  "data": [
    {
      "id": "6507a1b2c3d4e5f6a7b8c9e1",
      "event": "client.created",
      "status": "SUCCESS",
      "responseStatus": 200,
      "responseBody": "{\"received\":true}",
      "attempt": 1,
      "nextRetryAt": null,
      "createdAt": "2025-01-15T10:30:01.000Z"
    }
  ],
  "meta": {
    "total": 42
  }
}

Testing Webhooks

Trigger a test delivery to verify your webhook endpoint:

curl -X POST "https://api.incrm.app/api/v1/webhook/6507a1b2c3d4e5f6a7b8c9d0/test" \
  -H "X-Api-Key: incrm_your-key"

This creates a delivery record with status PENDING for testing purposes. It does not make an actual HTTP call to your URL — use it to verify the webhook setup and delivery log integration.

Best Practices

  1. Respond quickly — return 200 within 10 seconds to avoid retries. Process the event asynchronously if needed.
  2. Use idempotency — use the X-InCRM-Delivery-Id header to deduplicate deliveries, especially during retries.
  3. Verify signatures — always validate the X-InCRM-Signature header before processing events.
  4. Store the secret securely — the webhook secret is shown only once. Treat it like a password.
  5. Monitor delivery logs — periodically check /deliveries for failed deliveries to catch integration issues early.
  6. Use HTTPS — always use HTTPS URLs for webhook endpoints to protect payload data in transit.