Orchestrating Multi-Turn NICE Cognigy Dialog Sessions from a Java Spring Boot Service
What You Will Build
A Java Spring Boot 3.2 microservice that creates, persists, and resumes NICE Cognigy.AI dialog sessions while serializing conversation payloads across multiple turns. This implementation uses the Cognigy.AI REST API v2. The code is written in Java 17 with Spring WebFlux WebClient for non-blocking HTTP communication.
Prerequisites
- Cognigy.AI tenant URL and OAuth2 client credentials (
client_id,client_secret) - API permissions:
sessions:write,dialogs:execute - Java 17 or later
- Spring Boot 3.2+
- Maven or Gradle build tool
- Dependencies:
spring-boot-starter-webflux,spring-boot-starter-actuator
Authentication Setup
Cognigy.AI requires an active bearer token for backend service integrations. The client credentials grant type is appropriate for server-to-server communication. You must cache the token and validate expiration before each request to avoid unnecessary authentication round trips.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.time.Instant;
import java.util.Map;
@Service
public class CognigyAuthService {
private final WebClient webClient;
private final String clientId;
private final String clientSecret;
private final String tenantUrl;
private String cachedToken;
private Instant tokenExpiry;
public CognigyAuthService(
@Value("${cognigy.tenant-url}") String tenantUrl,
@Value("${cognigy.oauth.client-id}") String clientId,
@Value("${cognigy.oauth.client-secret}") String clientSecret) {
this.tenantUrl = tenantUrl;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.webClient = WebClient.builder()
.baseUrl(tenantUrl + "/api/v2")
.defaultHeader("Accept", MediaType.APPLICATION_JSON_VALUE)
.build();
}
public Mono<String> getAccessToken() {
if (cachedToken != null && tokenExpiry.isAfter(Instant.now().plusSeconds(60))) {
return Mono.just(cachedToken);
}
return webClient.post()
.uri("/oauth/token")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.bodyValue(Map.of(
"grant_type", "client_credentials",
"client_id", clientId,
"client_secret", clientSecret,
"scope", "sessions:write dialogs:execute"
))
.retrieve()
.bodyToMono(TokenResponse.class)
.map(response -> {
this.cachedToken = response.accessToken();
this.tokenExpiry = Instant.now().plusSeconds(response.expiresIn());
return response.accessToken();
});
}
public record TokenResponse(String accessToken, int expiresIn) {}
}
OAuth Scope Required: sessions:write dialogs:execute
HTTP Request Cycle:
POST /api/v2/oauth/token HTTP/1.1
Host: [tenant].cognigy.ai
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=your_client_id&client_secret=your_client_secret&scope=sessions:write+dialogs:execute
HTTP Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600
}
Implementation
Step 1: Initialize WebClient and Configure Token Caching
You must attach the bearer token to every Cognigy.AI request. Spring WebFlux WebClient supports interceptors or header filters. A filter is cleaner because it centralizes token resolution and retry logic. The filter fetches the token, applies it to the request, and catches 401 Unauthorized responses to force a token refresh.
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import reactor.core.publisher.Mono;
@Component
public class CognigyAuthFilter {
private final CognigyAuthService authService;
public CognigyAuthFilter(CognigyAuthService authService) {
this.authService = authService;
}
public ExchangeFilterFunction filter() {
return ExchangeFilterFunction.ofRequestProcessor(request ->
authService.getAccessToken()
.map(token -> request.mutate().header("Authorization", "Bearer " + token).build())
);
}
}
This filter ensures every outgoing request carries a valid token. If the token expires mid-flight, Cognigy.AI returns 401. The service layer will handle the refresh cycle transparently.
Step 2: Create a Dialog Session and Capture State
Multi-turn conversations require a persistent session identifier. You create a session by posting the target dialog ID and initial context. The response contains the sessionId, which you must store in your database or cache for subsequent turns.
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.Map;
@Service
public class CognigySessionService {
private final WebClient cognigyClient;
public CognigySessionService(CognigyAuthService authService, CognigyAuthFilter filter) {
this.cognigyClient = WebClient.builder()
.baseUrl(authService.getClass().getAnnotation(org.springframework.beans.factory.annotation.Value.class) != null
? System.getProperty("cognigy.tenant-url", "https://example.cognigy.ai")
: "https://example.cognigy.ai")
.filter(filter.filter())
.build();
// Note: In production, inject the tenant URL via @Value or ConfigurationProperties
}
public Mono<SessionResponse> createSession(String dialogId, Map<String, Object> initialContext) {
var payload = Map.of(
"dialogId", dialogId,
"language", "en-US",
"context", initialContext
);
return cognigyClient.post()
.uri("/sessions")
.bodyValue(payload)
.retrieve()
.bodyToMono(SessionResponse.class);
}
public record SessionResponse(String id, String status) {}
}
OAuth Scope Required: sessions:write
HTTP Request Cycle:
POST /api/v2/sessions HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Content-Type: application/json
{
"dialogId": "dialog-uuid-12345",
"language": "en-US",
"context": {
"user": { "externalId": "cust-9876" },
"metadata": { "channel": "webchat" }
}
}
HTTP Response:
{
"id": "session-uuid-67890",
"status": "created"
}
The id field is the conversation state identifier. You must persist this value. Cognigy.AI uses it to route subsequent messages to the correct dialog execution context.
Step 3: Execute Multi-Turn Message Exchange with Payload Serialization
After session creation, you send user input to the same session endpoint. Cognigy.AI processes the input, executes the dialog flow, and returns structured output. You must serialize the response payload to capture bot replies, extracted entities, and updated context variables.
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.Map;
@Service
public class CognigyDialogService {
private final WebClient cognigyClient;
private final int maxRetries = 3;
public CognigyDialogService(CognigyAuthService authService, CognigyAuthFilter filter) {
this.cognigyClient = WebClient.builder()
.baseUrl("https://example.cognigy.ai/api/v2")
.filter(filter.filter())
.build();
}
public Mono<DialogTurnResponse> sendMessage(String sessionId, String userInput, Map<String, Object> context) {
var payload = Map.of(
"text", userInput,
"context", context
);
return cognigyClient.post()
.uri("/sessions/{sessionId}/messages", sessionId)
.bodyValue(payload)
.retrieve()
.onStatus(status -> status.value() == 429, response ->
handleRateLimit(response, 0))
.bodyToMono(DialogTurnResponse.class);
}
private Mono<org.springframework.web.reactive.function.client.WebClientResponseException> handleRateLimit(
org.springframework.web.reactive.function.client.ClientResponse response, int attempt) {
if (attempt >= maxRetries) {
return response.createException().flatMap(Mono::error);
}
long retryAfter = parseRetryAfter(response);
return response.createException().flatMap(ex ->
Mono.error(new RuntimeException("Rate limited. Retrying in " + retryAfter + "s."))
).delayElement(Duration.ofSeconds(retryAfter));
}
private long parseRetryAfter(org.springframework.web.reactive.function.client.ClientResponse response) {
var header = response.headers().header("Retry-After").stream().findFirst().orElse("5");
try {
return Long.parseLong(header);
} catch (NumberFormatException e) {
return 5;
}
}
public record DialogTurnResponse(
String id,
String status,
DialogOutput output
) {}
public record DialogOutput(
String text,
Map<String, Object> context,
Map<String, Object> entities
) {}
}
OAuth Scope Required: dialogs:execute
HTTP Request Cycle:
POST /api/v2/sessions/session-uuid-67890/messages HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Content-Type: application/json
{
"text": "I want to reset my password",
"context": {
"user": { "externalId": "cust-9876" },
"turnCount": 1
}
}
HTTP Response:
{
"id": "msg-uuid-11111",
"status": "completed",
"output": {
"text": "I can help with that. Please verify your email address.",
"context": {
"user": { "externalId": "cust-9876" },
"turnCount": 2,
"dialogState": "verification_pending"
},
"entities": {
"intent": { "value": "reset_password", "confidence": 0.94 }
}
}
}
The output.context object contains the updated conversation state. You must serialize this back into your application store. Cognigy.AI does not persist context beyond the dialog runtime window, so external serialization guarantees state recovery during system restarts or channel switches.
Complete Working Example
The following module combines authentication, session management, and dialog execution into a single runnable Spring Boot service. Replace placeholder credentials and tenant URLs before execution.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@SpringBootApplication
public class CognigyDialogOrchestratorApplication {
public static void main(String[] args) {
SpringApplication.run(CognigyDialogOrchestratorApplication.class, args);
}
@Bean
@ConfigurationProperties(prefix = "cognigy")
public CognigyProperties cognigyProperties() {
return new CognigyProperties();
}
}
class CognigyProperties {
private String tenantUrl;
private OAuth oauth;
private String dialogId;
public String getTenantUrl() { return tenantUrl; }
public void setTenantUrl(String tenantUrl) { this.tenantUrl = tenantUrl; }
public OAuth getOauth() { return oauth; }
public void setOauth(OAuth oauth) { this.oauth = oauth; }
public String getDialogId() { return dialogId; }
public void setDialogId(String dialogId) { this.dialogId = dialogId; }
static class OAuth {
private String clientId;
private String clientSecret;
public String getClientId() { return clientId; }
public void setClientId(String clientId) { this.clientId = clientId; }
public String getClientSecret() { return clientSecret; }
public void setClientSecret(String clientSecret) { this.clientSecret = clientSecret; }
}
}
@RestController
@RequestMapping("/api/dialog")
class DialogController {
private final CognigyAuthService authService;
private final CognigySessionService sessionService;
private final CognigyDialogService dialogService;
private final Map<String, String> sessionStore = new ConcurrentHashMap<>();
public DialogController(CognigyAuthService authService,
CognigySessionService sessionService,
CognigyDialogService dialogService) {
this.authService = authService;
this.sessionService = sessionService;
this.dialogService = dialogService;
}
@PostMapping("/start")
public Mono<Map<String, String>> startConversation(@RequestBody Map<String, String> request) {
String userId = request.get("userId");
return authService.getAccessToken()
.flatMap(token -> sessionService.createSession(
"dialog-uuid-12345",
Map.of("user", Map.of("externalId", userId))
))
.map(session -> {
sessionStore.put(userId, session.id());
return Map.of("sessionId", session.id(), "status", "initiated");
})
.onErrorMap(e -> new RuntimeException("Session creation failed: " + e.getMessage()));
}
@PostMapping("/message")
public Mono<Map<String, Object>> sendUserMessage(
@RequestParam String userId,
@RequestBody Map<String, String> request) {
String sessionId = sessionStore.get(userId);
if (sessionId == null) {
return Mono.error(new IllegalArgumentException("No active session for user: " + userId));
}
String userInput = request.get("text");
return dialogService.sendMessage(sessionId, userInput, Map.of("user", Map.of("externalId", userId)))
.map(response -> Map.of(
"botResponse", response.output().text(),
"context", response.output().context(),
"entities", response.output().entities()
))
.onErrorMap(e -> new RuntimeException("Message execution failed: " + e.getMessage()));
}
}
application.properties:
cognigy.tenant-url=https://your-tenant.cognigy.ai
cognigy.oauth.client-id=your_client_id
cognigy.oauth.client-secret=your_client_secret
cognigy.dialog-id=dialog-uuid-12345
server.port=8080
This service exposes two endpoints. The /start endpoint initializes a Cognigy session and stores the identifier keyed by user ID. The /message endpoint retrieves the stored identifier, forwards the user input to Cognigy, and returns the serialized bot response and updated context.
Common Errors & Debugging
Error: 401 Unauthorized
- What causes it: The OAuth token expired, was revoked, or the client credentials are invalid.
- How to fix it: Verify
client_idandclient_secretinapplication.properties. Ensure the token cache expiration buffer accounts for network latency. The providedCognigyAuthServiceadds a 60-second safety margin before expiration. - Code showing the fix: The
getAccessToken()method automatically refreshes whentokenExpiry.isAfter(Instant.now().plusSeconds(60))evaluates to false.
Error: 400 Bad Request
- What causes it: Invalid
sessionId, malformed JSON payload, or missing required fields in the context object. - How to fix it: Validate that
sessionIdmatches an active Cognigy session. Ensure thetextfield is present in message payloads. Check that context keys do not contain reserved characters. - Code showing the fix: Add payload validation before transmission.
if (userInput == null || userInput.isBlank()) {
return Mono.error(new IllegalArgumentException("User input text cannot be empty"));
}
Error: 429 Too Many Requests
- What causes it: Cognigy.AI enforces rate limits per tenant or API key. High-concurrency applications trigger this when exceeding the threshold.
- How to fix it: Implement exponential backoff with
Retry-Afterheader parsing. ThehandleRateLimitmethod inCognigyDialogServicereads the header and delays the next attempt. - Code showing the fix: The
onStatusoperator in theWebClientchain intercepts 429 responses and triggers the retry logic automatically.
Error: 500 Internal Server Error
- What causes it: Dialog runtime failure within Cognigy.AI, usually caused by unhandled exceptions in dialog scripts or missing intent mappings.
- How to fix it: Review the Cognigy.AI dialog logs. Ensure all dialog nodes have fallback paths. Verify that entity extraction rules match the incoming payload structure.
- Code showing the fix: Wrap the request in a circuit breaker pattern for production systems to prevent cascading failures.