> ## Documentation Index
> Fetch the complete documentation index at: https://webhooks.docs.crm.africa/llms.txt
> Use this file to discover all available pages before exploring further.

# How to Verify Incoming Webhook Requests from Grow CRM

> Learn how to verify every incoming Grow CRM webhook request using shared-secret headers before trusting or processing any payload.

Every HTTP POST that Grow CRM sends to your endpoint includes a set of identifying headers alongside the JSON body. Checking these headers before you process the payload protects your server from requests sent by third parties pretending to be Grow CRM. Make verification the very first thing your handler does — reject the request immediately if it fails, and never perform side-effects on an unverified payload.

## Headers sent with every delivery

Grow CRM attaches the following three headers to every webhook request:

| Header               | Description                                                                                            |
| -------------------- | ------------------------------------------------------------------------------------------------------ |
| `X-Webhook-Secret`   | The endpoint's signing secret, as shown on the endpoint's detail page in the CRM.                      |
| `X-Webhook-Event`    | The event key for this delivery, matching the `event` field in the JSON body (e.g. `invoice.created`). |
| `X-Webhook-Delivery` | A unique identifier for this specific delivery attempt.                                                |

## v1: shared-secret verification

The current version uses a **shared-secret** model. Your receiving endpoint must read the `X-Webhook-Secret` header and compare it against the secret shown on the endpoint's detail page in Grow CRM. Reject the request with a `401` status if the values do not match.

<CodeGroup>
  ```php title="verify.php" theme={null}
  <?php
  $expected_secret = 'the secret shown on the endpoint detail page';
  $received_secret = $_SERVER['HTTP_X_WEBHOOK_SECRET'] ?? '';

  if (!hash_equals($expected_secret, $received_secret)) {
      http_response_code(401);
      exit;
  }

  $payload = json_decode(file_get_contents('php://input'), true);
  // ... handle $payload['event'] / $payload['data']
  ```

  ```javascript title="webhook.js" theme={null}
  // Node / Express
  app.post('/webhooks/growcrm', (req, res) => {
    const expected = process.env.GROWCRM_WEBHOOK_SECRET;
    const received = req.header('X-Webhook-Secret');

    if (!received || received !== expected) {
      return res.sendStatus(401);
    }

    const { event, id, data } = req.body;
    // ... handle the event
    res.sendStatus(200);
  });
  ```
</CodeGroup>

### Use constant-time comparison

Use a **constant-time comparison** function — `hash_equals` in PHP, or an equivalent in your language's standard library — rather than `==` or `===`. Standard equality operators short-circuit as soon as they find a differing character, which leaks timing information an attacker can exploit to guess the secret one character at a time.

## Keep the secret private

Treat your endpoint's signing secret like a password. Anyone who possesses it can send requests to your endpoint that your code will accept as genuine. Follow these rules:

* Never log request headers in production — your secret will appear in your logs.
* Never transmit webhooks over plain HTTP — always use `https://` so the secret is encrypted in transit.
* If a secret is compromised, delete the endpoint in Grow CRM and recreate it. A new secret is generated automatically for every new endpoint.

## Recommended future hardening: HMAC-SHA256

<Note>
  A shared secret sent on every request has two limitations: it travels over the wire on every call (so it must never be logged or sent over plain HTTP), and it does not protect the **payload itself** — nothing stops a captured request from being replayed later.

  A future version of this module is expected to add HMAC-SHA256 request signing, the same approach used by Stripe and GitHub:

  * The endpoint's secret is used as an HMAC key and is **never sent over the wire**.
  * The signature is computed over `timestamp + "." + raw_body` and sent in a header such as `X-Webhook-Signature: t=1730000000,v1=<hex-hmac>`.
  * Your code recomputes the HMAC locally, compares it to the header value, and rejects requests whose timestamp is too old — protecting against replay attacks.

  If you are building a new integration today, isolate your verification logic in a single function. Swapping a shared-secret check for an HMAC check later will then be a small, contained change.
</Note>
