Who is this page for?
This guide is for the developer who wants to receive Vardast's leads and orders on their own server — a CRM, a website, or any other system. By the end, every time a customer leaves a lead or places an order in chat, the details arrive on your server automatically and securely.
The big picture
Think of Vardast as a courier: it puts each event (a lead or an order) into a package, stamps it with a security seal (a signature), and delivers it to your server's address. Your job is three things: receive the package, check the seal so it isn't forged, and send back a confirmation.
The workflow — in 4 steps
Here is the whole path you'll follow:
- In the dashboard: create a Webhook and enter your server's address. In return, Vardast gives you a signing secret.
- On your own server: build an address (endpoint) that accepts POST requests.
- Automatically: Vardast sends a test message; your server must answer it correctly so the connection becomes active.
- From then on: every lead and order is sent to your server automatically.
Step 1 — Create a Webhook in the dashboard
Go to: Dashboard → Reports → Report Settings → Webhook tab. (Available on the Standard plan and higher.) You fill in three things:
- Server URL: the HTTPS address where you want events delivered — e.g. https://api.yourshop.com/vardast-webhook.
- Name: any label so you recognise this connection later (e.g. “Main CRM”).
- Events: tick which events to receive (new lead, new order).
After you create it, Vardast shows a signing secret (like whsec_…) exactly once. Copy it and store it safely right then — it is never shown again. You will need it in step 3 to verify the signature.
Step 2 — Build the receiving server
On your own server, build an address that accepts POST requests — the same address you entered in step 1. The full code (Node.js, Python, PHP) is in step 3; that one snippet does all three jobs: verify the signature, answer the test message, and receive the event.
What each parameter means
When Vardast sends a POST to your server, the information arrives in two places: a package (the JSON body, called the envelope) and a few headers. Here is what each means:
Inside the package (JSON body)
- id — the unique id of this event. If you receive the same id twice, it is a duplicate; ignore the second one.
- type — the event type: lead.created for a lead or order.created for an order.
- channel_id — which of your channels (Instagram, Telegram, …) produced this event.
- created_at — when the event happened.
- data — the information itself: name, phone, address, order items, etc.
Headers (alongside the package)
- X-Vardast-Event — the event type (same as type).
- X-Vardast-Event-Id — the same id; use it to spot duplicates.
- X-Vardast-Timestamp — when it was sent; used to verify the signature.
- X-Vardast-Signature — the security seal, built with your signing secret. Use it to be sure the package is genuine and untampered.
An example package:
{
"id": "evt_8f14e45f0b2c4a91",
"type": "lead.created",
"api_version": "2026-06-01",
"created_at": "2026-06-13T09:30:00Z",
"channel_id": "a1b2c3d4-...",
"data": {
"full_name": "Ali Rezaei",
"phone_number": "+989121234567",
"username": "ali.rezaei",
"platform": "IG",
"contact_id": "...",
"created_at": "2026-06-13T09:30:00Z"
}
}Step 3 — Verify the signature (the most important step)
Why? So you can be sure the package really came from Vardast, not from someone who guessed your server's address. How? Using your signing secret, you build a signature over “timestamp + the raw package text” and compare it with X-Vardast-Signature; if they match, it is genuine. Three notes:
- Compute the signature over the raw package text, not the JSON your app rebuilt — otherwise the result differs.
- If X-Vardast-Timestamp is more than 5 minutes old, reject the request.
- During a key change, the header may carry two signatures separated by a comma; accept it if one of them matches.
Node.js (Express)
const crypto = require('crypto');
function verifyVardast(rawBody, headers, secret) {
const ts = headers['x-vardast-timestamp'];
const sigHeader = headers['x-vardast-signature'];
if (!ts || !sigHeader) return false;
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // replay window
const expected = 'v1=' + crypto
.createHmac('sha256', secret)
.update(`${ts}.${rawBody}`)
.digest('hex');
return sigHeader.split(',').some((sig) => {
sig = sig.trim();
return sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
});
}
// express.raw, NOT express.json
app.post('/vardast-webhook', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body.toString('utf8');
if (!verifyVardast(rawBody, req.headers, process.env.VARDAST_WEBHOOK_SECRET)) {
return res.status(401).send('invalid signature');
}
const event = JSON.parse(rawBody);
if (event.type === 'webhook.verification') {
return res.status(200).send(event.data.challenge);
}
// dedupe by event.id, then enqueue for async processing
return res.status(200).send('ok');
});Python (Flask)
import hmac, hashlib, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = "whsec_..." # from your environment
def verify(raw_body: bytes, headers) -> bool:
ts = headers.get("X-Vardast-Timestamp")
sig_header = headers.get("X-Vardast-Signature")
if not ts or not sig_header:
return False
if abs(time.time() - int(ts)) > 300: # replay window
return False
expected = "v1=" + hmac.new(
SECRET.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return any(hmac.compare_digest(s.strip(), expected)
for s in sig_header.split(","))
@app.post("/vardast-webhook")
def webhook():
raw = request.get_data() # raw bytes, NOT request.json
if not verify(raw, request.headers):
abort(401)
event = request.get_json()
if event["type"] == "webhook.verification":
return event["data"]["challenge"], 200
# dedupe by event["id"], then enqueue for async processing
return "ok", 200PHP
<?php
$secret = getenv('VARDAST_WEBHOOK_SECRET');
$raw = file_get_contents('php://input'); // raw body
$ts = $_SERVER['HTTP_X_VARDAST_TIMESTAMP'] ?? '';
$sigHdr = $_SERVER['HTTP_X_VARDAST_SIGNATURE'] ?? '';
if ($ts === '' || $sigHdr === '' || abs(time() - (int)$ts) > 300) {
http_response_code(401); exit('invalid');
}
$expected = 'v1=' . hash_hmac('sha256', $ts . '.' . $raw, $secret);
$ok = false;
foreach (explode(',', $sigHdr) as $sig) {
if (hash_equals(trim($sig), $expected)) { $ok = true; break; }
}
if (!$ok) { http_response_code(401); exit('invalid signature'); }
$event = json_decode($raw, true);
if ($event['type'] === 'webhook.verification') {
http_response_code(200); echo $event['data']['challenge']; exit;
}
// dedupe by $event['id'], then enqueue for async processing
http_response_code(200); echo 'ok';Step 4 — Reply correctly
- The test message (activation): if type equals webhook.verification, return the value of data.challenge in the response body with a 2xx code. This is what makes your connection active. Until you answer it correctly, no real events arrive.
- Normal events: reply fast — under 10 seconds — with a 2xx code. Do the heavy work after you reply (queue it).
After you are connected — good to know
- Retries: if your server does not answer, Vardast resends 7 times over about 33 hours.
- Auto-disable: if every attempt fails for 3 days straight, the connection is disabled automatically and you are notified; re-enable it from the dashboard after fixing the issue.
- Secret rotation: if your secret leaks, use “Rotate secret” in the dashboard; the old secret stays valid for 24 hours so you have time to update your server.
Final checklist
- I created a Webhook in the dashboard and saved the signing secret.
- I built an endpoint on my server that accepts POST.
- I verify the signature on every request, over the raw body.
- I answer webhook.verification by returning data.challenge.
- I reply with 2xx under 10 seconds and deduplicate by id.