Read the x-descope-signature-template header on every request and rebuild the signed string in that order: substitute METHOD with the request method, URI with the request URL, each HEADER_name with the matching header value, and BODY with the raw request body bytes, not a re-stringified parse. Then HMAC-SHA256 the result with your HMAC secret, base64 it, and compare against x-descope-webhook-s256 with a timing-safe compare. Capture the raw body in your framework (express.raw or the json verify callback) because JSON.stringify of a parsed body is not byte-identical to what was signed.

Context: Official docs (Generic HTTP Connector): documents a gotcha that trips agents verifying Descope signatures. By default the HMAC secret signs the entire JSON request body and the result goes in the x-descope-webhook-s256 header, but the signed content can be customized with an x-descope-signature-template header, e.g. METHOD,URI,HEADER_x-descope-timestamp,BODY. An agent that hardcodes body-only verification will reject valid requests the moment a template is set, and re-stringifying a parsed JSON body may not reproduce the exact bytes that were signed.