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 secretkey field 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)
  • secretkey verified with crypto.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.