How to Integrate Microsoft Teams with the Pega Digital Messaging Client Channel API
Pega Digital Messaging supports a number of channels out of the box, including Web Messaging, WhatsApp, Facebook Messenger, and SMS. Microsoft Teams is not one of them.
That does not mean Teams is off the table. Pega provides the Client Channel API, which is the supported extension point for bringing any conversational surface into Digital Messaging. This article walks through a complete Microsoft Teams integration end to end: what the Client Channel API actually is, why the pattern requires you to host a server, and every configuration step on both the Microsoft and Pega sides.
This is a build, not a toggle. Budget accordingly, and read the section on hosting before you commit to a timeline.
1. What is the Client Channel API?
The Client Channel API was introduced in Pega Customer Service 8.7 to let customers connect channels that Digital Messaging does not natively support, such as Microsoft Teams, Cisco Webex, Slack, or a proprietary in-house messaging surface.
It is important to set expectations correctly here. The Client Channel API is not a connector. It is a published JSON contract. Pega gives you a documented message schema, two webhook endpoints, and an authentication scheme. You supply an integration layer that speaks that contract on one side and speaks your channel’s native API on the other.
1.1 The two directions
Every Client Channel API integration is built around two webhooks pointing in opposite directions.
| Direction | Endpoint | Who owns it | What flows through it |
|---|---|---|---|
| Inbound to Pega | Digital Messaging Webhook | Pega generates it for you | Customer messages, typing indicators, and session-end events that your integration layer POSTs to Digital Messaging Service |
| Outbound from Pega | Client Webhook | You create and host it | Agent and bot replies, menus, carousels, link buttons, typing indicators, and session-end events that Digital Messaging Service POSTs to you |
Both are ordinary HTTPS POST endpoints exchanging JSON. There is no persistent socket and no SDK running inside Pega.
1.2 Message types on the contract
Your integration layer sends these payload types to Digital Messaging Service:
text(with optional attachments, postback values, and context data)typing_indicatorcustomer_end_session
Your integration layer receives these payload types from Digital Messaging Service:
text(with optional attachments including content type, file name, and size)menu(a title plus a list of selectable items, each with a text label and a postback payload)carousel(a set of cards, each with its own nested item list)link_button(a title, label, and URL)typing_indicatorcsr_end_session
Menus and carousels are the part teams usually underestimate. Digital Messaging Service will send them whenever your bot or agent uses those response types, and you are responsible for rendering them in something Teams understands. Plan for that mapping up front rather than discovering it in UAT.
1.3 Authentication
Authentication uses a JSON Web Token that your integration layer signs itself. There is no token endpoint to call and no token that Pega hands you to reuse.
You are given two values when you create the connection:
- Connection Id, which is used as the JWT issuer (
iss) claim - JWT Secret, which is the signing key
You sign a JWT with those values and present it as a bearer token on every request:
const jwt = require("jsonwebtoken");
const token = jwt.sign({ iss: process.env.DMS_CONNECTION_ID }, process.env.DMS_JWT_SECRET);
// Then send: Authorization: Bearer YOUR_TOKEN
Two constraints matter operationally:
- Each token is valid for five minutes from issue, and that window is fixed. Sign per request. Do not cache a token for the lifetime of the process.
- The JWT Secret is a long-lived credential. Treat it like a database password, not like a config value.
1.4 Response codes
Digital Messaging Service returns 200 on successful delivery. Failures come back as:
400Malformed payload, typically an invalid message type403Invalid or expired JWT404API endpoint not found422Unprocessable request, typically missing mandatory fields or invalid JSON
A steady trickle of 403 responses in production almost always means token caching, not a bad secret.
2. Why you need to host a server
This is the single most important planning consideration, so it gets its own section.
Microsoft Teams does not speak the Digital Messaging JSON contract, and Digital Messaging Service does not speak the Bot Framework Activity schema. Neither side will adapt to the other. Something has to sit in the middle, translate in both directions, and be reachable by both. That something is a server that you build, deploy, and operate. Pega does not host it, and Microsoft does not host it.
2.1 What the server is actually responsible for
Publicly reachable HTTPS. Azure Bot Service posts inbound Teams activity to your server, and Digital Messaging Service posts agent replies to your server. Both require a publicly resolvable endpoint with a valid TLS certificate. A server behind a corporate firewall with no ingress path will not work. For local development you can front localhost with a reverse proxy tunnel such as ngrok, but that is a development convenience only.
Two inbound routes. One route receives Teams activity, conventionally /api/messages. A second route receives the Digital Messaging Client Webhook. Keep them separate, because they authenticate differently.
Conversation state. This is the requirement most proofs of concept get wrong. Digital Messaging Service identifies a conversation by customer_id. Teams requires a full ConversationReference object to send a message back into an existing chat. Your server must store the mapping between the two and keep it available for the life of the conversation. An in-memory map is fine for a demo and will fail the moment you run more than one instance or restart the process. Use Redis, a database, or another durable store for anything real.
JWT signing on every outbound call. See section 1.3.
Secret management. The server holds a Microsoft client secret and a Pega JWT Secret. Both belong in a managed secret store, not in source control.
Availability. Digital Messaging Service will POST to your Client Webhook whenever an agent types. If your host has scaled to zero, that request may time out and the customer sees a dropped conversation. Prefer an always-warm deployment. If you use serverless functions, understand the cold start and state implications before you commit.
Observability. You are now the middle of a three-party conversation. Structured logging with correlation on customer_id and message_id will save you considerable time during UAT.
2.2 Hosting options
Any platform that runs a long-lived Node process behind HTTPS will work. Common choices:
| Option | Notes |
|---|---|
| Azure App Service | Natural fit given the Microsoft side of the integration, and simple managed identity story |
| AWS ECS or Fargate | Good control over always-on behavior and networking |
| AWS App Runner | Lower operational overhead than ECS for a single service |
| Render or Heroku | Fastest to stand up, appropriate for pilots, check the cold start behavior on lower tiers |
| Your own infrastructure | Viable if you can publish an HTTPS ingress path and terminate TLS with a trusted certificate |
Serverless functions are workable but require deliberate handling of both cold starts and externalized conversation state.
2.3 Architecture
The flow is:
- A user sends a message in Microsoft Teams.
- Azure Bot Service forwards the activity to your messaging endpoint.
- Your integration layer translates the activity into Digital Messaging JSON and POSTs it to the Digital Messaging Webhook with a signed JWT.
- Digital Messaging Service routes the message to your Pega Customer Service application, where it is queued and assigned per your routing rules.
- The agent or bot responds in Pega.
- Digital Messaging Service POSTs that response to your Client Webhook.
- Your integration layer looks up the stored conversation reference and delivers the message back into the Teams chat.
3. Prerequisites
Before starting, confirm you have all of the following.
On the Pega side
- Pega Customer Service 8.7 or later with a Digital Messaging channel interface configured.
- Digital Messaging Service enabled for your application. Digital Messaging Service is hosted on Pega Cloud, and the credentials that enable it are provisioned by Pega. If your application is not yet enabled, contact your Pega account team to begin that process.
- Access to Digital Messaging Manager for the channel interface.
On the Microsoft side
- A Microsoft 365 tenant with Teams.
- Sufficient permission to register a Teams app in the Developer Portal. In many organizations this requires a directory role such as Application Developer in Microsoft Entra ID. If you do not hold it, your Teams or identity administrator can either grant it or register the app on your behalf. Microsoft documents the role in the Entra built-in roles reference: Microsoft Entra Built-in Roles - Microsoft Entra ID | Microsoft Learn.
- A Teams administrator who can approve an app published to your organization.
For the integration layer
- Node.js 18 LTS or later.
- A hosting target that meets the requirements in section 2.
- A durable store for conversation state if you are going beyond a demo.
4. Step 1: Create the Teams app and bot
Sign in to the Microsoft Teams Developer Portal at https://dev.teams.microsoft.com/ with your organizational account.
4.1 Create the app
Select Apps in the left navigation, then New app.
Complete the basic information. The fields that matter downstream are the app name, the short and long descriptions, developer information, and the privacy policy and terms of use URLs. Microsoft validates that the URLs are well formed, and org publishing will fail without them. An application ID is generated for you.
Save the app.
4.2 Add the bot feature
Within the app, go to Configure, then App features, and add the Bot feature.
Configure the bot:
- Bot name, which is what users see in the Teams client.
- Messaging endpoint, which is your integration layer URL plus
/api/messages. You will not have this yet if you have not deployed. Enter a placeholder and return to update it after section 7. - Scopes, selecting Personal for one-to-one support conversations, and Team or Group chat only if you genuinely need them.
4.3 Record the credentials
The portal generates a Bot ID, which is also referred to as the App ID or Client ID. Under Client secrets, choose to add a client secret for your bot.
Copy both values immediately:
- Bot ID / App ID, a GUID in the form
00000000-0000-0000-0000-000000000000 - Client secret, which is displayed only once
Store them in your secret manager now. If you lose the secret you will need to generate a new one.
5. Step 2: Publish the app to your organization
From the app overview, go to Publish, then Publish to org, and select Publish your app.
This submits the app for approval by your organization’s Teams administrator. Until it is approved, the app will not appear in the Teams client for your users. Approval happens in the Microsoft Teams admin center under Teams apps, then Manage apps.
Once approved, users install it like any other app. In the Teams client, open Apps, search for the bot by name, and select Add. The bot then appears in the chat list.
If publishing fails, see section 9.
6. Step 3: Create the Client Channel API connection in Pega
Now configure the Pega side.
- Log in to your Pega application.
- In App Studio, open Channels, select your Digital Messaging channel interface, and choose Manage connections to open Digital Messaging Manager.
- Add a new connection and select Client Channel API.
- Give the connection a meaningful name, such as
Microsoft Teams. - Set the Client Webhook to the endpoint on your integration layer that will receive messages from Digital Messaging Service, for example https://your-integration-layer.example.com/dms/webhook. As with the Teams messaging endpoint, you may enter a placeholder now and update it after deployment.
- Save the connection.
Digital Messaging Manager now displays the values your integration layer needs.
Record all three:
| Value | Used as |
|---|---|
| Connection Id (JWT issuer) | The iss claim in the JWT you sign, and the channel identifier in the Node package configuration |
| Digital Messaging Webhook | The URL your integration layer POSTs customer messages to |
| JWT Secret | The key you sign the JWT with |
Treat the JWT Secret as a production credential from this moment forward.
7. Step 4: Build the integration layer
Pega publishes an official Node.js package, dms-client-channel: https://www.npmjs.com/package/dms-client-channel, which handles the Digital Messaging side of the contract: JWT signing, payload validation, and callbacks for each inbound message type. Use it rather than hand-rolling the contract.
7.1 Install dependencies
npm init -y
npm install botbuilder dms-client-channel express dotenv
A note on package selection. Some older examples reference botbuilder-adapter-teams, which belongs to the Botkit family. For a new build, use the current botbuilder v4 SDK with CloudAdapter, which is what Microsoft supports today.
7.2 Configure environment variables
# Microsoft Teams bot
MICROSOFT_APP_ID=00000000-0000-0000-0000-000000000000
MICROSOFT_APP_PASSWORD=your-client-secret
MICROSOFT_APP_TYPE=MultiTenant
# Pega Digital Messaging
DMS_CONNECTION_ID=your-connection-id
DMS_JWT_SECRET=your-jwt-secret
DMS_WEBHOOK_URL=https://incoming.REGION.pega.digital/messages
# Server
PORT=3978
Use the exact Digital Messaging Webhook value shown in Digital Messaging Manager rather than constructing the URL yourself.
7.3 Initialize both sides
require("dotenv").config();
const express = require("express");
const {
CloudAdapter,
ConfigurationServiceClientCredentialFactory,
createBotFrameworkAuthenticationFromConfiguration,
TurnContext,
} = require("botbuilder");
const dmsClientChannel = require("dms-client-channel");
// Microsoft Teams side
const credentialsFactory = new ConfigurationServiceClientCredentialFactory({
MicrosoftAppId: process.env.MICROSOFT_APP_ID,
MicrosoftAppPassword: process.env.MICROSOFT_APP_PASSWORD,
MicrosoftAppType: process.env.MICROSOFT_APP_TYPE,
});
const adapter = new CloudAdapter(
createBotFrameworkAuthenticationFromConfiguration(null, credentialsFactory)
);
// Pega Digital Messaging side
const dms = dmsClientChannel({
JWT_SECRET: process.env.DMS_JWT_SECRET,
CHANNEL_ID: process.env.DMS_CONNECTION_ID,
API_URL: process.env.DMS_WEBHOOK_URL,
});
const app = express();
app.use(express.json());
Check the package README for the exact initialization signature, since it has changed across versions.
7.4 Store the conversation reference
// Demo only. Use Redis or a database in production.
const conversations = new Map();
To restate section 2.1: without this mapping, messages flow from Teams into Pega and agent replies have nowhere to go. This is the most common cause of a “half working” integration.
7.5 Teams to Pega
app.post("/api/messages", (req, res) => {
adapter.process(req, res, async (context) => {
const activity = context.activity;
if (activity.type !== "message") return;
const customerId = activity.from.aadObjectId || activity.from.id;
// Save the reference so replies can be delivered later.
conversations.set(customerId, TurnContext.getConversationReference(activity));
const dmsMessage = {
type: "text",
customer_id: customerId,
customer_name: activity.from.name,
message_id: activity.id,
text: [activity.text],
};
dms.sendMessage(dmsMessage, (response) => {
if (response.status !== 200) {
console.error("DMS rejected message", response.status, response.statusText);
}
});
});
});
Use a stable identifier for customer_id. In Teams, from.aadObjectId is stable for a given user in your tenant, which is usually what you want for conversation continuity.
7.6 Pega to Teams
The package exposes a callback for each inbound message type. Assign the ones you intend to support:
async function sendToTeams(customerId, buildActivity) {
const reference = conversations.get(customerId);
if (!reference) {
console.warn("No conversation reference for", customerId);
return;
}
await adapter.continueConversationAsync(
process.env.MICROSOFT_APP_ID,
reference,
async (context) => buildActivity(context)
);
}
// Agent or bot text
dms.onTextMessage = async (message) => {
// Depending on version, text arrives as a string or an array of strings.
const body = Array.isArray(message.text) ? message.text.join("\n") : message.text;
const prefix = message.csr_name ? `**${message.csr_name}:** ` : "";
await sendToTeams(message.customer_id, (context) =>
context.sendActivity(`${prefix}${body}`)
);
};
// Menus map cleanly onto Teams suggested actions
dms.onMenuMessage = async (message) => {
await sendToTeams(message.customer_id, (context) =>
context.sendActivity({
text: message.title,
suggestedActions: {
actions: message.items.map((item) => ({
type: "messageBack",
title: item.text,
text: item.text,
value: item.payload,
})),
to: [],
},
})
);
};
// Carousels are best rendered as Adaptive Cards
dms.onCarouselMessage = async (message) => {
/* map message.items to an Adaptive Card carousel */
};
dms.onUrlLinkMessage = async (message) => {
await sendToTeams(message.customer_id, (context) =>
context.sendActivity(`[${message.label}](${message.url})`)
);
};
dms.onTypingIndicator = async (customerId) => {
await sendToTeams(customerId, (context) => context.sendActivity({ type: "typing" }));
};
dms.onCsrEndSession = async (customerId) => {
await sendToTeams(customerId, (context) =>
context.sendActivity("This conversation has ended. Send a message to start a new one.")
);
conversations.delete(customerId);
};
When the customer selects a menu item, Teams returns the value you set as messageBack data. Send that back to Digital Messaging Service in the postback field rather than in text, so Pega treats it as a menu selection instead of free text.
7.7 Mount the Client Webhook route
Finally, expose the route you registered as the Client Webhook in Digital Messaging Manager and hand the request to the package. Refer to express_endpoints_example.js in the package’s examples folder for the exact handler wiring, then start the server:
app.listen(process.env.PORT || 3978, () => {
console.log(`Listening on ${process.env.PORT || 3978}`);
});
8. Step 5: Deploy and connect the endpoints
- Deploy the integration layer to your chosen host and confirm it is reachable over HTTPS from the public internet.
- In the Teams Developer Portal, set the bot’s Messaging endpoint to https://your-host/api/messages and save.
- In Digital Messaging Manager, set the Client Webhook on your Client Channel API connection to https://your-host/dms/webhook and save.
Both sides must point at the deployed URL. A placeholder left in either place produces a one-way conversation.
Testing end to end
Work outward from the smallest loop:
- Bot reachability. Use Preview in Teams in the Developer Portal, or the web chat test in the bot configuration, and confirm your server logs the inbound activity.
- Outbound authentication. Send a message from Teams and confirm Digital Messaging Service returns
200. A403means the JWT is wrong or stale. - Arrival in Pega. Confirm the interaction appears in the agent desktop and routes to the expected queue.
- The return path. Reply as an agent and confirm the message arrives in the Teams chat. This is where a missing conversation reference shows up.
- Rich content. Test menus, carousels, link buttons, typing indicators, attachments, and agent-initiated session end.
- Restart resilience. Restart your server mid-conversation and confirm the conversation survives. If it does not, your conversation state is not durable.
9. Troubleshooting
UnableToParseTeamsAppManifest when publishing. The app manifest is not valid. Check that every required field is populated, including name, short and long description, and version; that the privacy policy and terms of use URLs are well formed absolute URLs; and that the manifest schema version is one Teams currently accepts. If you have been editing the manifest JSON by hand, validate the syntax.
AADSTS700016: Application with identifier ... was not found in the directory. The App ID presented at authentication does not resolve to a registered application. Confirm that MICROSOFT_APP_ID matches the Bot ID in the Developer Portal exactly, that MICROSOFT_APP_TYPE matches how the bot was registered, that the client secret has not expired, and that the app has been installed or consented to in the tenant.
403 Invalid/Expired JWT from Digital Messaging Service. Almost always token reuse. Sign a fresh JWT per request. Also confirm that the iss claim is the Connection Id and not the channel name, and that the JWT Secret was copied without truncation.
The bot does not appear in Teams. Confirm the app is approved in the Teams admin center, that the user is permitted to install custom or line-of-business apps, and that the app was published to the scope you expect.
Messages reach Pega but replies never reach Teams. Check three things in order: the Client Webhook in Digital Messaging Manager points at your deployed URL; your webhook route returns quickly with a success status; and you have a stored conversation reference for that customer_id. If you are running multiple instances with an in-memory map, replies will only work when the request happens to land on the instance that handled the original message.
Attachments fail to transfer. Attachment URLs on the contract are temporary and private. Download the file within your integration layer and re-upload it to the destination rather than passing the URL through.
10. Security considerations
- Keep the client secret and the JWT Secret in a managed secret store. Never commit them, and never log them.
- Let the Bot Framework adapter validate inbound requests on
/api/messages. Do not disable that validation to make local testing easier and then ship it. - Restrict who can reach your Client Webhook route as far as your host allows, and reject any payload that does not match the expected schema.
- Confirm with your Pega account team which Digital Messaging Service region your application is provisioned in if you have data residency obligations.
- If your integration layer touches sensitive customer data, review the Private Data API rather than putting that data in ordinary message text.
11. Reference
- Setting up the IVA for Client Channel API: Pegasystems Documentation
- Client Channel API payload requirements: https://docs-previous.pega.com/conversational-channels/87/client-channel-api-payload-requirements
dms-client-channelon npm: https://www.npmjs.com/package/dms-client-channel- Bot Framework SDK documentation: Azure AI Bot Service documentation - Bot Service | Microsoft Learn
- Publish Teams apps using the Developer Portal: Publish App using Developer Portal - Teams | Microsoft Learn
- Microsoft Entra built-in roles reference: Microsoft Entra Built-in Roles - Microsoft Entra ID | Microsoft Learn





