docs/platform/integrations/chat/webex-messaging.mdx
The Webex Messaging integration lets your application send Novu workflow notifications to Webex rooms and direct conversations. Your users authorize a Webex integration through OAuth, and Novu stores the resulting connection securely, refreshes its access token when needed, and routes each message to the room or person endpoint you register.
<CardGroup cols={2}> <Card title="Send to rooms" icon="messages-square" href="#send-to-a-webex-room"> Deliver plain text or Markdown notifications to a room or an existing message thread. </Card> <Card title="Send direct messages" icon="user" href="#send-to-a-webex-person"> Link a subscriber through OAuth or identify the recipient by Webex person ID or email. </Card> </CardGroup>Webex integrations use OAuth 2.0 to act on behalf of an authorized Webex user. The user who authorizes the connection must be able to access the rooms and people you want to target.
<CodeGroup>
```text title="US region"
https://api.novu.co/v1/integrations/chat/oauth/callback
```
```text title="EU region"
https://eu.api.novu.co/v1/integrations/chat/oauth/callback
```
</CodeGroup>

- `spark:messages_write`
- `spark:rooms_read`
- `spark:people_read`
- `spark:memberships_read`
Webex adds `spark:kms` to registered integrations. Novu includes it when generating the authorization URL because Webex requires it for encrypted message content.

Novu requests these scopes by default:
| Scope | How Novu uses it |
|---|---|
spark:messages_write | Send room and direct messages. |
spark:rooms_read | Allow your application to discover rooms before registering a room endpoint. |
spark:people_read | Resolve the authorized Webex person and support direct-message linking. |
spark:memberships_read | Allow your application to inspect room membership when choosing destinations. |
spark:kms | Access encrypted Webex content. Webex requires this scope for integrations that interact with messages. |
For more information, see Webex Integrations and Authorization.

- **Client ID**: The Webex integration client ID.
- **Client Secret**: The Webex integration client secret.
- **Redirect URL** (optional): Where Novu sends the browser after OAuth succeeds. If omitted, Novu displays a success page.
- **Base URL** (optional): Leave empty to use `https://webexapis.com/v1`.

Create a channel connection before registering room or person endpoints. Providing your own connectionIdentifier makes it easy to reference the connection later.
The following example creates a shared connection for the acme context. Call it from your backend when an administrator or authorized user clicks Connect Webex in your application.
const novu = new Novu({ secretKey: "<NOVU_SECRET_KEY>" });
const response = await novu.integrations.generateConnectOAuthUrl({ integrationIdentifier: 'webex-messaging', connectionIdentifier: 'webex-acme', connectionMode: 'shared', context: { tenant: 'acme', }, autoLinkUser: false, });
</Tab>
<Tab title="Python">
```python
import os
import novu_py
from novu_py import Novu
with Novu(secret_key=os.getenv("NOVU_SECRET_KEY", "")) as novu:
response = novu.integrations.generate_connect_o_auth_url(
generate_connect_oauth_url_request_dto={
"integration_identifier": "webex-messaging",
"connection_identifier": "webex-acme",
"connection_mode": novu_py.GenerateConnectOauthURLRequestDtoConnectionMode.SHARED,
"context": {
"tenant": "acme",
},
"auto_link_user": False,
}
)
novugo "github.com/novuhq/novu-go"
"github.com/novuhq/novu-go/models/components"
)
s := novugo.New(novugo.WithSecurity(os.Getenv("NOVU_SECRET_KEY")))
response, err := s.Integrations.GenerateConnectOAuthURL( context.Background(), components.GenerateConnectOauthURLRequestDto{ IntegrationIdentifier: "webex-messaging", ConnectionIdentifier: novugo.Pointer("webex-acme"), ConnectionMode: components.GenerateConnectOauthURLRequestDtoConnectionModeShared.ToPointer(), Context: map[string]components.GenerateConnectOauthURLRequestDtoContext{ "tenant": components.CreateGenerateConnectOauthURLRequestDtoContextStr("acme"), }, AutoLinkUser: novugo.Pointer(false), }, nil, )
</Tab>
<Tab title="PHP">
```php
use novu;
use novu\Models\Components;
$sdk = novu\Novu::builder()->setSecurity('<NOVU_SECRET_KEY>')->build();
$response = $sdk->integrations->generateConnectOAuthUrl(
generateConnectOauthUrlRequestDto: new Components\GenerateConnectOauthUrlRequestDto(
integrationIdentifier: 'webex-messaging',
connectionIdentifier: 'webex-acme',
context: [
'tenant' => 'acme',
],
connectionMode: Components\GenerateConnectOauthUrlRequestDtoConnectionMode::Shared,
autoLinkUser: false,
),
);
var sdk = new NovuSDK(secretKey: "<NOVU_SECRET_KEY>");
var response = await sdk.Integrations.GenerateConnectOAuthUrlAsync( generateConnectOauthUrlRequestDto: new GenerateConnectOauthUrlRequestDto() { IntegrationIdentifier = "webex-messaging", ConnectionIdentifier = "webex-acme", ConnectionMode = GenerateConnectOauthUrlRequestDtoConnectionMode.Shared, Context = new Dictionary<string, GenerateConnectOauthUrlRequestDtoContext>() { { "tenant", GenerateConnectOauthUrlRequestDtoContext.CreateStr("acme") }, }, AutoLinkUser = false, });
</Tab>
<Tab title="Java">
```java
import co.novu.Novu;
import co.novu.models.components.*;
import java.util.Map;
Novu novu = Novu.builder().secretKey("<NOVU_SECRET_KEY>").build();
var response = novu.integrations().generateConnectOAuthUrl()
.body(GenerateConnectOauthUrlRequestDto.builder()
.integrationIdentifier("webex-messaging")
.connectionIdentifier("webex-acme")
.connectionMode(GenerateConnectOauthUrlRequestDtoConnectionMode.SHARED)
.context(Map.ofEntries(
Map.entry("tenant", GenerateConnectOauthUrlRequestDtoContextUnion.of("acme"))))
.autoLinkUser(false)
.build())
.call();
Open the returned URL when the user is ready to authorize Webex:
window.open(response.result.url, '_blank');
After authorization, Novu exchanges the code, reads the authorized person's Webex organization, and stores the access and refresh tokens on the connection. Novu refreshes the access token automatically before message delivery when it is close to expiring.
For a subscriber-scoped connection, set autoLinkUser to true. Novu creates the connection and a webex_person endpoint for the Webex person who completes OAuth.
const response = await novu.integrations.generateConnectOAuthUrl({
integrationIdentifier: 'webex-messaging',
connectionIdentifier: 'webex-user-123',
connectionMode: 'subscriber',
subscriberId: 'user-123',
autoLinkUser: true,
});
Use a separate link-user flow when the Webex organization is already connected and another subscriber needs a personal endpoint.
<Tabs> <Tab title="Node.js"> ```typescript import { Novu } from '@novu/api';const novu = new Novu({ secretKey: "<NOVU_SECRET_KEY>" });
const response = await novu.integrations.generateLinkUserOAuthUrl({ integrationIdentifier: 'webex-messaging', subscriberId: 'user-123', connectionIdentifier: 'webex-acme', context: { tenant: 'acme', }, });
</Tab>
<Tab title="Python">
```python
import os
from novu_py import Novu
with Novu(secret_key=os.getenv("NOVU_SECRET_KEY", "")) as novu:
response = novu.integrations.generate_link_user_o_auth_url(
generate_link_user_oauth_url_request_dto={
"integration_identifier": "webex-messaging",
"subscriber_id": "user-123",
"connection_identifier": "webex-acme",
"context": {
"tenant": "acme",
},
}
)
novugo "github.com/novuhq/novu-go"
"github.com/novuhq/novu-go/models/components"
)
s := novugo.New(novugo.WithSecurity(os.Getenv("NOVU_SECRET_KEY")))
response, err := s.Integrations.GenerateLinkUserOAuthURL( context.Background(), components.GenerateLinkUserOauthURLRequestDto{ IntegrationIdentifier: "webex-messaging", SubscriberID: "user-123", ConnectionIdentifier: novugo.Pointer("webex-acme"), Context: map[string]components.GenerateLinkUserOauthURLRequestDtoContext{ "tenant": components.CreateGenerateLinkUserOauthURLRequestDtoContextStr("acme"), }, }, nil, )
</Tab>
<Tab title="PHP">
```php
use novu;
use novu\Models\Components;
$sdk = novu\Novu::builder()->setSecurity('<NOVU_SECRET_KEY>')->build();
$response = $sdk->integrations->generateLinkUserOAuthUrl(
generateLinkUserOauthUrlRequestDto: new Components\GenerateLinkUserOauthUrlRequestDto(
subscriberId: 'user-123',
integrationIdentifier: 'webex-messaging',
connectionIdentifier: 'webex-acme',
context: [
'tenant' => 'acme',
],
),
);
var sdk = new NovuSDK(secretKey: "<NOVU_SECRET_KEY>");
var response = await sdk.Integrations.GenerateLinkUserOAuthUrlAsync( generateLinkUserOauthUrlRequestDto: new GenerateLinkUserOauthUrlRequestDto() { IntegrationIdentifier = "webex-messaging", SubscriberId = "user-123", ConnectionIdentifier = "webex-acme", Context = new Dictionary<string, GenerateLinkUserOauthUrlRequestDtoContext>() { { "tenant", GenerateLinkUserOauthUrlRequestDtoContext.CreateStr("acme") }, }, });
</Tab>
<Tab title="Java">
```java
import co.novu.Novu;
import co.novu.models.components.*;
import java.util.Map;
Novu novu = Novu.builder().secretKey("<NOVU_SECRET_KEY>").build();
var response = novu.integrations().generateLinkUserOAuthUrl()
.body(GenerateLinkUserOauthUrlRequestDto.builder()
.integrationIdentifier("webex-messaging")
.subscriberId("user-123")
.connectionIdentifier("webex-acme")
.context(Map.ofEntries(
Map.entry("tenant", GenerateLinkUserOauthUrlRequestDtoContextUnion.of("acme"))))
.build())
.call();
The subscriber signs in to Webex through the returned URL. Novu reads their identity from people/me and creates a webex_person endpoint linked to the existing connection.
Every Webex endpoint must reference a Webex Messaging connection through connectionIdentifier. If the connection has a Novu context, the endpoint must use the same context.
Use the Webex Rooms API or a room picker in your application to obtain the room ID. The authorizing Webex user must be a member of the room.
Create a webex_room endpoint for the subscriber and context that should receive messages:
with Novu(secret_key=os.getenv("NOVU_SECRET_KEY", "")) as novu: novu.channel_endpoints.create(request_body={ "identifier": "webex-acme-alerts", "integration_identifier": "webex-messaging", "connection_identifier": "webex-acme", "subscriber_id": "user-123", "context": { "tenant": "acme", }, "type": "webex_room", "endpoint": { "room_id": "<WEBEX_ROOM_ID>", }, })
</Tab>
<Tab title="Go">
```go
import (
"context"
"os"
novugo "github.com/novuhq/novu-go"
"github.com/novuhq/novu-go/models/components"
"github.com/novuhq/novu-go/models/operations"
)
s := novugo.New(novugo.WithSecurity(os.Getenv("NOVU_SECRET_KEY")))
response, err := s.ChannelEndpoints.Create(
context.Background(),
operations.CreateChannelEndpointsControllerCreateChannelEndpointRequestBodyWebexRoom(
components.CreateWebexRoomEndpointDto{
Identifier: novugo.Pointer("webex-acme-alerts"),
IntegrationIdentifier: "webex-messaging",
ConnectionIdentifier: "webex-acme",
SubscriberID: "user-123",
Context: map[string]components.CreateWebexRoomEndpointDtoContext{
"tenant": components.CreateCreateWebexRoomEndpointDtoContextStr("acme"),
},
Endpoint: components.WebexRoomEndpointDto{
RoomID: "<WEBEX_ROOM_ID>",
},
},
),
nil,
)
$sdk = novu\Novu::builder()->setSecurity('<NOVU_SECRET_KEY>')->build();
$sdk->channelEndpoints->create( requestBody: new Components\CreateWebexRoomEndpointDto( identifier: 'webex-acme-alerts', integrationIdentifier: 'webex-messaging', connectionIdentifier: 'webex-acme', subscriberId: 'user-123', context: [ 'tenant' => 'acme', ], type: Components\CreateWebexRoomEndpointDtoType::WebexRoom, endpoint: new Components\WebexRoomEndpointDto( roomId: '<WEBEX_ROOM_ID>', ), ), );
</Tab>
<Tab title=".NET">
```csharp
using Novu;
using Novu.Models.Components;
using Novu.Models.Requests;
using System.Collections.Generic;
var sdk = new NovuSDK(secretKey: "<NOVU_SECRET_KEY>");
await sdk.ChannelEndpoints.CreateAsync(
requestBody: ChannelEndpointsControllerCreateChannelEndpointRequestBody.CreateWebexRoom(
new CreateWebexRoomEndpointDto() {
Identifier = "webex-acme-alerts",
IntegrationIdentifier = "webex-messaging",
ConnectionIdentifier = "webex-acme",
SubscriberId = "user-123",
Context = new Dictionary<string, CreateWebexRoomEndpointDtoContext>() {
{ "tenant", CreateWebexRoomEndpointDtoContext.CreateStr("acme") },
},
Endpoint = new WebexRoomEndpointDto() {
RoomId = "<WEBEX_ROOM_ID>",
},
}));
Novu novu = Novu.builder().secretKey("<NOVU_SECRET_KEY>").build();
novu.channelEndpoints().create() .requestBody(CreateWebexRoomEndpointDto.builder() .identifier("webex-acme-alerts") .integrationIdentifier("webex-messaging") .connectionIdentifier("webex-acme") .subscriberId("user-123") .context(Map.ofEntries( Map.entry("tenant", CreateWebexRoomEndpointDtoContextUnion.of("acme")))) .type(CreateWebexRoomEndpointDtoType.WEBEX_ROOM) .endpoint(WebexRoomEndpointDto.builder() .roomId("<WEBEX_ROOM_ID>") .build()) .build()) .call();
</Tab>
<Tab title="cURL">
```bash
curl -L -X POST 'https://api.novu.co/v1/channel-endpoints' \
-H 'Content-Type: application/json' \
-H 'Authorization: ApiKey <NOVU_SECRET_KEY>' \
-d '{
"identifier": "webex-acme-alerts",
"integrationIdentifier": "webex-messaging",
"connectionIdentifier": "webex-acme",
"subscriberId": "user-123",
"context": {
"tenant": "acme"
},
"type": "webex_room",
"endpoint": {
"roomId": "<WEBEX_ROOM_ID>"
}
}'
To send every notification from an endpoint as a reply in an existing Webex thread, add the parent message ID:
{
"type": "webex_room",
"endpoint": {
"roomId": "<WEBEX_ROOM_ID>",
"parentId": "<WEBEX_PARENT_MESSAGE_ID>"
}
}
The recommended approach is to use autoLinkUser or the link-user OAuth flow. These flows identify the person who signs in and create the endpoint automatically.
You can also create a webex_person endpoint manually. Provide exactly one of personId or personEmail.
with Novu(secret_key=os.getenv("NOVU_SECRET_KEY", "")) as novu: novu.channel_endpoints.create(request_body={ "identifier": "webex-user-123", "integration_identifier": "webex-messaging", "connection_identifier": "webex-acme", "subscriber_id": "user-123", "context": { "tenant": "acme", }, "type": "webex_person", "endpoint": { "person_email": "[email protected]", }, })
</Tab>
<Tab title="Go">
```go
import (
"context"
"os"
novugo "github.com/novuhq/novu-go"
"github.com/novuhq/novu-go/models/components"
"github.com/novuhq/novu-go/models/operations"
)
s := novugo.New(novugo.WithSecurity(os.Getenv("NOVU_SECRET_KEY")))
response, err := s.ChannelEndpoints.Create(
context.Background(),
operations.CreateChannelEndpointsControllerCreateChannelEndpointRequestBodyWebexPerson(
components.CreateWebexPersonEndpointDto{
Identifier: novugo.Pointer("webex-user-123"),
IntegrationIdentifier: "webex-messaging",
ConnectionIdentifier: "webex-acme",
SubscriberID: "user-123",
Context: map[string]components.CreateWebexPersonEndpointDtoContext{
"tenant": components.CreateCreateWebexPersonEndpointDtoContextStr("acme"),
},
Endpoint: components.WebexPersonEndpointDto{
PersonEmail: novugo.Pointer("[email protected]"),
},
},
),
nil,
)
$sdk = novu\Novu::builder()->setSecurity('<NOVU_SECRET_KEY>')->build();
$sdk->channelEndpoints->create( requestBody: new Components\CreateWebexPersonEndpointDto( identifier: 'webex-user-123', integrationIdentifier: 'webex-messaging', connectionIdentifier: 'webex-acme', subscriberId: 'user-123', context: [ 'tenant' => 'acme', ], type: Components\CreateWebexPersonEndpointDtoType::WebexPerson, endpoint: new Components\WebexPersonEndpointDto( personEmail: '[email protected]', ), ), );
</Tab>
<Tab title=".NET">
```csharp
using Novu;
using Novu.Models.Components;
using Novu.Models.Requests;
using System.Collections.Generic;
var sdk = new NovuSDK(secretKey: "<NOVU_SECRET_KEY>");
await sdk.ChannelEndpoints.CreateAsync(
requestBody: ChannelEndpointsControllerCreateChannelEndpointRequestBody.CreateWebexPerson(
new CreateWebexPersonEndpointDto() {
Identifier = "webex-user-123",
IntegrationIdentifier = "webex-messaging",
ConnectionIdentifier = "webex-acme",
SubscriberId = "user-123",
Context = new Dictionary<string, CreateWebexPersonEndpointDtoContext>() {
{ "tenant", CreateWebexPersonEndpointDtoContext.CreateStr("acme") },
},
Endpoint = new WebexPersonEndpointDto() {
PersonEmail = "[email protected]",
},
}));
Novu novu = Novu.builder().secretKey("<NOVU_SECRET_KEY>").build();
novu.channelEndpoints().create() .requestBody(CreateWebexPersonEndpointDto.builder() .identifier("webex-user-123") .integrationIdentifier("webex-messaging") .connectionIdentifier("webex-acme") .subscriberId("user-123") .context(Map.ofEntries( Map.entry("tenant", CreateWebexPersonEndpointDtoContextUnion.of("acme")))) .type(CreateWebexPersonEndpointDtoType.WEBEX_PERSON) .endpoint(WebexPersonEndpointDto.builder() .personEmail("[email protected]") .build()) .build()) .call();
</Tab>
<Tab title="cURL">
```bash
curl -L -X POST 'https://api.novu.co/v1/channel-endpoints' \
-H 'Content-Type: application/json' \
-H 'Authorization: ApiKey <NOVU_SECRET_KEY>' \
-d '{
"identifier": "webex-user-123",
"integrationIdentifier": "webex-messaging",
"connectionIdentifier": "webex-acme",
"subscriberId": "user-123",
"context": {
"tenant": "acme"
},
"type": "webex_person",
"endpoint": {
"personEmail": "[email protected]"
}
}'
Create a workflow with a Chat step and select the Webex Messaging integration. After the matching connection and endpoint exist, trigger the workflow with the same subscriber and context.
<Tabs> <Tab title="Node.js"> ```typescript import { Novu } from '@novu/api';const novu = new Novu({ secretKey: "<NOVU_SECRET_KEY>" });
await novu.trigger({ workflowId: 'incident-alert', to: { subscriberId: 'user-123', }, context: { tenant: 'acme', }, payload: { service: 'checkout', status: 'degraded', }, });
</Tab>
<Tab title="Python">
```python
import os
import novu_py
from novu_py import Novu
with Novu(secret_key=os.getenv("NOVU_SECRET_KEY", "")) as novu:
novu.trigger(trigger_event_request_dto=novu_py.TriggerEventRequestDto(
workflow_id="incident-alert",
to={"subscriber_id": "user-123"},
context={
"tenant": "acme",
},
payload={
"service": "checkout",
"status": "degraded",
},
))
novugo "github.com/novuhq/novu-go"
"github.com/novuhq/novu-go/models/components"
)
s := novugo.New(novugo.WithSecurity(os.Getenv("NOVU_SECRET_KEY")))
response, err := s.Trigger(context.Background(), components.TriggerEventRequestDto{ WorkflowID: "incident-alert", To: components.CreateToStr("user-123"), Context: map[string]components.TriggerEventRequestDtoContext{ "tenant": components.CreateTriggerEventRequestDtoContextStr("acme"), }, Payload: map[string]any{ "service": "checkout", "status": "degraded", }, }, nil)
</Tab>
<Tab title="PHP">
```php
use novu;
use novu\Models\Components;
$sdk = novu\Novu::builder()->setSecurity('<NOVU_SECRET_KEY>')->build();
$sdk->trigger(
triggerEventRequestDto: new Components\TriggerEventRequestDto(
workflowId: 'incident-alert',
to: 'user-123',
context: [
'tenant' => 'acme',
],
payload: [
'service' => 'checkout',
'status' => 'degraded',
],
),
);
var sdk = new NovuSDK(secretKey: "<NOVU_SECRET_KEY>");
await sdk.TriggerAsync(triggerEventRequestDto: new TriggerEventRequestDto() { WorkflowId = "incident-alert", To = To.CreateStr("user-123"), Context = new Dictionary<string, TriggerEventRequestDtoContext>() { { "tenant", TriggerEventRequestDtoContext.CreateStr("acme") }, }, Payload = new Dictionary<string, object>() { { "service", "checkout" }, { "status", "degraded" }, }, });
</Tab>
<Tab title="Java">
```java
import co.novu.Novu;
import co.novu.models.components.*;
import java.util.Map;
Novu novu = Novu.builder().secretKey("<NOVU_SECRET_KEY>").build();
novu.trigger()
.body(TriggerEventRequestDto.builder()
.workflowId("incident-alert")
.to(To2.of("user-123"))
.context(Map.ofEntries(
Map.entry("tenant", TriggerEventRequestDtoContextUnion.of("acme"))))
.payload(Map.ofEntries(
Map.entry("service", "checkout"),
Map.entry("status", "degraded")))
.build())
.call();
When the workflow runs, Novu resolves matching Webex endpoints, obtains a valid access token from the referenced connection, and posts the Chat step body to the Webex Messages API.
The dashboard Chat step sends its body as Webex text. Use trigger overrides when you need Webex-specific message fields such as markdown, files, or attachments.
The example below adds Webex Markdown to a specific Chat step:
await novu.trigger({
workflowId: 'incident-alert',
to: {
subscriberId: 'user-123',
},
context: {
tenant: 'acme',
},
payload: {
service: 'checkout',
status: 'degraded',
},
overrides: {
steps: {
'webex-chat-step': {
providers: {
'webex-messaging': {
_passthrough: {
body: {
markdown: '**Checkout** is currently _degraded_.',
},
},
},
},
},
},
},
});
Replace webex-chat-step with the Chat step identifier from your workflow.
For supported Webex payload fields, see Create a Message.
| Error or symptom | What to check |
|---|---|
| OAuth fails with an invalid redirect URI | Confirm the Webex integration contains the Novu callback for the correct region. The value must match exactly. |
WEBEX_INVALID_CREDENTIALS | Reconnect Webex. The access or refresh token may be invalid or expired. |
WEBEX_INSUFFICIENT_PERMISSIONS | Confirm the required scopes were selected in Webex and requested during OAuth. |
WEBEX_DESTINATION_NOT_FOUND | Verify the room or person identifier and confirm the authorizing user can access that destination. |
| Endpoint context mismatch | Use the same Novu context values on the endpoint and its referenced channel connection. |
WEBEX_RATE_LIMITED | Wait for the retry-after interval reported by Webex before trying again. |