Authenticating NICE Cognigy.AI Webhook Requests with Python
What You Will Build
You will build a production-grade Python FastAPI service that receives NICE Cognigy.AI webhook events, verifies cryptographic signatures, enforces timestamp drift limits, validates IP allowlists, and routes verified payloads to downstream systems while emitting audit logs and SIEM callbacks. This implementation uses the Python httpx library and standard cryptographic primitives to handle the complete authentication pipeline. The code covers Python 3.10 and above.
Prerequisites
- NICE Cognigy.AI workspace with webhook endpoint configured
- Shared secret key generated in Cognigy.AI for webhook signing
- Python 3.10+ runtime
- Dependencies:
fastapi,uvicorn,httpx,cryptography,pydantic,structlog - Required permissions: Cognigy.AI Admin or Developer role to configure webhooks and secrets
- Note: Webhook ingestion does not use OAuth. Authentication relies on HMAC-SHA256 signature verification with a workspace shared secret.
Authentication Setup
Cognigy.AI webhooks sign every outgoing request using a workspace-specific shared secret. The platform appends three security headers to every POST request: X-Cognigy-Webhook-Id, X-Cognigy-Timestamp, and X-Cognigy-Signature. Your service must reconstruct the exact payload string that Cognigy hashed, apply the same HMAC-SHA256 algorithm, and compare the result against the provided signature header.
Store the shared secret in an environment variable. Never embed secrets in source code. The following environment variables are required:
COGNIGY_WEBHOOK_SECRET: Base64-encoded or raw shared secretCOGNIGY_ALLOWED_IPS: Comma-separated IPv4 addresses or CIDR rangesMAX_TIMESTAMP_DRIFT_SECONDS: Maximum acceptable clock skewSIEM_ENDPOINT_URL: HTTPS URL for your external security event collector
Implementation
Step 1: Atomic POST Receive and FastAPI Setup
The ingestion endpoint must accept POST requests atomically. FastAPI handles concurrent requests efficiently, but you must ensure the request body is read exactly once before validation. The endpoint returns a structured JSON response indicating success or failure.
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
import time
import logging
app = FastAPI(title="Cognigy Webhook Authenticator")
logger = logging.getLogger("cognigy_auth")
@app.post("/webhooks/cognigy")
async def receive_webhook(request: Request) -> JSONResponse:
start_time = time.perf_counter()
try:
body_bytes = await request.body()
if not body_bytes:
raise HTTPException(status_code=400, detail="Empty request body")
# Pass to validation pipeline
auth_result = await validate_cognigy_request(request, body_bytes)
latency_ms = (time.perf_counter() - start_time) * 1000
logger.info("Webhook authenticated", latency_ms=latency_ms, webhook_id=auth_result.webhook_id)
return JSONResponse(
status_code=200,
content={"status": "verified", "message": "Payload accepted"}
)
except HTTPException:
raise
except Exception as e:
logger.error("Authentication pipeline failed", error=str(e))
raise HTTPException(status_code=500, detail="Internal validation error")
Expected request structure:
POST /webhooks/cognigy HTTP/1.1
Host: your-domain.com
Content-Type: application/json
X-Cognigy-Webhook-Id: wh_8f9a2b3c4d5e
X-Cognigy-Timestamp: 1715429871
X-Cognigy-Signature: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6
X-Cognigy-Nonce: n_9x8y7z6w5v4u3t2s1r0q
{
"type": "conversation.started",
"data": {
"conversationId": "conv_123456",
"userId": "user_789012",
"timestamp": "2024-05-11T14:31:11Z"
}
}
Step 2: Header Extraction and Signature Matrix Parsing
Cognigy sends signature metadata in a fixed header matrix. You must extract these headers atomically and validate their presence before proceeding. Missing headers indicate either a misconfigured webhook or a spoofed request.
from dataclasses import dataclass
from typing import Optional
@dataclass
class AuthHeaders:
webhook_id: str
timestamp: str
signature: str
nonce: Optional[str]
source_ip: str
def extract_security_headers(request: Request) -> AuthHeaders:
headers = request.headers
webhook_id = headers.get("X-Cognigy-Webhook-Id")
timestamp = headers.get("X-Cognigy-Timestamp")
signature = headers.get("X-Cognigy-Signature")
nonce = headers.get("X-Cognigy-Nonce")
source_ip = request.client.host if request.client else "unknown"
if not all([webhook_id, timestamp, signature]):
raise HTTPException(status_code=400, detail="Missing required security headers")
return AuthHeaders(
webhook_id=webhook_id,
timestamp=timestamp,
signature=signature,
nonce=nonce,
source_ip=source_ip
)
Step 3: Timestamp Drift Enforcement and Nonce Validation
Cognigy appends a Unix epoch timestamp to prevent replay attacks. Your service must enforce a maximum drift window. Nonce validation prevents duplicate processing of the same webhook event. You will track processed nonces in an in-memory sliding window for this example.
import os
import time
from collections import deque
MAX_DRIFT = int(os.getenv("MAX_TIMESTAMP_DRIFT_SECONDS", "300"))
NONCE_WINDOW = deque(maxlen=10000)
def enforce_timestamp_drift(timestamp_str: str) -> None:
try:
webhook_ts = int(timestamp_str)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid timestamp format")
current_ts = int(time.time())
drift = abs(current_ts - webhook_ts)
if drift > MAX_DRIFT:
raise HTTPException(
status_code=403,
detail=f"Timestamp drift {drift}s exceeds maximum allowed {MAX_DRIFT}s"
)
def validate_nonce(nonce: Optional[str]) -> None:
if not nonce:
logger.warning("Nonce missing in webhook request")
return
if nonce in NONCE_WINDOW:
raise HTTPException(status_code=429, detail="Duplicate nonce detected")
NONCE_WINDOW.append(nonce)
Step 4: HMAC Verification and IP Allowlist Pipeline
The core security check reconstructs the signed payload exactly as Cognigy does. The platform concatenates the timestamp and raw request body, then applies HMAC-SHA256 using the shared secret. You must also verify the source IP against an allowlist before accepting the signature.
import hmac
import hashlib
import ipaddress
import os
def load_ip_allowlist() -> list:
allowed = os.getenv("COGNIGY_ALLOWED_IPS", "0.0.0.0/0").split(",")
networks = []
for ip in allowed:
networks.append(ipaddress.ip_network(ip.strip(), strict=False))
return networks
ALLOWED_NETWORKS = load_ip_allowlist()
def verify_ip(source_ip: str) -> None:
try:
client_addr = ipaddress.ip_address(source_ip)
if not any(client_addr in net for net in ALLOWED_NETWORKS):
raise HTTPException(status_code=403, detail="Source IP not in allowlist")
except ValueError:
raise HTTPException(status_code=400, detail="Invalid IP address format")
def verify_hmac_signature(body_bytes: bytes, headers: AuthHeaders) -> None:
secret = os.getenv("COGNIGY_WEBHOOK_SECRET")
if not secret:
raise HTTPException(status_code=500, detail="Webhook secret not configured")
# Cognigy signs: timestamp + raw_body
payload_to_sign = f"{headers.timestamp}{body_bytes.decode('utf-8')}".encode('utf-8')
computed_signature = hmac.new(
secret.encode('utf-8'),
payload_to_sign,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(computed_signature, headers.signature):
raise HTTPException(status_code=401, detail="Signature verification failed")
Step 5: SIEM Callback, Metrics, and Audit Logging
Verified webhooks trigger downstream security logging. You will emit structured audit events, track validation latency and success rates, and forward critical events to an external SIEM endpoint using httpx.
import httpx
import json
from datetime import datetime, timezone
from structlog import get_logger
structlog_logger = get_logger()
metrics = {"total_requests": 0, "success_count": 0, "failure_count": 0}
async def emit_siem_event(webhook_id: str, source_ip: str, status: str, latency_ms: float) -> None:
siem_url = os.getenv("SIEM_ENDPOINT_URL")
if not siem_url:
return
event_payload = {
"event_type": "cognigy_webhook_auth",
"webhook_id": webhook_id,
"source_ip": source_ip,
"status": status,
"latency_ms": round(latency_ms, 2),
"timestamp": datetime.now(timezone.utc).isoformat()
}
try:
async with httpx.AsyncClient(timeout=5.0) as client:
await client.post(siem_url, json=event_payload, headers={"Content-Type": "application/json"})
except httpx.RequestError as e:
structlog_logger.error("SIEM callback failed", error=str(e))
async def validate_cognigy_request(request: Request, body_bytes: bytes) -> AuthHeaders:
metrics["total_requests"] += 1
headers = extract_security_headers(request)
verify_ip(headers.source_ip)
enforce_timestamp_drift(headers.timestamp)
validate_nonce(headers.nonce)
verify_hmac_signature(body_bytes, headers)
metrics["success_count"] += 1
structlog_logger.info("Audit: Webhook authenticated",
webhook_id=headers.webhook_id,
source_ip=headers.source_ip,
nonce=headers.nonce)
await emit_siem_event(headers.webhook_id, headers.source_ip, "success", 0.0)
return headers
Complete Working Example
The following module combines all validation stages, metrics tracking, and audit logging into a single runnable FastAPI application. Run it with uvicorn cognigy_authenticator:app --port 8000.
import os
import time
import logging
import hashlib
import hmac
import ipaddress
from collections import deque
from datetime import datetime, timezone
from dataclasses import dataclass
from typing import Optional
import httpx
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
from structlog import get_logger
# Configuration
app = FastAPI(title="Cognigy Webhook Authenticator")
logger = logging.getLogger("cognigy_auth")
structlog_logger = get_logger()
MAX_DRIFT = int(os.getenv("MAX_TIMESTAMP_DRIFT_SECONDS", "300"))
NONCE_WINDOW = deque(maxlen=10000)
metrics = {"total_requests": 0, "success_count": 0, "failure_count": 0}
@dataclass
class AuthHeaders:
webhook_id: str
timestamp: str
signature: str
nonce: Optional[str]
source_ip: str
def load_ip_allowlist() -> list:
allowed = os.getenv("COGNIGY_ALLOWED_IPS", "0.0.0.0/0").split(",")
return [ipaddress.ip_network(ip.strip(), strict=False) for ip in allowed]
ALLOWED_NETWORKS = load_ip_allowlist()
def extract_security_headers(request: Request) -> AuthHeaders:
headers = request.headers
webhook_id = headers.get("X-Cognigy-Webhook-Id")
timestamp = headers.get("X-Cognigy-Timestamp")
signature = headers.get("X-Cognigy-Signature")
nonce = headers.get("X-Cognigy-Nonce")
source_ip = request.client.host if request.client else "unknown"
if not all([webhook_id, timestamp, signature]):
raise HTTPException(status_code=400, detail="Missing required security headers")
return AuthHeaders(webhook_id, timestamp, signature, nonce, source_ip)
def enforce_timestamp_drift(timestamp_str: str) -> None:
try:
webhook_ts = int(timestamp_str)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid timestamp format")
current_ts = int(time.time())
drift = abs(current_ts - webhook_ts)
if drift > MAX_DRIFT:
raise HTTPException(status_code=403, detail=f"Timestamp drift {drift}s exceeds maximum allowed {MAX_DRIFT}s")
def validate_nonce(nonce: Optional[str]) -> None:
if not nonce:
logger.warning("Nonce missing in webhook request")
return
if nonce in NONCE_WINDOW:
raise HTTPException(status_code=429, detail="Duplicate nonce detected")
NONCE_WINDOW.append(nonce)
def verify_ip(source_ip: str) -> None:
try:
client_addr = ipaddress.ip_address(source_ip)
if not any(client_addr in net for net in ALLOWED_NETWORKS):
raise HTTPException(status_code=403, detail="Source IP not in allowlist")
except ValueError:
raise HTTPException(status_code=400, detail="Invalid IP address format")
def verify_hmac_signature(body_bytes: bytes, headers: AuthHeaders) -> None:
secret = os.getenv("COGNIGY_WEBHOOK_SECRET")
if not secret:
raise HTTPException(status_code=500, detail="Webhook secret not configured")
payload_to_sign = f"{headers.timestamp}{body_bytes.decode('utf-8')}".encode('utf-8')
computed_signature = hmac.new(secret.encode('utf-8'), payload_to_sign, hashlib.sha256).hexdigest()
if not hmac.compare_digest(computed_signature, headers.signature):
raise HTTPException(status_code=401, detail="Signature verification failed")
async def emit_siem_event(webhook_id: str, source_ip: str, status: str, latency_ms: float) -> None:
siem_url = os.getenv("SIEM_ENDPOINT_URL")
if not siem_url:
return
event_payload = {
"event_type": "cognigy_webhook_auth",
"webhook_id": webhook_id,
"source_ip": source_ip,
"status": status,
"latency_ms": round(latency_ms, 2),
"timestamp": datetime.now(timezone.utc).isoformat()
}
try:
async with httpx.AsyncClient(timeout=5.0) as client:
await client.post(siem_url, json=event_payload, headers={"Content-Type": "application/json"})
except httpx.RequestError as e:
structlog_logger.error("SIEM callback failed", error=str(e))
async def validate_cognigy_request(request: Request, body_bytes: bytes) -> AuthHeaders:
metrics["total_requests"] += 1
headers = extract_security_headers(request)
verify_ip(headers.source_ip)
enforce_timestamp_drift(headers.timestamp)
validate_nonce(headers.nonce)
verify_hmac_signature(body_bytes, headers)
metrics["success_count"] += 1
structlog_logger.info("Audit: Webhook authenticated", webhook_id=headers.webhook_id, source_ip=headers.source_ip, nonce=headers.nonce)
await emit_siem_event(headers.webhook_id, headers.source_ip, "success", 0.0)
return headers
@app.post("/webhooks/cognigy")
async def receive_webhook(request: Request) -> JSONResponse:
start_time = time.perf_counter()
try:
body_bytes = await request.body()
if not body_bytes:
raise HTTPException(status_code=400, detail="Empty request body")
auth_result = await validate_cognigy_request(request, body_bytes)
latency_ms = (time.perf_counter() - start_time) * 1000
logger.info("Webhook authenticated", latency_ms=latency_ms, webhook_id=auth_result.webhook_id)
return JSONResponse(status_code=200, content={"status": "verified", "message": "Payload accepted"})
except HTTPException:
metrics["failure_count"] += 1
raise
except Exception as e:
metrics["failure_count"] += 1
logger.error("Authentication pipeline failed", error=str(e))
raise HTTPException(status_code=500, detail="Internal validation error")
@app.get("/metrics")
def get_metrics():
return metrics
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The HMAC signature does not match the computed hash. This occurs when the shared secret differs between Cognigy.AI and your environment, or when the payload string concatenation order is incorrect.
- Fix: Verify that
COGNIGY_WEBHOOK_SECRETmatches exactly. Ensure the signed payload follows the patterntimestamp + raw_body. Print the computed signature temporarily for comparison. - Code Fix: Ensure
payload_to_signconstruction matches Cognigy specification exactly. Usehmac.compare_digestto prevent timing attacks.
Error: 403 Forbidden
- Cause: Source IP is blocked by the allowlist, or timestamp drift exceeds the configured threshold.
- Fix: Add the Cognigy.AI outbound IP range to
COGNIGY_ALLOWED_IPS. If using a reverse proxy, ensureX-Forwarded-Foris correctly parsed. AdjustMAX_TIMESTAMP_DRIFT_SECONDSif your server clock is unsynchronized. - Code Fix: Enable NTP synchronization on the host. Expand the allowlist CIDR ranges if Cognigy uses dynamic egress IPs.
Error: 429 Too Many Requests
- Cause: Duplicate nonce detected within the sliding window. Cognigy retrains failed deliveries, which can trigger duplicate validation attempts.
- Fix: Increase the nonce window size or implement a persistent backing store for nonces in high-throughput environments. Configure Cognigy retry intervals to exceed your deduplication window.
- Code Fix: Replace
dequewith Redis or PostgreSQL-backed nonce tracking for production scaling.
Error: 500 Internal Server Error
- Cause: Missing environment variables, SIEM endpoint unreachable, or unhandled exception in the validation pipeline.
- Fix: Validate all required environment variables at startup. Wrap SIEM callbacks in try-except blocks to prevent webhook rejection due to downstream failures.
- Code Fix: Add startup health checks that verify secret configuration and network connectivity before accepting traffic.