Webhooks

Set up webhooks to receive real-time notifications about call events

Introduction

Webhooks allow you to receive real-time notifications when events occur in your Kejue account. Instead of polling the API for updates, Kejue will send HTTP POST requests to your specified endpoint whenever an event is triggered.

What are Webhooks?

Webhooks are HTTP callbacks that notify your application when specific events happen. Kejue sends webhook requests to your server with event data, allowing you to:

  • Process call completions automatically
  • Update your database with call results
  • Trigger follow-up actions based on call outcomes
  • Sync data between systems in real-time

Webhook Events

Kejue sends webhooks for various events. The most common event is call.ended, which is triggered when a call completes.

Example Payload

Here's an example webhook payload for the call.ended event:

{
  "event": "call.ended",
  "business_id": "busi_1234567890abcdef",
  "call_id": "call_0987654321fedcba",
  "lead_id": 123,
  "call_type": "outbound",
  "started_at": "2023-10-27T10:00:00Z",
  "ended_at": "2023-10-27T10:05:00Z",
  "duration": 300.5,
  "call_success": "Good",
  "call_cost": "0.45",
  "assistant_id": "asst_abcdef1234567890",
  "assistant_group_id": "grp_fedcba0987654321",
  "call_source": "api",
  "summary": "The customer was interested in the new product and requested a follow-up call next week.",
  "recording_url": "https://bnhzukuzoyjggktijotc.supabase.co/storage/v1/object/public/CallRecordings/8be0b9e1-c8...7c-stereo.mp3",
  "transcript": "Agent: Hello, this is... \nCustomer: Hi, I'm interested...",
  "callback": {
    "id": 2185,
    "scheduled_at": "2025-09-12T07:00:00+00:00"
  },
  "appointment": {
    "id": 107,
    "meeting_time": "2025-09-12T17:00:00+00:00",
    "title": "Consultation - Youssef Abdelsalam",
    "duration_minutes": 30
  },
  "lead": {
    "id": 123,
    "business_id": "busi_1234567890abcdef",
    "first_name": "John",
    "last_name": "Doe",
    "email": "john.doe@example.com",
    "phone_number": "+15551234567",
    "country": "USA",
    "timezone": "America/New_York",
    "whatsapp_number": "+15551234567",
    "language": "english",
    "status": "interested",
    "lead_source": "campaign",
    "contact_attempts": 1,
    "notes": "Initial contact, seems very interested.",
    "priority": "high",
    "metadata": {
      "phone_agent": {
        "custom_field": "custom_value"
      },
      "whatsapp_agent": null
    },
    "last_called_at": "2023-10-27T10:00:00Z",
    "last_messaged_at": null,
    "retry_count": 0
  }
}

Payload Fields

FieldTypeDescription
eventstringThe event type (e.g., "call.ended")
business_idstringYour Kejue business identifier
call_idstringUnique identifier for the call
lead_idintegerID of the lead associated with the call
call_typestringEither "inbound" or "outbound"
started_atstring (ISO 8601)When the call started
ended_atstring (ISO 8601)When the call ended
durationnumberCall duration in seconds
summarystringAI-generated summary of the call
transcriptstringFull transcript of the call
recording_urlstringURL to access the call recording
leadobjectComplete lead information including contact details and metadata

Headers

Kejue includes the following headers with every webhook request:

{
  "X-Kejue-Signature": "sha256=a1b2c3d4e5f6...",
  "X-Kejue-Timestamp": "1672531200000",
  "X-Kejue-Event": "call.ended",
  "Content-Type": "application/json"
}
HeaderTypeDescription
X-Kejue-SignaturestringHMAC SHA-256 signature in format "sha256=signature"
X-Kejue-TimestampstringUnix timestamp (milliseconds) when the webhook was sent
X-Kejue-EventstringThe event type (e.g., "call.ended")
Content-TypestringAlways "application/json"

Security

Signature Verification

Kejue signs all webhook requests using HMAC SHA-256. You must verify the signature to ensure the request is authentic and hasn't been tampered with.

The signature is calculated as follows:

  1. Combine the timestamp and raw request body: timestamp + "." + raw_body
  2. Compute HMAC SHA-256 of this string using your webhook secret
  3. Compare the computed signature with the X-Kejue-Signature header

Always use constant-time comparison functions (like hmac.compare_digest in Python or crypto.timingSafeEqual in Node.js) to prevent timing attacks.

Best Practices

  • Verify signatures immediately: Reject requests with invalid signatures
  • Check timestamps: Reject requests older than 5 minutes to prevent replay attacks
  • Store secrets securely: Never commit webhook secrets to version control
  • Use HTTPS: Always use HTTPS endpoints for webhooks
  • Idempotency: Handle duplicate webhooks gracefully using the call_id or event ID

Implementation Examples

import os
import hmac
import hashlib
import json
from flask import Flask, request, jsonify

app = Flask(__name__)

KEJUE_WEBHOOK_SECRET = os.environ.get("KEJUE_WEBHOOK_SECRET")

@app.route('/webhooks/kejue', methods=['POST'])
def handle_kejue_webhook():
    if not KEJUE_WEBHOOK_SECRET:
        return jsonify({"error": "Server configuration error"}), 500

    signature_header = request.headers.get('X-Kejue-Signature')
    timestamp_header = request.headers.get('X-Kejue-Timestamp')
    raw_body = request.data

    if not signature_header or not timestamp_header:
        return jsonify({"error": "Missing required headers"}), 400

    signed_payload = f"{timestamp_header}.".encode('utf-8') + raw_body

    expected_signature = hmac.new(
        key=KEJUE_WEBHOOK_SECRET.encode('utf-8'),
        msg=signed_payload,
        digestmod=hashlib.sha256
    ).hexdigest()

    try:
        received_signature = signature_header.split('=')[1]
    except IndexError:
        return jsonify({"error": "Invalid signature format"}), 400

    if not hmac.compare_digest(expected_signature, received_signature):
        return jsonify({"error": "Signature verification failed"}), 403

    event_payload = json.loads(raw_body)
    print(f"Received call.ended event for lead ID: {event_payload.get('lead_id')}")

    return jsonify({"status": "success"}), 200

if __name__ == '__main__':
    app.run(port=5000)
const express = require('express');
const crypto = require('crypto');

const app = express();
const KEJUE_WEBHOOK_SECRET = process.env.KEJUE_WEBHOOK_SECRET;

// Middleware to capture raw body for signature verification
app.use('/webhooks/kejue', express.raw({ type: 'application/json' }));

app.post('/webhooks/kejue', (req, res) => {
  if (!KEJUE_WEBHOOK_SECRET) {
    return res.status(500).json({ error: 'Server configuration error' });
  }

  const signatureHeader = req.headers['x-kejue-signature'];
  const timestampHeader = req.headers['x-kejue-timestamp'];

  if (!signatureHeader || !timestampHeader) {
    return res.status(400).json({ error: 'Missing required headers' });
  }

  const signedPayload = Buffer.concat([
    Buffer.from(timestampHeader + '.'),
    req.body
  ]);

  const expectedSignature = crypto
    .createHmac('sha256', KEJUE_WEBHOOK_SECRET)
    .update(signedPayload)
    .digest('hex');

  const receivedSignature = signatureHeader.split('=')[1];

  if (!crypto.timingSafeEqual(
    Buffer.from(expectedSignature),
    Buffer.from(receivedSignature)
  )) {
    return res.status(403).json({ error: 'Signature verification failed' });
  }

  const eventPayload = JSON.parse(req.body.toString());
  console.log(`Received ${eventPayload.event} event for lead ID: ${eventPayload.lead_id}`);

  res.json({ status: 'success' });
});

app.listen(3000, () => {
  console.log('Webhook server listening on port 3000');
});
require 'sinatra'
require 'openssl'
require 'json'

KEJUE_WEBHOOK_SECRET = ENV['KEJUE_WEBHOOK_SECRET']

post '/webhooks/kejue' do
  return status 500 if KEJUE_WEBHOOK_SECRET.nil?

  signature_header = request.env['HTTP_X_KEJUE_SIGNATURE']
  timestamp_header = request.env['HTTP_X_KEJUE_TIMESTAMP']
  raw_body = request.body.read

  if signature_header.nil? || timestamp_header.nil?
    return status 400
  end

  signed_payload = "#{timestamp_header}.#{raw_body}"

  expected_signature = OpenSSL::HMAC.hexdigest(
    'sha256',
    KEJUE_WEBHOOK_SECRET,
    signed_payload
  )

  received_signature = signature_header.split('=')[1]

  unless Rack::Utils.secure_compare(expected_signature, received_signature)
    return status 403
  end

  event_payload = JSON.parse(raw_body)
  puts "Received #{event_payload['event']} event for lead ID: #{event_payload['lead_id']}"

  { status: 'success' }.to_json
end
<?php

$KEJUE_WEBHOOK_SECRET = getenv('KEJUE_WEBHOOK_SECRET');

if (!$KEJUE_WEBHOOK_SECRET) {
    http_response_code(500);
    echo json_encode(['error' => 'Server configuration error']);
    exit;
}

$signatureHeader = $_SERVER['HTTP_X_KEJUE_SIGNATURE'] ?? null;
$timestampHeader = $_SERVER['HTTP_X_KEJUE_TIMESTAMP'] ?? null;
$rawBody = file_get_contents('php://input');

if (!$signatureHeader || !$timestampHeader) {
    http_response_code(400);
    echo json_encode(['error' => 'Missing required headers']);
    exit;
}

$signedPayload = $timestampHeader . '.' . $rawBody;
$expectedSignature = hash_hmac('sha256', $signedPayload, $KEJUE_WEBHOOK_SECRET);

$receivedSignature = explode('=', $signatureHeader)[1] ?? '';

if (!hash_equals($expectedSignature, $receivedSignature)) {
    http_response_code(403);
    echo json_encode(['error' => 'Signature verification failed']);
    exit;
}

$eventPayload = json_decode($rawBody, true);
error_log("Received {$eventPayload['event']} event for lead ID: {$eventPayload['lead_id']}");

http_response_code(200);
echo json_encode(['status' => 'success']);
?>
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strings"
)

var webhookSecret = os.Getenv("KEJUE_WEBHOOK_SECRET")

func verifySignature(signature, timestamp, body string) bool {
    signedPayload := timestamp + "." + body
    mac := hmac.New(sha256.New, []byte(webhookSecret))
    mac.Write([]byte(signedPayload))
    expectedSignature := hex.EncodeToString(mac.Sum(nil))

    receivedSignature := strings.Split(signature, "=")[1]
    return hmac.Equal([]byte(expectedSignature), []byte(receivedSignature))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    if webhookSecret == "" {
        http.Error(w, "Server configuration error", http.StatusInternalServerError)
        return
    }

    signature := r.Header.Get("X-Kejue-Signature")
    timestamp := r.Header.Get("X-Kejue-Timestamp")

    if signature == "" || timestamp == "" {
        http.Error(w, "Missing required headers", http.StatusBadRequest)
        return
    }

    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Error reading body", http.StatusBadRequest)
        return
    }

    if !verifySignature(signature, timestamp, string(body)) {
        http.Error(w, "Signature verification failed", http.StatusForbidden)
        return
    }

    var eventPayload map[string]interface{}
    if err := json.Unmarshal(body, &eventPayload); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }

    fmt.Printf("Received %s event for lead ID: %v\n",
        eventPayload["event"], eventPayload["lead_id"])

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(map[string]string{"status": "success"})
}

func main() {
    http.HandleFunc("/webhooks/kejue", webhookHandler)
    http.ListenAndServe(":8080", nil)
}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.net.InetSocketAddress;

public class WebhookServer {
    private static final String WEBHOOK_SECRET = System.getenv("KEJUE_WEBHOOK_SECRET");
    private static final ObjectMapper objectMapper = new ObjectMapper();

    static class WebhookHandler implements HttpHandler {
        @Override
        public void handle(HttpExchange exchange) throws IOException {
            if (!exchange.getRequestMethod().equals("POST")) {
                exchange.sendResponseHeaders(405, -1);
                return;
            }

            if (WEBHOOK_SECRET == null) {
                sendError(exchange, 500, "Server configuration error");
                return;
            }

            String signature = exchange.getRequestHeaders().getFirst("X-Kejue-Signature");
            String timestamp = exchange.getRequestHeaders().getFirst("X-Kejue-Timestamp");

            if (signature == null || timestamp == null) {
                sendError(exchange, 400, "Missing required headers");
                return;
            }

            String body = new String(exchange.getRequestBody().readAllBytes(), 
                                    StandardCharsets.UTF_8);
            String signedPayload = timestamp + "." + body;

            try {
                Mac mac = Mac.getInstance("HmacSHA256");
                SecretKeySpec secretKeySpec = new SecretKeySpec(
                    WEBHOOK_SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
                mac.init(secretKeySpec);
                byte[] hash = mac.doFinal(signedPayload.getBytes(StandardCharsets.UTF_8));
                String expectedSignature = HexFormat.of().formatHex(hash);

                String receivedSignature = signature.split("=")[1];

                if (!MessageDigest.isEqual(
                    expectedSignature.getBytes(StandardCharsets.UTF_8),
                    receivedSignature.getBytes(StandardCharsets.UTF_8)
                )) {
                    sendError(exchange, 403, "Signature verification failed");
                    return;
                }

                var eventPayload = objectMapper.readValue(body, Map.class);
                System.out.printf("Received %s event for lead ID: %s%n",
                    eventPayload.get("event"), eventPayload.get("lead_id"));

                String response = "{\"status\":\"success\"}";
                exchange.sendResponseHeaders(200, response.length());
                exchange.getResponseBody().write(response.getBytes());
            } catch (Exception e) {
                sendError(exchange, 500, "Processing error");
            } finally {
                exchange.close();
            }
        }

        private void sendError(HttpExchange exchange, int code, String message) 
                throws IOException {
            String response = String.format("{\"error\":\"%s\"}", message);
            exchange.sendResponseHeaders(code, response.length());
            exchange.getResponseBody().write(response.getBytes());
            exchange.close();
        }
    }

    public static void main(String[] args) throws IOException {
        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
        server.createContext("/webhooks/kejue", new WebhookHandler());
        server.setExecutor(null);
        server.start();
        System.out.println("Webhook server listening on port 8080");
    }
}

Troubleshooting

Common Issues

Signature verification fails

  • Ensure you're using the correct webhook secret from your Kejue dashboard
  • Verify you're reading the raw request body (not parsed JSON) for signature calculation
  • Check that you're combining timestamp and body in the correct format: timestamp + "." + raw_body
  • Ensure you're comparing the signature after the sha256= prefix

Webhook not received

  • Verify your endpoint is publicly accessible (use a tool like ngrok for local testing)
  • Check that your server is listening on the correct port
  • Ensure your webhook URL is configured correctly in the Kejue dashboard
  • Check server logs for any errors

Duplicate webhooks

  • Implement idempotency checks using the call_id or event timestamp
  • Store processed webhook IDs to prevent duplicate processing
  • Use a database transaction or distributed lock for critical operations

Testing Webhooks Locally

To test webhooks locally, use a tunneling service like ngrok:

# Install ngrok
# Then expose your local server
ngrok http 5000

# Use the ngrok URL in your Kejue webhook configuration
# Example: https://abc123.ngrok.io/webhooks/kejue

Next Steps

  • Configure your webhook endpoint in the Kejue dashboard
  • Test your webhook implementation with the examples above
  • Set up monitoring and logging for webhook events
  • Review the API Reference for other integration options

Last updated on