Build a Java Webhook to Bridge Cognigy.AI Intents to Legacy SOAP CRM
What You Will Build
- This tutorial constructs a Java webhook that intercepts Cognigy.AI conversation events, extracts user intents and slot values, and queries a legacy SOAP CRM for order details.
- The solution uses the Cognigy.AI REST API for context management, Jakarta JAXB for XML parsing, and standard Java HTTP clients for SOAP communication.
- The implementation is written in Java 17 and leverages Spring Boot 3.x for the webhook server.
Prerequisites
- Cognigy.AI API token or credentials with
context:readandcontext:writepermissions enabled in the Cognigy.AI console. - Legacy SOAP CRM endpoint URL and valid WS-Security credentials (username/password).
- Java Development Kit 17 or later.
- Spring Boot 3.x project with dependencies:
spring-boot-starter-web,jakarta.xml.bind-api,jakarta.xml.bind,jackson-databind. - Maven or Gradle for dependency management.
Authentication Setup
Cognigy.AI REST API authentication requires a Bearer token obtained through the login endpoint. The token expires after a configurable duration and must be cached or refreshed before context updates. The SOAP CRM requires WS-Security headers containing a UsernameToken with a generated nonce and created timestamp.
The following method retrieves a Cognigy.AI token and caches it in memory. Production systems should use a distributed cache or token rotation service.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;
public class CognigyAuthService {
private static final String LOGIN_URL = "https://api.cognigy.ai/v1/auth/login";
private static final HttpClient httpClient = HttpClient.newBuilder().build();
private static final ObjectMapper mapper = new ObjectMapper();
private String cachedToken = null;
private long tokenExpiry = 0;
public String getBearerToken(String email, String password) throws Exception {
if (cachedToken != null && System.currentTimeMillis() < tokenExpiry) {
return cachedToken;
}
String requestBody = mapper.writeValueAsString(Map.of(
"email", email,
"password", password
));
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(LOGIN_URL))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException("Cognigy.AI authentication failed with status " + response.statusCode());
}
Map<String, Object> responseBody = mapper.readValue(response.body(), Map.class);
cachedToken = (String) responseBody.get("token");
tokenExpiry = System.currentTimeMillis() + (3600 * 1000); // Cache for 1 hour
return cachedToken;
}
}
The Cognigy.AI console requires the API user to have context modification rights. The SOAP CRM WS-Security header construction is handled in the implementation steps below.
Implementation
Step 1: Webhook Endpoint & Payload Parsing
Cognigy.AI sends a JSON payload to configured webhooks when a user triggers an intent or updates slots. The payload contains the contextId, intent, and slots map. The webhook endpoint must validate the payload structure and extract the necessary data before proceeding.
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/webhooks/cognigy")
public class CognigyWebhookController {
private final SoapCrmService soapCrmService;
private final CognigyContextService cognigyContextService;
public CognigyWebhookController(SoapCrmService soapCrmService, CognigyContextService cognigyContextService) {
this.soapCrmService = soapCrmService;
this.cognigyContextService = cognigyContextService;
}
@PostMapping("/intent")
public ResponseEntity<String> handleIntent(@RequestBody CognigyPayload payload) {
try {
if (payload.contextId() == null || payload.intent() == null) {
return ResponseEntity.badRequest().body("Missing contextId or intent");
}
String orderNumber = extractSlotValue(payload.slots(), "orderNumber");
if (orderNumber == null) {
return ResponseEntity.ok("No orderNumber slot provided");
}
// Business logic continues in Step 2 and 3
// Placeholder for flow control
return ResponseEntity.ok("Webhook received");
} catch (Exception e) {
return ResponseEntity.status(500).body("Internal error: " + e.getMessage());
}
}
private String extractSlotValue(Map<String, Object> slots, String slotName) {
if (slots == null) return null;
Object value = slots.get(slotName);
if (value instanceof Map) {
return (String) ((Map<?, ?>) value).get("value");
}
return value != null ? value.toString() : null;
}
}
public record CognigyPayload(String contextId, String intent, Map<String, Object> slots) {}
The extractSlotValue method handles Cognigy.AI slot structure variations. Slots often arrive as nested objects containing a value field. The controller validates required fields before delegating to service layers. Missing fields return a 200 OK with a message to prevent Cognigy.AI webhook retry loops.
Step 2: SOAP Request Construction with WS-Security
Legacy SOAP systems frequently require WS-Security authentication. The UsernameToken profile requires a password digest, nonce, and created timestamp. I construct the XML envelope manually to avoid heavy dependency chains while maintaining strict namespace compliance.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.Base64;
import java.util.UUID;
public class SoapCrmService {
private static final String SOAP_ENDPOINT = "https://crm.legacy-system.com/api/orders";
private static final HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(java.time.Duration.ofSeconds(10))
.build();
public String buildSoapRequest(String orderNumber, String username, String password) throws Exception {
String timestamp = Instant.now().toString();
String nonce = Base64.getEncoder().encodeToString(UUID.randomUUID().toString().getBytes());
// Generate PasswordDigest: Base64( SHA1( nonce + timestamp + password ) )
String digestInput = nonce + timestamp + password;
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
byte[] digestBytes = sha1.digest(digestInput.getBytes(StandardCharsets.UTF_8));
String passwordDigest = Base64.getEncoder().encodeToString(digestBytes);
String soapEnvelope = """
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ord="http://legacy-crm.com/orders">
<soapenv:Header>
<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
soapenv:mustUnderstand="1">
<wsse:UsernameToken>
<wsse:Username>%s</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">%s</wsse:Password>
<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">%s</wsse:Nonce>
<wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">%s</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>
</soapenv:Header>
<soapenv:Body>
<ord:GetOrderDetails>
<ord:OrderNumber>%s</ord:OrderNumber>
</ord:GetOrderDetails>
</soapenv:Body>
</soapenv:Envelope>
""".formatted(username, passwordDigest, nonce, timestamp, orderNumber);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(SOAP_ENDPOINT))
.header("Content-Type", "text/xml;charset=UTF-8")
.header("SOAPAction", "GetOrderDetails")
.POST(HttpRequest.BodyPublishers.ofString(soapEnvelope))
.build();
return httpClient.send(request, HttpResponse.BodyHandlers.ofString()).body();
}
}
The PasswordDigest calculation prevents plaintext password transmission over networks. The wsse:Security header uses mustUnderstand="1" to force SOAP processors to validate authentication before processing the body. The SOAP endpoint URL and namespace prefixes must match your CRM WSDL exactly.
Step 3: JAXB Response Parsing & Retry Logic
SOAP responses contain XML that JAXB unmarshals into Java objects. Transient network failures or CRM throttling require retry logic with exponential backoff. The retry loop catches specific exceptions and delays subsequent attempts.
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Unmarshaller;
import jakarta.xml.bind.annotation.*;
import java.io.StringReader;
import java.util.concurrent.TimeUnit;
@XmlRootElement(name = "GetOrderDetailsResponse", namespace = "http://legacy-crm.com/orders")
@XmlAccessorType(XmlAccessType.FIELD)
public class OrderResponse {
@XmlElement(name = "OrderId")
private String orderId;
@XmlElement(name = "Status")
private String status;
@XmlElement(name = "CustomerName")
private String customerName;
public String getOrderId() { return orderId; }
public String getStatus() { return status; }
public String getCustomerName() { return customerName; }
}
public class SoapCrmService {
// ... previous code ...
public OrderResponse fetchOrderWithRetry(String orderNumber, String username, String password, int maxRetries) throws Exception {
int attempt = 0;
long baseDelayMs = 1000;
Exception lastException = null;
while (attempt < maxRetries) {
try {
String xmlResponse = buildSoapRequest(orderNumber, username, password);
return unmarshalResponse(xmlResponse);
} catch (Exception e) {
lastException = e;
attempt++;
if (attempt >= maxRetries) {
break;
}
long delay = baseDelayMs * (long) Math.pow(2, attempt - 1);
TimeUnit.MILLISECONDS.sleep(delay);
}
}
throw new RuntimeException("SOAP request failed after " + maxRetries + " attempts", lastException);
}
private OrderResponse unmarshalResponse(String xml) throws JAXBException {
// Handle SOAP Faults before unmarshalling
if (xml.contains("<soap:Fault>") || xml.contains("<soapenv:Fault>")) {
throw new RuntimeException("SOAP Fault received: " + extractFaultReason(xml));
}
JAXBContext context = JAXBContext.newInstance(OrderResponse.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
return (OrderResponse) unmarshaller.unmarshal(new StringReader(xml));
}
private String extractFaultReason(String xml) {
int reasonStart = xml.indexOf("<faultstring>");
int reasonEnd = xml.indexOf("</faultstring>");
if (reasonStart != -1 && reasonEnd != -1) {
return xml.substring(reasonStart + 13, reasonEnd);
}
return "Unknown SOAP Fault";
}
}
The exponential backoff formula baseDelay * 2^(attempt-1) prevents immediate retry storms. The extractFaultReason method parses SOAP Fault bodies before JAXB attempts unmarshalling, which would otherwise throw UnmarshalException. JAXB requires the response XML root element to match the @XmlRootElement annotation exactly.
Step 4: Map CRM Fields to Cognigy Context Variables
After retrieving order details, the system updates the Cognigy.AI conversation context. The REST API accepts a PUT request to the context endpoint with a JSON payload containing the variables object.
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Map;
public class CognigyContextService {
private static final String CONTEXT_BASE_URL = "https://api.cognigy.ai/v1/contexts";
private static final HttpClient httpClient = HttpClient.newBuilder().build();
private static final ObjectMapper mapper = new ObjectMapper();
public void updateContextVariables(String contextId, String bearerToken, OrderResponse orderData) throws Exception {
if (contextId == null || bearerToken == null) {
throw new IllegalArgumentException("contextId and bearerToken are required");
}
Map<String, Object> variables = Map.of(
"crmOrderId", orderData.getOrderId(),
"crmOrderStatus", orderData.getStatus(),
"crmCustomerName", orderData.getCustomerName(),
"crmLastUpdated", Instant.now().toString()
);
String requestBody = mapper.writeValueAsString(Map.of("variables", variables));
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(CONTEXT_BASE_URL + "/" + contextId))
.header("Authorization", "Bearer " + bearerToken)
.header("Content-Type", "application/json")
.PUT(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200 && response.statusCode() != 204) {
throw new RuntimeException("Cognigy.AI context update failed with status " + response.statusCode() + ": " + response.body());
}
}
}
The PUT /v1/contexts/{contextId} endpoint merges the provided variables into the existing context. Overwriting requires explicit handling in Cognigy.AI dialog flows. The service validates required parameters before constructing the HTTP request. Status codes 200 and 204 indicate successful context modification.
Complete Working Example
The following Spring Boot application integrates all components. Replace placeholder credentials and endpoints with your environment values.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class CognigySoapBridgeApplication {
public static void main(String[] args) {
SpringApplication.run(CognigySoapBridgeApplication.class, args);
}
@Bean
public CognigyAuthService cognigyAuthService() {
return new CognigyAuthService();
}
@Bean
public SoapCrmService soapCrmService() {
return new SoapCrmService();
}
@Bean
public CognigyContextService cognigyContextService() {
return new CognigyContextService();
}
}
// Update CognigyWebhookController to use services
@RestController
@RequestMapping("/webhooks/cognigy")
public class CognigyWebhookController {
private final SoapCrmService soapCrmService;
private final CognigyContextService cognigyContextService;
private final CognigyAuthService cognigyAuthService;
public CognigyWebhookController(SoapCrmService soapCrmService,
CognigyContextService cognigyContextService,
CognigyAuthService cognigyAuthService) {
this.soapCrmService = soapCrmService;
this.cognigyContextService = cognigyContextService;
this.cognigyAuthService = cognigyAuthService;
}
@PostMapping("/intent")
public ResponseEntity<String> handleIntent(@RequestBody CognigyPayload payload) {
try {
if (payload.contextId() == null || payload.intent() == null) {
return ResponseEntity.badRequest().body("Missing contextId or intent");
}
String orderNumber = extractSlotValue(payload.slots(), "orderNumber");
if (orderNumber == null) {
return ResponseEntity.ok("No orderNumber slot provided");
}
String token = cognigyAuthService.getBearerToken("api-user@cognigy.ai", "your-api-password");
OrderResponse orderData = soapCrmService.fetchOrderWithRetry(orderNumber, "crm-user", "crm-password", 3);
cognigyContextService.updateContextVariables(payload.contextId(), token, orderData);
return ResponseEntity.ok("Order details mapped to context");
} catch (Exception e) {
return ResponseEntity.status(500).body("Internal error: " + e.getMessage());
}
}
private String extractSlotValue(Map<String, Object> slots, String slotName) {
if (slots == null) return null;
Object value = slots.get(slotName);
if (value instanceof Map) {
return (String) ((Map<?, ?>) value).get("value");
}
return value != null ? value.toString() : null;
}
}
Deploy this application to a cloud function, container, or virtual machine. Configure the public URL in Cognigy.AI dialog flow webhook settings. The endpoint processes requests synchronously, which aligns with Cognigy.AI webhook timeout limits.
Common Errors & Debugging
Error: 401 Unauthorized from Cognigy.AI
- Cause: Expired Bearer token or missing
context:writepermission. - Fix: Verify API user permissions in the Cognigy.AI console. Implement token refresh logic before context updates. Check that the
Authorizationheader uses the exact formatBearer <token>. - Code adjustment: Add token expiry validation and force re-authentication on 401 responses.
Error: SOAP Fault - Security Header Missing
- Cause: WS-Security namespace mismatch or invalid PasswordDigest calculation.
- Fix: Ensure
http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsdis correctly referenced. Verify the digest input order:nonce + timestamp + password. Use Wireshark or Postman to compare generated headers against known working requests.
Error: JAXB UnmarshalException
- Cause: SOAP response XML namespace differs from
@XmlRootElementnamespace or element names do not match@XmlElementannotations. - Fix: Log the raw XML response before unmarshalling. Adjust namespace URIs and element names in the
OrderResponseclass. Remove XML comments or processing instructions that JAXB parsers reject.
Error: 429 Too Many Requests from SOAP CRM
- Cause: CRM rate limiting during rapid webhook triggers.
- Fix: Increase
baseDelayMsin the retry logic. Implement circuit breaker patterns for sustained failures. Add jitter to backoff calculations to prevent thundering herd effects.