☰ Menu

Verification (Webhooks)

Last update: October 1, 2025

In this article:

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

x-fa-request-timestamp
x-fa-signature

Replay protection
Reject requests where x-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.

Learn more about HMAC.

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 or x-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.

Picture of Manuel Saucedo
Manuel Saucedo

Technical Writer

🙄

😐

😊

😍

0

Welcome to servis.ai Free Edition

Link your email to begin

Continue with Google

Continue with Microsoft

By continuing, you agree to servis.ai Terms of Use. Read our Privacy Policy.

Get Started with servis.ai

30-minute demo where you see servis.ai in action.

Unlock the essential servis.ai features at no cost.

servis-logo

How can I be of servis?

How can I be of servis?