HMAC Verification
How to sign custom integration webhook requests so VYG accepts them.
Sign webhook requests with HMAC-SHA256 using your webhook secret and the exact body bytes
you send. An invalid signature returns 401 Unauthorized; correct it before retrying.
HMAC SHA-256 is the default and recommended way to authenticate your webhooks. In the app it is the Signing secret security check. If your platform can't compute an HMAC signature, see Validation Methods for the alternatives (secret header or allowed IP addresses) and how to choose.
The algorithm
- Take the exact bytes of the JSON body you are about to send. Do not pretty-print, re-stringify, or normalize whitespace after signing.
- Compute
HMAC-SHA256(secret, body). - Hex-encode the result, lowercase.
- Send it in the
x-vyg-signatureheader.
Sign the bytes you send
Serialize the body once, sign it, and send the same value. Changing whitespace or field order after signing invalidates the signature. Make sure your HTTP client sends the body unchanged.
Worked examples
All three examples below produce the same signature for the same body and secret. You can paste them locally to verify.
Fixture
secret = whsec_demo_secret
body = {"type":"checkout_abandoned","event_id":"evt_demo_1","data":{"id":"ck_001"}}
expected x-vyg-signature:
d9eea748ce5eff60b7f4edea5da4488ef750296f9ca6c145ea95b0337e580c6bcURL + OpenSSL
SECRET='whsec_demo_secret'
BODY='{"type":"checkout_abandoned","event_id":"evt_demo_1","data":{"id":"ck_001"}}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
echo "$SIG"
# d9eea748ce5eff60b7f4edea5da4488ef750296f9ca6c145ea95b0337e580c6b
curl -sS -X POST "https://<your-vyg-webhook-host>/custom/<webhookId>" \
-H 'content-type: application/json' \
-H 'x-vyg-topic: checkout_abandoned' \
-H 'x-vyg-event-id: evt_demo_1' \
-H "x-vyg-signature: $SIG" \
--data "$BODY"Note the printf '%s' (not echo) — echo adds a trailing newline that
would invalidate the signature.
Node.js
import { createHmac } from 'node:crypto';
const secret = 'whsec_demo_secret';
const rawBody = '{"type":"checkout_abandoned","event_id":"evt_demo_1","data":{"id":"ck_001"}}';
const signature = createHmac('sha256', secret).update(rawBody).digest('hex');
// d9eea748ce5eff60b7f4edea5da4488ef750296f9ca6c145ea95b0337e580c6b
await fetch('https://<your-vyg-webhook-host>/custom/<webhookId>', {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-vyg-topic': 'checkout_abandoned',
'x-vyg-event-id': 'evt_demo_1',
'x-vyg-signature': signature,
},
body: rawBody,
});The string passed to .update() and the string passed as body must be
the same value. If you build the payload as an object, stringify it
once into a const rawBody, sign that, and send that.
Python
import hmac
import hashlib
import urllib.request
secret = b'whsec_demo_secret'
raw_body = b'{"type":"checkout_abandoned","event_id":"evt_demo_1","data":{"id":"ck_001"}}'
signature = hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
# d9eea748ce5eff60b7f4edea5da4488ef750296f9ca6c145ea95b0337e580c6b
req = urllib.request.Request(
'https://<your-vyg-webhook-host>/custom/<webhookId>',
data=raw_body,
method='POST',
headers={
'content-type': 'application/json',
'x-vyg-topic': 'checkout_abandoned',
'x-vyg-event-id': 'evt_demo_1',
'x-vyg-signature': signature,
},
)
urllib.request.urlopen(req)If you are using requests, build the body with json.dumps(...) into a
string variable, sign that string, and pass it as data= (not json=,
which would re-serialize and may change the bytes).
Rotating your secret
Open your data source in Settings → Data sources and click Replace signing secret. It is in Connection details, on the Settings tab and in the ⋯ menu. The previous secret stays valid for 24 hours while you update your sender.
After 24 hours, requests signed with the previous secret return 401. Rotating again
replaces the previous secret and starts a new 24-hour window.
Recommended rollout
- Click Replace signing secret and copy the new secret. It is shown only once.
- Deploy the new secret to your sender. Any time within the next 24 hours is safe, because requests signed with the old secret still verify until then.
- After your deploy is live, confirm new requests succeed with
200 OK. The old secret expires on its own; there is nothing to remove.
Replacing the signing secret does not change any API key. Webhook delivery uses the secret;
API integrations use a vyg_ba_ key from Settings → Data API Keys.