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
secretfield 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:
payloadis the raw JSON request body stringwebhook_secretis the plaintext secret returned at webhook creation (e.g.,whsec_abc123...)- The HMAC key is the SHA-256 hash of the plaintext secret, not the plaintext itself
Verification Steps
- Read the raw request body as a string (do not parse then re-serialize).
- Compute
SHA-256(your_webhook_secret)to get the signing key. - Compute
HMAC-SHA256(raw_body, signing_key)to get the expected signature. - Compare the expected signature against the value in
X-InCRM-Signature(after stripping thesha256=prefix). - 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
- Respond quickly — return
200within 10 seconds to avoid retries. Process the event asynchronously if needed. - Use idempotency — use the
X-InCRM-Delivery-Idheader to deduplicate deliveries, especially during retries. - Verify signatures — always validate the
X-InCRM-Signatureheader before processing events. - Store the secret securely — the webhook secret is shown only once. Treat it like a password.
- Monitor delivery logs — periodically check
/deliveriesfor failed deliveries to catch integration issues early. - Use HTTPS — always use HTTPS URLs for webhook endpoints to protect payload data in transit.