Processing NICE Cognigy.AI Payment Intents with TypeScript and Stripe Webhooks
What You Will Build
- A TypeScript Express service that intercepts Cognigy.AI checkout dialog events, creates Stripe Checkout Sessions with dynamic line items, and processes payment outcomes.
- Uses the Stripe Node.js SDK, Cognigy.AI Session and Dialog APIs, and the Datadog Metrics API.
- Written in TypeScript with Express, ready for deployment on any Node.js runtime.
Prerequisites
- Cognigy.AI API Key with
session:writeanddialog:writepermissions - Stripe Secret Key (
sk_live_...) and Webhook Signing Secret (whsec_...) - Datadog API Key and Application Key
- Node.js 18+ and npm or pnpm
- Dependencies:
express,stripe,axios,uuid,dotenv,cors,@types/express,@types/node,@types/uuid
Install dependencies:
npm install express stripe axios uuid dotenv cors
npm install --save-dev @types/express @types/node @types/uuid typescript ts-node nodemon
Authentication Setup
Cognigy.AI backend integrations authenticate via API keys. You must configure your key in the Cognigy.AI Studio under Settings. The key must possess session:write to modify session variables and dialog:write to trigger dialog routing. Stripe requires a secret key for server-side API calls and a webhook secret for signature verification. Datadog requires both an API key and an application key for metric ingestion.
Configure environment variables in a .env file:
COGNIGY_API_KEY=your_cognigy_api_key
STRIPE_SECRET_KEY=sk_live_your_stripe_secret_key
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret
DATADOG_API_KEY=your_datadog_api_key
DATADOG_APP_KEY=your_datadog_app_key
COGNIGY_BASE_URL=https://api.cognigy.ai
Initialize the HTTP clients and Stripe SDK:
import Stripe from 'stripe';
import axios from 'axios';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-11-20.acacia',
timeout: 10000,
});
const cognigy = axios.create({
baseURL: process.env.COGNIGY_BASE_URL,
headers: {
Authorization: `Bearer ${process.env.COGNIGY_API_KEY}`,
'Content-Type': 'application/json',
},
});
const datadog = axios.create({
baseURL: 'https://api.datadoghq.com',
headers: {
'DD-API-KEY': process.env.DATADOG_API_KEY,
'DD-APPLICATION-KEY': process.env.DATADOG_APP_KEY,
'Content-Type': 'application/json',
},
});
Implementation
Step 1: Capture Checkout Dialog Node and Initialize Stripe Session
Cognigy.AI sends a payload to your webhook when the conversation reaches a configured dialog node. The payload contains the sessionId, the originating nodeId, and a payload object carrying cart data. You must extract the line items, generate a unique idempotency key, and call the Stripe Checkout API.
The idempotency key prevents duplicate charges if Cognigy.AI retries the webhook call or if the client resends the request. Stripe guarantees that identical requests with the same Idempotency-Key header return the same response without creating a second session.
import { v4 as uuidv4 } from 'uuid';
interface CognigyPaymentPayload {
sessionId: string;
nodeId: string;
payload: {
items: Array<{ priceId: string; quantity: number }>;
};
}
export async function handlePaymentInit(
req: import('express').Request,
res: import('express').Response
) {
const { sessionId, payload }: CognigyPaymentPayload = req.body;
if (!payload?.items?.length) {
return res.status(400).json({ error: 'Missing cart items' });
}
const idempotencyKey = `cognigy_${sessionId}_${uuidv4()}`;
try {
const session = await stripe.checkout.sessions.create(
{
line_items: payload.items.map((item) => ({
price: item.priceId,
quantity: item.quantity,
})),
mode: 'payment',
success_url: `${process.env.COGNIGY_BASE_URL}/api/v1/sessions/${sessionId}/dialog?type=dialog&dialog=payment_success`,
cancel_url: `${process.env.COGNIGY_BASE_URL}/api/v1/sessions/${sessionId}/dialog?type=dialog&dialog=payment_cancelled`,
metadata: { cognigy_session_id: sessionId },
},
{
idempotencyKey,
}
);
res.json({ success: true, url: session.url, sessionId: session.id });
} catch (error: any) {
if (error.status === 400) {
return res.status(400).json({ error: 'Invalid Stripe price or configuration' });
}
if (error.status === 429) {
return res.status(429).json({ error: 'Stripe rate limit exceeded' });
}
res.status(500).json({ error: 'Failed to create checkout session' });
}
}
Expected Request:
POST /cognigy/payment-init
{
"sessionId": "sess_a1b2c3d4",
"nodeId": "checkout_node",
"priceId": "price_123abc",
"quantity": 2
}
Expected Response:
{
"success": true,
"url": "https://checkout.stripe.com/c/pay/cs_live_xxx#fidkdWxOYHwnPyd1blpxYHZxWjA0V0N...",
"sessionId": "cs_live_xxx"
}
Step 2: Handle Stripe Webhook Callbacks and Update Cognigy Session Variables
Stripe sends asynchronous events to your webhook endpoint when payment states change. You must verify the webhook signature to ensure the request originates from Stripe. After verification, extract the cognigy_session_id from the session metadata and update the Cognigy.AI session variables.
Cognigy.AI session variables persist across dialog turns. Updating them allows subsequent dialog nodes to read the payment outcome and adjust conversation flow accordingly.
export async function handleStripeWebhook(
req: import('express').Request,
res: import('express').Response
) {
const sig = req.headers['stripe-signature'] as string;
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret!);
} catch (err: any) {
return res.status(400).json({ error: `Webhook signature verification failed: ${err.message}` });
}
const session = event.data.object as Stripe.Checkout.Session;
const cognigySessionId = session.metadata?.cognigy_session_id;
if (!cognigySessionId) {
return res.status(200).json({ skipped: true, reason: 'Missing cognigy_session_id metadata' });
}
switch (event.type) {
case 'checkout.session.completed':
await updateCognigyVariables(cognigySessionId, {
payment_status: 'completed',
stripe_session_id: session.id,
payment_amount: String(session.amount_total || 0),
});
await logMetric('cognigy.payment.success', 1, ['status:completed', `session:${cognigySessionId}`]);
break;
case 'checkout.session.expired':
case 'payment_intent.payment_failed':
await updateCognigyVariables(cognigySessionId, {
payment_status: 'failed',
stripe_session_id: session.id,
failure_reason: session.payment_intent ? 'payment_declined' : 'session_expired',
});
await logMetric('cognigy.payment.failed', 1, ['status:failed', `session:${cognigySessionId}`]);
await routeToDialogNode(cognigySessionId, 'payment_retry_node');
break;
default:
break;
}
res.json({ received: true });
}
Cognigy Variables Update Request:
POST /api/v1/sessions/sess_a1b2c3d4/variables
Authorization: Bearer <COGNIGY_API_KEY>
Content-Type: application/json
{
"variables": {
"payment_status": "completed",
"stripe_session_id": "cs_live_xxx",
"payment_amount": "2499"
}
}
Cognigy Variables Update Response:
{
"status": "success",
"sessionId": "sess_a1b2c3d4",
"updatedVariables": ["payment_status", "stripe_session_id", "payment_amount"]
}
Step 3: Route Failed Payments to Retry Dialog via Dialog API
When a payment fails or expires, the conversation must return to a recovery path. Cognigy.AI provides a Dialog API endpoint that accepts a target node identifier. Sending a dialog type message forces the active session to jump to the specified node, bypassing linear flow execution.
You must implement retry logic for 429 and 5xx responses. Cognigy.AI enforces rate limits on session manipulation endpoints. Exponential backoff prevents cascading failures during high volume.
async function updateCognigyVariables(sessionId: string, variables: Record<string, string>) {
return retryWithBackoff(() =>
cognigy.post(`/api/v1/sessions/${sessionId}/variables`, { variables })
);
}
async function routeToDialogNode(sessionId: string, targetNode: string) {
return retryWithBackoff(() =>
cognigy.post(`/api/v1/sessions/${sessionId}/dialog`, {
type: 'dialog',
dialog: targetNode,
})
);
}
async function retryWithBackoff<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
const status = error.response?.status;
if (status === 429 || (status >= 500 && status < 600)) {
const delay = Math.pow(2, attempt) * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
throw new Error('Maximum retry attempts exceeded');
}
Dialog Routing Request:
POST /api/v1/sessions/sess_a1b2c3d4/dialog
Authorization: Bearer <COGNIGY_API_KEY>
Content-Type: application/json
{
"type": "dialog",
"dialog": "payment_retry_node"
}
Dialog Routing Response:
{
"status": "success",
"message": "Dialog routed to payment_retry_node"
}
Step 4: Log Transaction Metrics to Datadog
Transaction metrics require ingestion via the Datadog Series API. The API accepts an array of metric objects containing the metric name, type, timestamp, value, and tags. You must format the payload strictly according to the Datadog specification. The API returns a 202 Accepted response upon successful ingestion.
async function logMetric(metricName: string, value: number, tags: string[]) {
const timestamp = Math.floor(Date.now() / 1000);
const payload = {
series: [
{
metric: metricName,
type: 'count',
points: [[timestamp, value]],
tags: tags,
},
],
};
try {
await retryWithBackoff(() => datadog.post('/api/v1/series', payload));
} catch (error: any) {
console.error(`Failed to log metric ${metricName}:`, error.response?.data || error.message);
}
}
Datadog Metrics Request:
POST /api/v1/series
DD-API-KEY: <DATADOG_API_KEY>
DD-APPLICATION-KEY: <DATADOG_APP_KEY>
Content-Type: application/json
{
"series": [
{
"metric": "cognigy.payment.success",
"type": "count",
"points": [[1715423890, 1]],
"tags": ["status:completed", "session:sess_a1b2c3d4"]
}
]
}
Datadog Metrics Response:
{
"status": "ok"
}
Complete Working Example
The following file combines all components into a single runnable Express server. Replace environment variables before execution.
import express from 'express';
import cors from 'cors';
import Stripe from 'stripe';
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
import dotenv from 'dotenv';
dotenv.config();
const app = express();
app.use(cors());
app.use(express.json({ verify: (req, res, buf) => { (req as any).rawBody = buf; } }));
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-11-20.acacia',
timeout: 10000,
});
const cognigy = axios.create({
baseURL: process.env.COGNIGY_BASE_URL,
headers: {
Authorization: `Bearer ${process.env.COGNIGY_API_KEY}`,
'Content-Type': 'application/json',
},
});
const datadog = axios.create({
baseURL: 'https://api.datadoghq.com',
headers: {
'DD-API-KEY': process.env.DATADOG_API_KEY,
'DD-APPLICATION-KEY': process.env.DATADOG_APP_KEY,
'Content-Type': 'application/json',
},
});
async function retryWithBackoff<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
const status = error.response?.status;
if (status === 429 || (status >= 500 && status < 600)) {
const delay = Math.pow(2, attempt) * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
throw new Error('Maximum retry attempts exceeded');
}
async function updateCognigyVariables(sessionId: string, variables: Record<string, string>) {
return retryWithBackoff(() => cognigy.post(`/api/v1/sessions/${sessionId}/variables`, { variables }));
}
async function routeToDialogNode(sessionId: string, targetNode: string) {
return retryWithBackoff(() =>
cognigy.post(`/api/v1/sessions/${sessionId}/dialog`, { type: 'dialog', dialog: targetNode })
);
}
async function logMetric(metricName: string, value: number, tags: string[]) {
const timestamp = Math.floor(Date.now() / 1000);
const payload = { series: [{ metric: metricName, type: 'count', points: [[timestamp, value]], tags }] };
try {
await retryWithBackoff(() => datadog.post('/api/v1/series', payload));
} catch (error: any) {
console.error(`Failed to log metric ${metricName}:`, error.response?.data || error.message);
}
}
app.post('/cognigy/payment-init', async (req, res) => {
const { sessionId, payload } = req.body;
if (!payload?.items?.length) return res.status(400).json({ error: 'Missing cart items' });
const idempotencyKey = `cognigy_${sessionId}_${uuidv4()}`;
try {
const session = await stripe.checkout.sessions.create(
{
line_items: payload.items.map((item: any) => ({ price: item.priceId, quantity: item.quantity })),
mode: 'payment',
success_url: `${process.env.COGNIGY_BASE_URL}/api/v1/sessions/${sessionId}/dialog?type=dialog&dialog=payment_success`,
cancel_url: `${process.env.COGNIGY_BASE_URL}/api/v1/sessions/${sessionId}/dialog?type=dialog&dialog=payment_cancelled`,
metadata: { cognigy_session_id: sessionId },
},
{ idempotencyKey }
);
res.json({ success: true, url: session.url, sessionId: session.id });
} catch (error: any) {
if (error.status === 400) return res.status(400).json({ error: 'Invalid Stripe price or configuration' });
if (error.status === 429) return res.status(429).json({ error: 'Stripe rate limit exceeded' });
res.status(500).json({ error: 'Failed to create checkout session' });
}
});
app.post('/stripe-webhook', async (req, res) => {
const sig = req.headers['stripe-signature'] as string;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent((req as any).rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch (err: any) {
return res.status(400).json({ error: `Webhook signature verification failed: ${err.message}` });
}
const session = event.data.object as Stripe.Checkout.Session;
const cognigySessionId = session.metadata?.cognigy_session_id;
if (!cognigySessionId) return res.status(200).json({ skipped: true });
switch (event.type) {
case 'checkout.session.completed':
await updateCognigyVariables(cognigySessionId, { payment_status: 'completed', stripe_session_id: session.id });
await logMetric('cognigy.payment.success', 1, ['status:completed', `session:${cognigySessionId}`]);
break;
case 'checkout.session.expired':
case 'payment_intent.payment_failed':
await updateCognigyVariables(cognigySessionId, { payment_status: 'failed', stripe_session_id: session.id });
await logMetric('cognigy.payment.failed', 1, ['status:failed', `session:${cognigySessionId}`]);
await routeToDialogNode(cognigySessionId, 'payment_retry_node');
break;
}
res.json({ received: true });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Payment service running on port ${PORT}`));
Common Errors & Debugging
Error: 401 Unauthorized
- What causes it: The Cognigy.AI API key lacks required permissions, the Stripe secret key is invalid, or Datadog credentials are missing.
- How to fix it: Verify the Cognigy key has
session:writeanddialog:write. Regenerate Stripe keys if rotated. Confirm Datadog API and Application keys are correct and scoped to the correct organization. - Code showing the fix:
// Validate credentials at startup if (!process.env.COGNIGY_API_KEY || !process.env.STRIPE_SECRET_KEY) { throw new Error('Missing required environment variables'); }
Error: 400 Bad Request (Webhook Signature Verification Failed)
- What causes it: The raw request body is modified before signature verification, or the webhook secret does not match the endpoint in Stripe.
- How to fix it: Use Express middleware to preserve the raw body. Pass
req.rawBodytoconstructEventinstead ofreq.body. - Code showing the fix:
app.use(express.json({ verify: (req, res, buf) => { (req as any).rawBody = buf; } })); event = stripe.webhooks.constructEvent((req as any).rawBody, sig, endpointSecret);
Error: 429 Rate Limit Exceeded
- What causes it: Cognigy.AI or Datadog enforces request quotas. Rapid session updates or metric bursts trigger throttling.
- How to fix it: Implement exponential backoff. The
retryWithBackoffhelper handles automatic retries with increasing delays. - Code showing the fix:
// Already implemented in retryWithBackoff helper const delay = Math.pow(2, attempt) * 1000; await new Promise((resolve) => setTimeout(resolve, delay));
Error: 502/503 Service Unavailable
- What causes it: Temporary outage on Cognigy.AI infrastructure or Stripe webhook delivery failure.
- How to fix it: Retry transient errors. Stripe automatically retries webhook deliveries up to three times over 30 minutes. The backoff helper catches 5xx responses and retries safely.
- Code showing the fix:
if (status >= 500 && status < 600) { const delay = Math.pow(2, attempt) * 1000; await new Promise((resolve) => setTimeout(resolve, delay)); continue; }