Skip to content
Last updated

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.


✅ Use Cases

  • 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)

🛠 How to Register Your Webhook

To start receiving real-time event data from Quantiv, follow these steps:

Step 1: Choose which events to subscribe to

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 NameDescription
lead.createdA new lead flow session has started
lead.updatedLead information has been updated
widget.createdA widget has been successfully created
widget.updatedA widget has been updated or reconfigured

Step 2: Create a webhook endpoint

Set up an HTTP endpoint (i.e., a URL) on your local machine that can receive unauthenticated incoming POST requests from Quantiv.

Step 3: Handle and validate incoming events

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:

  • previousObject provides the data prior to the change that triggered the event.
  • object provides 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 NumberRetry Timing
15 minutes after the actual request
215 minutes
330 minutes
41 hour
52 hours
64 hours
78 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.

Step 4: Deploy your webhook to a public HTTPS URL

Your server must be publicly accessible over HTTPS for Quantiv to reach it securely.

Step 5: Register your webhook using the Quantiv API

Use the Quantiv API to register your webhook URL and specify:

  • The environment using Test or Production API keys
  • Desired event types

🔐 Securing Your Webhook Endpoint

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-signature header
  • 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.