A webhook is JAI CRM telling another system that something just happened. You give us an address on the internet and choose the events you care about. When one happens, we send the details to that address within seconds. Nothing has to keep asking whether anything is new.
Zapier, Make, n8n, Pabbly and Activepieces all start from a webhook, and so can your own server. A few things people use them for:
- A new lead from IndiaMART or a scanned visiting card goes straight to a WhatsApp message for the salesperson.
- An invoice paid in full updates a Google Sheet the owner reads every evening.
- A won deal opens a project in another tool, with the customer and the amount already filled in.
- A purchase order sent to a supplier lands in the warehouse team’s chat.
Setting one up
- Go to Settings → API keys & webhooks and open the Webhooks tab. You need permission to manage API keys.
- Choose New endpoint and paste the address we should send to. It must start with
https://and be reachable from the internet. - Tick the events you want. At least one.
- Press Add. Your signing secret appears once. Copy it into the system that receives the events: it is how that system knows a delivery really came from us.
- Press the send button on the endpoint to post a test. The answer comes back on the screen straight away: delivered, or why not.
The secret is shown only once. If you lose it, remove the endpoint and add it again; the new one gets a new secret.
The twelve events
| Event | Sent when |
|---|---|
lead.created | A lead is added: typed in, scanned from a card at an exhibition, or arriving from IndiaMART or Just Dial. |
customer.created | A customer is added, by hand or by converting a lead. |
deal.won | A deal is marked won, or dragged into a won stage on the board. |
quotation.sent | A quotation or a proforma is sent to the customer. |
quotation.accepted | A quotation is accepted. |
invoice.created | An invoice, bill of supply or retainer is made: typed in, converted from a quotation, order or proforma, duplicated, raised by a recurring profile or a subscription, billed from a project’s hours, or rung up at the counter. Drafts included; status says which. |
invoice.paid | An invoice is paid in full, however the money came in. |
payment.received | A payment from a customer is recorded or paid online. |
purchaseOrder.sent | A purchase order is sent to a supplier. |
task.completed | A task is marked done: with its Complete button, on its page, from the Tasks list or board, or in a bulk change. Tasks inside a project are not included. |
salesDebitNote.sent | A debit note goes to a customer, adding to an invoice they already have: a rate revised upward, or freight left off. against names the invoice it adds to and reason says why. Never sent as invoice.created. |
salesDebitNote.paid | A customer’s debit note is paid in full, however the money came in. Never sent as invoice.paid. |
What a delivery looks like
POST https://hooks.example.com/jai-crm
Content-Type: application/json
X-BB-Event: invoice.paid
X-BB-Delivery: 6f2c9e1a-4b7d-4c1e-9a55-0d6b2f7e3c10
X-BB-Timestamp: 1760945662
X-BB-Attempt: 1
X-BB-Signature: 9b1f0c…
{
"id": "6f2c9e1a-4b7d-4c1e-9a55-0d6b2f7e3c10",
"event": "invoice.paid",
"at": "2026-10-20T09:14:22.000Z",
"data": {
"id": "a3e0…",
"type": "invoice",
"number": "INV/2026-27/0042",
"status": "paid",
"customer": { "id": "…", "name": "Example Traders", "gstin": "27AAAAA0000A1Z5" },
"total": 1180,
"amountPaid": 1180,
"amountDue": 0,
"lines": [ { "name": "Tiles", "quantity": 1, "rate": 1000, "taxRate": 18, "total": 1180 } ],
"url": "https://app.jaicrm.ai/sales/invoice/a3e0…"
}
}
idis this delivery. It stays the same if we send it again, so use it to ignore repeats.atis when the thing happened. Deliveries can arrive out of order; sort by this.datais the record a few seconds after the event: its id, the details a connector needs, the owner, your own custom fields undercustom, andurl, which opens it in JAI CRM.- Money is a plain number in the record’s currency, to the paisa. Dates are ISO 8601 in UTC.
Each kind of record has its own shape. Leads carry the name, company, phone numbers, email, source and stage. Customers add GSTIN, place of supply and addresses. Deals carry the amount, pipeline, stage and customer. Quotations, invoices and purchase orders carry the number, the customer or supplier, every line and the totals. Payments carry the amount, mode and which invoices they paid. Tasks carry the title, who did it and when. The full list of fields is in the API reference. Fields may be added over time; none will be renamed or removed.
Never in a delivery: passwords, keys, share-link tokens, PAN, drawn signatures or internal notes.
Records brought in from a spreadsheet import do not raise events, so importing four hundred leads does not send four hundred deliveries. A cheque recorded before it clears sends payment.received when you mark it cleared, not before.
Checking the signature
Anyone who finds your address could post to it. The signature proves a delivery came from us. Work out the HMAC-SHA256 of the raw body, exactly as it arrived, using your signing secret as the key, and compare it with X-BB-Signature. Throw the delivery away if they differ. In Node.js:
const crypto = require('crypto');
function fromJaiCrm(rawBody, signature, secret) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(String(signature || ''));
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Check the raw text before you parse it. Parsing and printing the JSON again changes the spacing, and the signature will not match.
Answering, and what happens when you cannot
- Answer with any
2xxwithin 10 seconds, then do the slow work afterwards. - If your server answers
5xx,408or429, takes longer than 10 seconds, or cannot be reached, we try again after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. Every try carries the same id, body and signature. - Any other answer is final. A redirect is not followed: give us the address it points to instead.
- After 20 deliveries in a row fail for good, the endpoint is switched off and the Webhooks tab says so. Fix the receiver, then press On to start again.
The delivery log
The list icon on an endpoint shows its last ten deliveries: the event, the time, which try it was, and what your server answered, as a status code and a short reason such as 200 OK or No answer within 10 seconds. We never keep the body of your server’s answer. The log is kept for 30 days.
Addresses we will not send to
We only post to https addresses that point to public servers on the internet. Addresses inside a private network, such as localhost, 10.x.x.x, 192.168.x.x or a cloud server’s metadata address, are refused when you save the endpoint, and checked again before every delivery in case the name has been pointed somewhere else since. To test on your own computer, use a tunnelling service that gives you a public https address.
Doing it from code
An integration can manage its own endpoints with an API key that may manage API keys: POST /api/settings/webhooks with { "url": "https://…", "events": ["lead.created"] } returns the endpoint with its secret, PATCH /api/settings/webhooks/{id} changes the address or events or switches it on, POST /api/settings/webhooks/{id}/test sends a test, and DELETE /api/settings/webhooks/{id}?confirm=true removes it.
Something missing or wrong here? Tell us. A question that needed asking usually means a page that needed writing.