# Lead webhooks — send leads in and get events out

> Post leads to a /hooks/lead address with no key, turn missed calls into leads, and receive signed lead.created and lead.status_changed webhooks.

Source: https://pathshala.mjxtechnologies.com/docs/developers/lead-webhook/

MJX CRM has two kinds of webhooks. **Lead sources** are addresses you post leads *to*. **Outgoing webhooks** are calls we make *to your server* when something happens to a lead. This page covers both.

## Part 1 — Lead sources: post leads in

A lead source is a private address made on the Integrations page. The token in the address is the credential, so there is no key and no login. Each source counts its hits and the leads it made.

> [!PATH] Apps › MJX CRM › CRM settings › Where enquiries come from

![Integrations screen, Where leads come from box with the Add source button marked](media/developers/lead-webhook-1.jpg "Apps › MJX CRM › CRM settings › Where enquiries come from")

(For a business the menu says **Where leads come from**.)

### How to make a lead source

1. Sign in as the **Owner** or an **Admin**.
2. In **Where leads come from — your sources**, pick the kind in the list.
3. Type a name, for example "Website".
4. Press [[Add source]].
5. Press [[Copy]] next to the address that appears.

| Kind (as listed) | Address | Plan |
|---|---|---|
| Any form or tool — your website (JSON / form fields) | `/hooks/lead/<token>` | Every plan |
| IndiaMART (Push API) | `/hooks/lead/<token>` | Pro |
| JustDial (lead URL) | `/hooks/lead/<token>` | Pro |
| Meta lead ads (Facebook / Instagram) | `/hooks/lead/<token>` | Pro |
| LinkedIn lead gen forms (Make / Zapier) | `/hooks/lead/<token>` | Pro |
| Google Ads lead forms | `/hooks/lead/<token>` | Pro |
| Missed call → lead (a Plivo number) | `/hooks/call/<token>` | Pro |

An institute can have up to 20 lead sources. The full address is shown on the source row; it looks like `https://pathshala.mjxtechnologies.com/hooks/lead/9f8e…` (the token is 40 letters and digits).

### Posting to a generic source ("Any form or tool")

Send `POST` (or `GET` with query fields) to the address as:

- JSON (`Content-Type: application/json`),
- form fields (`application/x-www-form-urlencoded`), or
- `multipart/form-data` (FormData). Any file in it is dropped.

A browser on any website may call it — CORS is open for `/hooks/lead/` and `/hooks/call/`, with no cookies. Limits: 256 KB body; for FormData, 80 fields of up to 20 KB each.

```js
fetch("https://pathshala.mjxtechnologies.com/hooks/lead/<token>", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Meena Iyer", phone: "98XXXXXX32", email: "meena@example.com",
                         message: "Admission for Class 6", source: "website" })
});
```

```bash
curl -X POST "https://pathshala.mjxtechnologies.com/hooks/lead/<token>" \
  -d "full_name=Meena Iyer" -d "mobile=98XXXXXX32" -d "requirement=Admission for Class 6"
```

**Field names do not need to match ours.** We read `name` / `full_name` / `your_name`, `phone` / `mobile` / `whatsapp`, `email`, `message` / `requirement` / `comments`, `company`, `service`, `budget`, `city`, `class` / `course` / `grade`, booking `reference`, `type`, `from`, `to`, `tags`, `source`, `page_url` and many other spellings. Nested `data`, `fields`, `lead` or `answers` objects are opened up. Anything we do not recognise is kept on the lead as **Additional details** (up to 30 fields). A field named `cf_<name>` fills that custom field.

A phone number **or** an email is required.

### Answers from a lead source

| Status | Body | When |
|---|---|---|
| 201 | `{"success":true,"data":{"id":"…","duplicate":false}}` | A new lead was made |
| 200 | `{"success":true,"data":{"id":"…","duplicate":true}}` | The number (or email) is known — a note "Enquired again via …" was added to that lead |
| 400 | `A phone number or an email is required.` | Neither was sent |
| 402 | `IndiaMART, JustDial, Facebook & Instagram, LinkedIn, Google Ads and missed-call sources come with MJX CRM Pro. …` | A Pro kind on a plan without Pro |
| 403 | `The key does not match this lead source. …` | Google Ads only: wrong `google_key` |
| 404 | `No such lead source.` | Wrong token, or the source was removed |
| 410 | `This lead source is switched off.` | The owner pressed [[Switch off]] |
| 422 | A sentence | Anything else that went wrong |

Errors use the `{"success":false,"error":{"message":"…"}}` envelope. The last problem is also shown on the source row as "Last problem: …".

### What the special kinds read

| Kind | What it expects |
|---|---|
| IndiaMART | IndiaMART's Push API body (a `RESPONSE` object with `SENDER_NAME`, `SENDER_MOBILE`, `SENDER_EMAIL`, `SUBJECT`, `QUERY_MESSAGE`, `QUERY_PRODUCT_NAME`, `SENDER_CITY`, `UNIQUE_QUERY_ID`, `SENDER_COMPANY`). Source `indiamart`. |
| JustDial | JustDial's lead URL fields: `name`, `mobile`, `email`, `city`, `category`, `area`, `leadid`. Source `justdial`. |
| Meta lead ads (set up by hand) | `GET` with `hub.mode=subscribe`, `hub.verify_token`, `hub.challenge` (we answer the challenge when the verify token matches the one on the source row), then Meta's `leadgen` `POST`. We fetch each lead from Meta with the **page access token** you saved on the source. Answers `EVENT_RECEIVED`. Source `meta_ads`. |
| LinkedIn | Flat keys from Make / Zapier (`firstName`, `lastName`, `emailAddress`, `phoneNumber`, `companyName`, `jobTitle`), an `answers` list of question/answer pairs, or a Lead Sync `leadGenFormResponse`. Source `linkedin`. |
| Google Ads | Google's lead form webhook body (`lead_id`, `form_id`, `campaign_id`, `google_key`, `is_test`, `gcl_id`, `user_column_data`). `google_key` must equal the **Key for Google Ads** shown on the source. Source `google_ads`. |

Set-up steps for each are in [Lead connectors setup](developers/lead-connectors-setup).

### Missed call → lead (`/hooks/call/<token>`)

Use this as the **Answer URL** of a Plivo application on your Plivo number. When someone calls:

1. We answer with Plivo XML: the caller hears your greeting (in Indian English) and the call ends.
2. The caller's number becomes a lead (source `missed_call`), or a note on their existing lead.
3. A high-priority task "Call back … — they called and the line was not answered" is made for the owner (or an admin), due in 30 minutes.
4. The call is logged as an inbound, unanswered call.

The default greeting is "Thank you for calling <your name>. We will call you back shortly." You can change it when adding the source (up to 300 characters).

---

## Part 2 — Outgoing webhooks: we call your server

When a lead is created or its status changes, we `POST` to your `https://` address. Needs **MJX CRM Pro**.

> [!PATH] Apps › MJX CRM › CRM settings › Advanced: API & webhooks › Webhooks

![Integrations screen, Webhooks box with the Add webhook button marked](media/developers/lead-webhook-2.jpg "Apps › MJX CRM › CRM settings › Advanced: API & webhooks › Webhooks")

### How to add a webhook

1. Type your address in the box (it must start with `https://`).
2. Tick the events: **A new lead arrives** (`lead.created`) and/or **A lead's status changes** (`lead.status_changed`).
3. Press [[Add webhook]].
4. Copy the **signing secret** (`whsec_…`) — "copy it now; it will not be shown again." Press [[I have copied it]].
5. Press [[Send a test]] to send a `ping`.

Each webhook row has [[Send a test]], [[Recent calls]] (the last 20 deliveries), [[Turn on again]] (when it is off) and [[Delete]]. An institute can have up to 10 webhooks.

### Address rules

- Must be `https://` on port 443, with no user name or password in it.
- Must be on the public internet: `localhost`, `.local`, `.internal`, and private, loopback, link-local or cloud-metadata addresses are refused, both when you add it and every time we send.
- Up to 500 characters.

### What we send

```
POST https://your-erp.example.com/mjx
Content-Type: application/json
User-Agent: MJX-CRM-Webhooks/1
X-MJX-Event: lead.created
X-MJX-Delivery: 7f1c2a9e-3b4d-4c5e-8f6a-1b2c3d4e5f60
X-MJX-Timestamp: 1793162400
X-MJX-Signature: sha256=5d2c…
```

```json
{
  "event": "lead.created",
  "delivery": "7f1c2a9e-3b4d-4c5e-8f6a-1b2c3d4e5f60",
  "created_at": "2026-10-28T05:30:00.000Z",
  "data": {
    "lead": {
      "id": "3f2b9c1e-6a4d-4f8e-9b7a-2c1d0e5f6a7b",
      "name": "Sunita Sharma",
      "phone": "+9198XXXXXX10",
      "status": "new",
      "tags": [],
      "…": "the full lead object"
    }
  }
}
```

| Event | When | Extra in `data` |
|---|---|---|
| `lead.created` | Any new lead — the public form, a lead source, a connector, the API, the MCP server, or one typed in by staff | — |
| `lead.status_changed` | The status really changes (not when it is saved unchanged) | `previous_status` |
| `ping` | You pressed [[Send a test]] | `message` instead of `lead` |

The `lead` is the same object as in the [API reference](developers/crm-api-reference).

### Check the signature

The signature is `sha256=` + the hex HMAC-SHA256 of `<X-MJX-Timestamp>.<raw body>` using your signing secret. Use the **raw** body exactly as received.

```js
// Node.js (Express): keep the raw body, e.g. express.json({ verify: (req, res, buf) => { req.rawBody = buf; } })
const crypto = require('crypto');
const expected = 'sha256=' + crypto.createHmac('sha256', process.env.MJX_WEBHOOK_SECRET)
  .update(req.headers['x-mjx-timestamp'] + '.' + req.rawBody).digest('hex');
const got = req.headers['x-mjx-signature'] || '';
const genuine = got.length === expected.length &&
  crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got));
```

```python
import hmac, hashlib
expected = "sha256=" + hmac.new(secret.encode(), (timestamp + "." + raw_body).encode(), hashlib.sha256).hexdigest()
genuine = hmac.compare_digest(expected, signature_header)
```

> [!TIP]
> Also reject calls whose `X-MJX-Timestamp` is far from your clock (for example more than 5 minutes), and use `X-MJX-Delivery` to ignore a delivery you have already handled.

### Retries and switching off

| Rule | Value |
|---|---|
| Success | Any `2xx` answer within 5 seconds |
| Retry | One retry, a minute later, with the same `X-MJX-Delivery` |
| Switch-off | After 20 failures in a row the webhook turns itself off ("Off after 20 failures in a row") |
| Turn back on | [[Turn on again]] — this also resets the failure count |
| History kept | The last 50 deliveries per webhook; [[Recent calls]] shows 20 |

Calls are sent in the background, so a slow server never slows down the CRM.

## Zapier and Make

- **Leads in:** use their "Webhooks / HTTP request" step and `POST` to `/api/v1/crm/leads` with your API key — or, with no key, to a generic lead source address.
- **Leads out:** make a "Catch hook" there and paste its `https://` address as a webhook here.

## Questions people ask

### Do I need an API key to post a website form to the CRM?

No. A lead source address carries its own secret token. Post the form to it from the browser or your server, with no key.

### What happens if the same person fills the form twice?

No second lead is made. The existing lead gets a note "Enquired again via …", and the answer says duplicate true.

### How do I check that a webhook call really came from MJX?

Compute HMAC-SHA256 of the timestamp, a dot and the raw body with your signing secret, and compare it with the X-MJX-Signature header.

### Why did my webhook turn itself off?

After 20 failed calls in a row it switches off. Fix your endpoint, then press "Turn on again" on the Integrations page.
