Webhook Signature Verification
On this page
Webhook signing is optional by default
Webhook workflows accept unsigned inbound posts by default. A new or
existing webhook workflow with no explicit setting uses
signature_required: false, so tools like Zapier, HighLevel,
and custom forms can post to the workflow URL without adding a signature.
When to require signatures
Turn on signing when the sender can compute an HMAC signature for every
request. In the workflow builder, open the Webhook trigger settings and
enable the signature requirement. Through the MCP tools, update the
workflow with trigger_config.signature_required: true using
workflow_update. Set it back to false to accept
unsigned posts again.
Where to find the signing secret
Each webhook workflow has its own signing_secret. The secret
is shown next to the Webhook URL in the workflow builder's Webhook trigger
settings after the workflow has been created. Keep it private and only
share it with the system that sends requests to that workflow.
Signature format
Send the signature in the x-uphex-signature header. The value
must be the lowercase hex HMAC-SHA256 digest of the raw request body, using
the workflow's signing_secret as the key. The header may be
sent as either the plain hex digest or with a sha256= prefix.
x-uphex-signature: sha256=<lowercase_hex_hmac_sha256>
Sign the exact raw body bytes you send over HTTP. Do not reformat JSON, add whitespace, change key order, or sign a parsed object after computing the digest.
Copy-paste examples
With OpenSSL:
BODY='{"email":"jane@example.com","first_name":"Jane"}'
SECRET='replace-with-workflow-signing-secret'
SIGNATURE=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -binary | xxd -p -c 256)
curl -X POST "$WEBHOOK_URL" -H "Content-Type: application/json" -H "x-uphex-signature: sha256=$SIGNATURE" --data "$BODY"
With Node.js:
import crypto from 'node:crypto';
const body = JSON.stringify({ email: 'jane@example.com', first_name: 'Jane' });
const secret = process.env.UPHEX_WEBHOOK_SIGNING_SECRET;
const signature = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
await fetch(process.env.UPHEX_WEBHOOK_URL, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-uphex-signature': `sha256=${signature}`,
},
body,
});
Troubleshooting
If a signed request is rejected, confirm the workflow has
trigger_config.signature_required: true, the sender is using
the matching workflow signing_secret, and the digest was
computed from the raw request body. If the sender cannot sign requests,
leave signature_required set to false.