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
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, 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:
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.
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
- 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.