Webhooks

Webhooks allow your application to receive real-time HTTP notifications when events occur in Brane. Instead of polling the API, you register a URL and Brane 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, Brane 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

incrm is the platform's technical name, so the headers start with X-InCRM-, as the API's address (api.incrm.app) and the API keys (incrm_) carry it too.

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. A delivery gets at most 3 attempts: the first send and two retries.

Retry Schedule

Attempt When
1st attempt Right after the event
2nd attempt (1st retry) ~60 seconds after the 1st attempt failed
3rd attempt (2nd retry) ~5 minutes (300 seconds) after the 2nd attempt failed

If the 3rd attempt fails too, the delivery is permanently FAILED.

Retries are processed every 30 seconds, so a retry can arrive up to 30 seconds after its delay. Up to 10 due retries are processed per cycle.

Each retry is sent once, even when Brane runs on several servers: a server takes the delivery before sending it, and no other server sends it meanwhile. If that server stops in the middle of a send, the delivery is sent again about a minute later, and the interrupted send counts as one of the 3 attempts.

Delivery is still at least once: when your endpoint processed a request but Brane did not get a 2xx response in time, the same delivery is sent again. Deduplicate on X-InCRM-Delivery-Id.

Delivery Statuses

Status Description
PENDING The first attempt is being sent
SUCCESS Delivered successfully (2xx response)
RETRYING An attempt failed; the next one is scheduled for nextRetryAt or being sent
FAILED All 3 attempts failed, or the webhook was deactivated before a retry

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 sends a signed webhook.test event to your URL right away and returns its delivery record (status PENDING). Check the outcome in the delivery log. A failed test delivery is retried like any other.

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.