Verify Webhook Events
Servis.ai offers two ways to verify the authenticity of any webhook request servis.ai receives, or to 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-timestampis 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 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 to you and the webhook sender. It’s used to create and verify the signature so you can be sure the request really came from your system or from 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 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-timestamporx-fa-signaturewas 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, most secure setup, or Custom if you need to match your own headers or signature format.