Verify Webhook Events
Servis.ai offers two ways to verify the authenticity of any webhook servis.ai is receivng (or match your app’s existing header scheme):
-
Verify with servis.ai’s headers (Default)
-
Verify with your own headers (Custom)
Verify with servis.ai’s headers (Default)
Set up in the webhook form
-
Secure Webhook = Yes
-
Secure Type = Default
-
Secret = your shared secret (keep it private)
Default Headers
Replay protection
Reject requests wherex-fa-request-timestamp
is older/newer than ~5 minutes from your server’s clock. Timestamps + HMAC significantly reduce replay risk.
crypto.timingSafeEqual
helps avoid timing attacks when comparing secrets.
Sender example (Default)
// DEFAULT option: example sender import crypto from 'crypto'; import fetch from 'node-fetch'; const WEBHOOK_URL = 'https://example.com/webhook/abc123'; const SECRET = 'sk_demo_12345abc67890'; // same value set in the form function sign(secret, ts, bodyStr) { const base = `v0:${ts}:${bodyStr}`; const hex = crypto.createHmac('sha256', secret).update(base).digest('hex'); return `sha256=${hex}`; } async function sendWebhook(data) { const ts = Math.floor(Date.now() / 1000).toString(); const bodyStr = JSON.stringify(data); const res = await fetch(WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-fa-request-timestamp': ts, 'x-fa-signature': sign(SECRET, ts, bodyStr), }, body: bodyStr }); console.log('Status:', res.status); console.log('Body:', await res.text()); } sendWebhook({ name: 'John Doe' }).catch(console.error);
Verify with your own headers (Custom)
If your application already uses specific header names or builds the signature differently, choose Custom. In this mode, verification is performed by a short function you paste into the webhook’s Custom Code box. You can keep x-fa-request-timestamp
and x-fa-signature
or change them—just make sure the code reads the same names your system sends.
Set up in the webhook form
-
Secure Webhook = Yes
-
Secure Type = Custom
-
Secret = the same value your system uses
Custom verification code (server side example)
(function(request, secret, context){ const crypto = context.libs.crypto; // Read the headers your system sends: // If your app uses different names, change these two lines accordingly. const ts = request.header('x-zm-request-timestamp'); const sig = request.header('x-zm-signature'); // Required format and presence check if (!ts || !sig || !sig.startsWith('sha256=')) { return false; } // Reject old or future requests beyond ±5 minutes const now = Math.floor(Date.now() / 1000); if (Math.abs(now - Number(ts)) > 300) { return false; } // Use the raw body if available to avoid formatting differences const payload = request.rawBody ?? JSON.stringify(request.body); const signed = `v0:${ts}:${payload}`; // Recompute expected signature with the same Secret const expected = 'sha256=' + crypto.createHmac('sha256', secret) .update(signed, 'utf8') .digest('hex'); // Constant-time comparison to avoid timing attacks const a = Buffer.from(expected, 'utf8'); const b = Buffer.from(sig, 'utf8'); if (a.length !== b.length) return false; return crypto.timingSafeEqual(a, b); }(request, secret, context));
HMAC-SHA256 is the recommended MAC for this scheme; compare using constant time and validate timestamps to mitigate replays.
Sender example (Custom)
// CUSTOM option: example sender import crypto from 'crypto'; import fetch from 'node-fetch'; const WEBHOOK_URL = 'https://example.com/webhook/abc123'; const SECRET = 'sk_demo_12345abc67890'; // same value you set in the form function sign(secret, ts, bodyStr) { const base = `v0:${ts}:${bodyStr}`; const hex = crypto.createHmac('sha256', secret).update(base).digest('hex'); return `sha256=${hex}`; } async function sendWebhook(data) { const ts = Math.floor(Date.now() / 1000).toString(); const bodyStr = JSON.stringify(data); const res = await fetch(WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-fa-request-timestamp': ts, // match the names your Custom code reads 'x-fa-signature': sign(SECRET, ts, bodyStr), }, body: bodyStr }); console.log('Status:', res.status); console.log('Body:', await res.text()); } sendWebhook({ name: 'John Doe' }).catch(console.error);
Secret: what it is and how to set it up
What it is
A private string known only by you and the webhook. It’s used to create and verify the signature so you can be sure the request really came from your system/servis.ai. (This is standard HMAC usage.)
How to set it
-
In the webhook form, enter a strong Secret (for example:
sk_demo_12345abc67890
). -
Use the same exact string in your sending code.
-
If you rotate it later, update it in both places at the same time.
Troubleshooting
-
Verification failed → Secret mismatch, body changed after signing, missing
sha256=
prefix, or timestamp too old/new. -
Missing required headers (Default) →
x-fa-request-timestamp
orx-fa-signature
was not sent. -
Tip → Always compute the signature over the exact raw JSON string you send; even whitespace changes the digest.
-
Tip → Keep a ~5-minute acceptance window to prevent replay attacks.
That’s it—pick Default for the quickest secure setup, or Custom if you need to match your own headers or signature format.