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
| Field | Type | Description |
|---|---|---|
event | string | The event type (e.g., "call.ended") |
business_id | string | Your Kejue business identifier |
call_id | string | Unique identifier for the call |
lead_id | integer | ID of the lead associated with the call |
call_type | string | Either "inbound" or "outbound" |
started_at | string (ISO 8601) | When the call started |
ended_at | string (ISO 8601) | When the call ended |
duration | number | Call duration in seconds |
summary | string | AI-generated summary of the call |
transcript | string | Full transcript of the call |
recording_url | string | URL to access the call recording |
lead | object | Complete 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"
}| Header | Type | Description |
|---|---|---|
X-Kejue-Signature | string | HMAC SHA-256 signature in format "sha256=signature" |
X-Kejue-Timestamp | string | Unix timestamp (milliseconds) when the webhook was sent |
X-Kejue-Event | string | The event type (e.g., "call.ended") |
Content-Type | string | Always "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:
- Combine the timestamp and raw request body:
timestamp + "." + raw_body - Compute HMAC SHA-256 of this string using your webhook secret
- Compare the computed signature with the
X-Kejue-Signatureheader
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_idoreventID
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_idor 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/kejueNext 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

