Skip to main content
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:

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.

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