Quantiv supports webhooks to allow CRM and App Providers to receive real-time notifications about key events related to:
- EngagePro™ - Lead Widget lifecycle (creation and updates)
- EngagePro™ - Lead Widgett interactions (lead creation and updates as end users go through the EngagePro™ - Lead Widget flow)
These webhooks can be used to trigger internal logic, populate analytics, or provide your Clients with up-to-date information about their leads for further follow-up.
Quantiv uses HTTPS to send these notifications to your application as a JSON payload.
- Store leads in your internal database
- Notify Clients about new leads
- Display lead's information to Clients
- Trigger internal logic (e.g., qualification, analytics, automation)
To start receiving real-time event data from Quantiv, follow these steps:
Decide which event types are relevant for your application’s logic.
A single webhook endpoint can listen for one or multiple types of events and Quantiv emits the following types of events:
| Event Name | Description |
|---|---|
lead.created | A new lead flow session has started |
lead.updated | Lead information has been updated |
widget.created | A widget has been successfully created |
widget.updated | A widget has been updated or reconfigured |
Set up an HTTP endpoint (i.e., a URL) on your local machine that can receive unauthenticated incoming POST requests from Quantiv.
Your endpoint must be configured to read event objects for the type of event notifications you want to receive.
Quantiv sends events to your webhook endpoint as part of a POST request with a JSON payload.
Check Event Objects
Each event is structured as an event object with id, event, and related event data nested under the data object.
Your endpoint must check the event type and parse the payload of each event.
Within the data object, there are two main objects:
previousObjectprovides the data prior to the change that triggered the event.objectprovides the updated data (after the change that triggered the event).
These objects only appear if relevant. For example, in a lead.created event only object will appear, but in a lead.updated event both previousObject and object will appear.
By comparing the information within previousObject and object, you can identify which field(s) changed, as well as the values before and after the change.
Return a 2xx Response
Your endpoint must quickly return a successful status code (2xx) before executing any complex logic that could cause a timeout.
For example, return a 200 response before calling any external services to verify lead information.
Events that do not return a 2xx status will enter Quantiv's built-in retry logic.
Built-In Retry Logic
| Retry Number | Retry Timing |
|---|---|
| 1 | 5 minutes after the actual request |
| 2 | 15 minutes |
| 3 | 30 minutes |
| 4 | 1 hour |
| 5 | 2 hours |
| 6 | 4 hours |
| 7 | 8 hours |
If there are no successful responses from your endpoint(s), Quantiv will:
- After 1 hour, email all active organization users that the endpoint is failing and needs attention.
- After 24 hours, mark failing endpoint(s) as inactive.
Your server must be publicly accessible over HTTPS for Quantiv to reach it securely.
Use the Quantiv API to register your webhook URL and specify:
- The environment using Test or Production API keys
- Desired event types
To protect your webhook from unauthorized access or spoofed calls, you can implement the webhook signature verification by providing the following items:
- Event payload
x-quantiv-signatureheader- Your endpoint’s secret
Below you can find Quantiv webhook signature decode solution examples for different languages.
If verification fails, the function returns an error.
Node.js / TypeScript / JavaScript
import * as crypto from 'crypto';
export class WebhookVerifier {
private readonly secret: string;
constructor(secret: string) {
this.secret = secret;
}
verifySignature(payload: string, signature: string): boolean {
try {
const cleanSignature = signature.replace('sha256=', '');
const expectedSignature = crypto
.createHmac('sha256', this.secret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(cleanSignature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
} catch (error) {
console.error('Signature verification failed:', error);
return false;
}
}
}Express.js
import express from 'express';
const app = express();
const verifier = new WebhookVerifier('your-webhook-secret-here');
app.use('/webhook', express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
const signature = req.headers['x-webhook-signature'] as string;
const payload = req.body.toString('utf8');
if (!signature) return res.status(400).json({ error: 'Missing signature header' });
if (!verifier.verifySignature(payload, signature)) {
return res.status(401).json({ error: 'Invalid signature' });
}
try {
const webhookData = JSON.parse(payload);
switch (webhookData.event) {
case 'user.created': handleUserCreated(webhookData.data); break;
case 'user.updated': handleUserUpdated(webhookData.data); break;
default: console.log('Unknown event type:', webhookData.event);
}
res.status(200).json({ received: true });
} catch (error) {
console.error('Error processing webhook:', error);
res.status(500).json({ error: 'Processing failed' });
}
});
function handleUserCreated(data: any) {
console.log('New user created:', data.userId);
}
function handleUserUpdated(data: any) {
console.log('User updated:', data.userId);
}Next.js
import { NextRequest, NextResponse } from 'next/server';
import { WebhookVerifier } from '../../lib/webhook-verifier';
const verifier = new WebhookVerifier(process.env.WEBHOOK_SECRET!);
export async function POST(request: NextRequest) {
try {
const signature = request.headers.get('x-webhook-signature');
const payload = await request.text();
if (!signature) return NextResponse.json({ error: 'Missing signature' }, { status: 400 });
if (!verifier.verifySignature(payload, signature)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
const webhookData = JSON.parse(payload);
await processWebhook(webhookData);
return NextResponse.json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
return NextResponse.json({ error: 'Internal error' }, { status: 500 });
}
}
async function processWebhook(data: any) {
console.log('Processing webhook:', data.event);
}Python (Flask)
import hashlib
import hmac
import json
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = "your-webhook-secret-here"
def verify_signature(payload, signature, secret):
try:
clean_signature = signature.replace('sha256=', '')
expected_signature = hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(clean_signature, expected_signature)
except Exception as e:
print(f"Signature verification failed: {e}")
return False
@app.route('/webhook', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Webhook-Signature')
payload = request.get_data(as_text=True)
if not signature:
return jsonify({'error': 'Missing signature header'}), 400
if not verify_signature(payload, signature, WEBHOOK_SECRET):
return jsonify({'error': 'Invalid signature'}), 401
try:
webhook_data = json.loads(payload)
event_type = webhook_data.get('event')
data = webhook_data.get('data')
if event_type == 'user.created':
handle_user_created(data)
elif event_type == 'user.updated':
handle_user_updated(data)
else:
print(f"Unknown event type: {event_type}")
return jsonify({'received': True})
except Exception as e:
print(f"Error processing webhook: {e}")
return jsonify({'error': 'Processing failed'}), 500
def handle_user_created(data):
print(f"New user created: {data.get('userId')}")
def handle_user_updated(data):
print(f"User updated: {data.get('userId')}")
if __name__ == '__main__':
app.run(debug=True)
</code></pre>
PHP
<?php
class WebhookVerifier {
private $secret;
public function __construct($secret) {
$this->secret = $secret;
}
public function verifySignature($payload, $signature) {
try {
$cleanSignature = str_replace('sha256=', '', $signature);
$expectedSignature = hash_hmac('sha256', $payload, $this->secret);
return hash_equals($cleanSignature, $expectedSignature);
} catch (Exception $e) {
error_log("Signature verification failed: " . $e->getMessage());
return false;
}
}
}
$webhookSecret = 'your-webhook-secret-here';
$verifier = new WebhookVerifier($webhookSecret);
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$payload = file_get_contents('php://input');
if (empty($signature)) {
http_response_code(400);
echo json_encode(['error' => 'Missing signature header']);
exit;
}
if (!$verifier->verifySignature($payload, $signature)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
try {
$webhookData = json_decode($payload, true);
$eventType = $webhookData['event'] ?? '';
$data = $webhookData['data'] ?? [];
switch ($eventType) {
case 'user.created': handleUserCreated($data); break;
case 'user.updated': handleUserUpdated($data); break;
default: error_log("Unknown event type: $eventType");
}
http_response_code(200);
echo json_encode(['received' => true]);
} catch (Exception $e) {
error_log("Error processing webhook: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Processing failed']);
}
function handleUserCreated($data) {
error_log("New user created: " . $data['userId']);
}
function handleUserUpdated($data) {
error_log("User updated: " . $data['userId']);
}Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
)
const webhookSecret = "your-webhook-secret-here"
type WebhookData struct {
Event string `json:"event"`
Data map[string]interface{} `json:"data"`
Timestamp string `json:"timestamp"`
ID string `json:"id"`
}
func verifySignature(payload, signature, secret string) bool {
cleanSignature := strings.TrimPrefix(signature, "sha256=")
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(payload))
expectedSignature := hex.EncodeToString(mac.Sum(nil))
expectedBytes, _ := hex.DecodeString(expectedSignature)
signatureBytes, _ := hex.DecodeString(cleanSignature)
return subtle.ConstantTimeCompare(expectedBytes, signatureBytes) == 1
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
signature := r.Header.Get("X-Webhook-Signature")
if signature == "" {
http.Error(w, "Missing signature header", http.StatusBadRequest)
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error reading body", http.StatusBadRequest)
return
}
payload := string(body)
if !verifySignature(payload, signature, webhookSecret) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
var webhookData WebhookData
if err := json.Unmarshal(body, &webhookData); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
switch webhookData.Event {
case "user.created":
handleUserCreated(webhookData.Data)
case "user.updated":
handleUserUpdated(webhookData.Data)
default:
log.Printf("Unknown event type: %s", webhookData.Event)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]bool{"received": true})
}
func handleUserCreated(data map[string]interface{}) {
log.Printf("New user created: %v", data["userId"])
}
func handleUserUpdated(data map[string]interface{}) {
log.Printf("User updated: %v", data["userId"])
}
func main() {
http.HandleFunc("/webhook", webhookHandler)
log.Println("Webhook server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}Webhooks are a powerful way to keep your platform in sync with what’s happening inside your Clients’ EngagePro™ - Lead Widgets — from the moment a widget is created to every update a lead makes in the flow.
By integrating webhooks, you unlock real-time visibility into lead activity and give your system the tools to:
- Enrich your internal dashboards and analytics
- Notify your Clients instantly
- Automate internal actions based on lead behavior
✅ Whether you're handling 10 Clients or 10,000 — webhooks ensure you're always up to date without polling or manual sync.