Webhooks
Security
Verifying the webhook secret key with a constant-time compare, the HTTPS requirement, and an honest look at the threat model.
Last updated Jul 14, 2026
Anyone who discovers your webhook URL can POST to it. Verification is how you tell a genuine OnePageCRM event from a forged one.
The threat model, honestly
OnePageCRM webhooks do not carry a signature header. There is no
HMAC of the body and no timestamp signature. The shared secret you
configure is sent in the body as the secretkey field, on every
delivery.
That means:
- The
secretkeyfield is the only authenticity signal. If it matches your stored secret, the request knew the secret; if not, reject it. - The secret travels with every payload, so transport security does the heavy lifting. Always use an HTTPS URL.
- A matching secret proves knowledge of the secret, not integrity of the payload. For anything high-stakes, treat the event as a trigger and re-fetch the record from the API before acting on it — and rotate the secret on any suspicion of exposure.
Verify with a constant-time compare
Compare the received secretkey against your stored secret using a
constant-time comparison, not ==. A plain equality check returns
early on the first differing byte, which leaks timing information an
attacker can use to recover the secret byte by byte.
Node
import crypto from "node:crypto";
const SECRET = process.env.OPCRM_WEBHOOK_SECRET;
function verifySecret(received) {
const a = Buffer.from(received ?? "", "utf8");
const b = Buffer.from(SECRET, "utf8");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
timingSafeEqual throws on unequal lengths, so check the length first.
Python
import hmac
import os
SECRET = os.environ["OPCRM_WEBHOOK_SECRET"]
def verify_secret(received: str | None) -> bool:
return hmac.compare_digest(received or "", SECRET)
Checklist
- Webhook URL uses HTTPS
- A secret key is configured on the webhook (in the Apps page)
-
secretkeyverified withcrypto.timingSafeEqual/hmac.compare_digest, never== - Requests with a missing or wrong secret are rejected before any processing
- The secret lives in your environment or secret store, not in source control
- The URL path is unguessable (e.g. includes a random segment) — cheap defense in depth
- High-stakes actions re-fetch the record from the API instead of trusting the payload alone
Rotate the secret from the Webhooks app — see Manage webhooks. Update your receiver first so it accepts both the old and new secret, then change the config.
Common questions
Does OnePageCRM sign webhook payloads?
No. There is no HMAC signature header and no timestamp signature. The
shared secret travels in the body as the secretkey field on every
delivery — matching it against your stored secret is the only
authenticity check.
How should I compare the secret?
With a constant-time comparison — crypto.timingSafeEqual in Node,
hmac.compare_digest in Python — never ==. A plain equality check
returns early on the first differing byte, which leaks timing
information an attacker can use to recover the secret.
How do I rotate a webhook secret? Update your receiver first so it accepts both the old and new secret, then change the config in the Webhooks app — see Manage webhooks.