Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 31 additions & 16 deletions integrations/zendesk/hub.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,43 @@
Optimize your customer support workflow with the Zendesk integration for your chatbot. Seamlessly manage tickets, engage customers, and access critical information—all within your bot. Elevate your customer service game and improve internal processes by triggering automations from real-time ticket updates.
# Zendesk integration

Optimize your customer support workflow with the Zendesk integration for your
chatbot. Seamlessly manage tickets, engage customers, and access critical
information—all within your bot. Elevate your customer service game and improve
internal processes by triggering automations from real-time ticket updates.

> 🤝 **Usage with HITL (Human in the Loop)**
> If you intend to use the Zendesk integration with HITL, ensure that you have the HITL plugin installed.
> If you intend to use the Zendesk integration with HITL,
> ensure that you have the HITL plugin installed.

## ⚠️ migrating from 2.x.x to 3.x.x

## Installation and Configuration
Zendesk is tightening its security requirements on January 12th, 2026.
From that date forward,
all third-party integrations must authenticate exclusively through OAuth.
Because the 2.x.x version of the Botpress Zendesk integration relies on API
tokens rather than OAuth, it will be deprecated.

1. Navigate to the Zendesk Admin Center.
2. Activate the Zendesk API feature.
3. Proceed to Settings and choose the option to Enable Token Access.
4. Incorporate your API token. For more details on this, refer to the [API Token Documentation](https://developer.zendesk.com/api-reference/introduction/security-and-auth/#api-token)
The 3.x.x version introduces full OAuth support and is the required upgrade path.

### Usage
What changes in 3.x.x?

For this integration, you'll require both a username and a password. Ensure you append /token to the end of the specified username.
- Authentication now uses Zendesk OAuth instead of API tokens.
- Existing API-token-based connections will stop working once Zendesk
enforces the new policy.
- The integration settings UI has been updated to support OAuth app credentials.

For instance:
## Requirements

Username: `jdoe@example.com/token`
Password: `API_TOKEN`
- A Zendesk account

### Knowledge Base Sync

1. Toggle the "Sync Knowledge Base With Bot" option to start syncing.
2. Enter the ID of the desired knowledge base where your Zendesk articles will be stored.
3. Enable the integration to complete the setup.
1. Toggle the "Sync Knowledge Base With Bot" option to start syncing.
2. Enter the ID of the desired knowledge base where your Zendesk articles will
be stored.
3. Enable the integration to complete the setup.

Once these steps are completed, your Zendesk articles will automatically sync to the specified knowledge base in Botpress. You can manually sync by using the "Sync KB" action.
Once these steps are completed,
your Zendesk articles will automatically sync to the specified
knowledge base in Botpress.
You can manually sync by using the "Sync KB" action.
15 changes: 13 additions & 2 deletions integrations/zendesk/integration.definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { actions, events, configuration, channels, states, user } from './src/de
export default new sdk.IntegrationDefinition({
name: 'zendesk',
title: 'Zendesk',
version: '2.8.6',
version: '3.0.0',
icon: 'icon.svg',
description:
'Optimize your support workflow. Trigger workflows from ticket updates as well as manage tickets, access conversations, and engage with customers.',
Expand All @@ -17,7 +17,18 @@ export default new sdk.IntegrationDefinition({
user,
actions,
events,
secrets: sentryHelpers.COMMON_SECRET_NAMES,
secrets: {
...sentryHelpers.COMMON_SECRET_NAMES,
CLIENT_ID: {
description: 'The client ID of your app',
},
CLIENT_SECRET: {
description: 'The client secret of your app',
},
CODE_CHALLENGE: {
description: 'The code challenge for PKCE',
},
},
entities: {
hitlTicket: {
schema: sdk.z.object({
Expand Down
4 changes: 4 additions & 0 deletions integrations/zendesk/linkTemplate.vrl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
webhookId = to_string!(.webhookId)
webhookUrl = to_string!(.webhookUrl)

"{{ webhookUrl }}/oauth/wizard/start?state={{ webhookId }}"
5 changes: 4 additions & 1 deletion integrations/zendesk/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "@botpresshub/zendesk",
"description": "Zendesk integration for Botpress",
"scripts": {
"check:type": "tsc --noEmit",
"check:bplint": "bp lint",
Expand All @@ -13,7 +14,9 @@
"@botpress/sdk-addons": "workspace:*",
"axios": "^1.4.0",
"axios-retry": "^4.5.0",
"lodash": "^4.17.21"
"lodash": "^4.17.21",
"preact": "^10.26.6",
"preact-render-to-string": "^6.5.13"
},
"devDependencies": {
"@botpress/cli": "workspace:*",
Expand Down
5 changes: 3 additions & 2 deletions integrations/zendesk/src/actions/call-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import * as bp from '.botpress'

export const callApi: bp.IntegrationProps['actions']['callApi'] = async ({
ctx,
client,
input,
}): Promise<bp.actions.callApi.output.Output> => {
const { method, path, headers, params, requestBody } = input
const client = getZendeskClient(ctx.configuration)
const zendeskClient = await getZendeskClient(client, ctx)

try {
const requestConfig: AxiosRequestConfig = {
Expand All @@ -23,7 +24,7 @@ export const callApi: bp.IntegrationProps['actions']['callApi'] = async ({
requestConfig.data = requestBody ? JSON.parse(requestBody) : {}
}

return await client.makeRequest(requestConfig)
return await zendeskClient.makeRequest(requestConfig)
} catch (error) {
throw new sdk.RuntimeError(`Error: ${error instanceof Error ? error.message : 'An unknown error occurred'}`)
}
Expand Down
7 changes: 4 additions & 3 deletions integrations/zendesk/src/actions/close-ticket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { transformTicket } from 'src/definitions/schemas'
import { getZendeskClient } from '../client'
import * as bp from '.botpress'

export const closeTicket: bp.IntegrationProps['actions']['closeTicket'] = async ({ ctx, input }) => {
const originalTicket = await getZendeskClient(ctx.configuration).getTicket(input.ticketId)
export const closeTicket: bp.IntegrationProps['actions']['closeTicket'] = async ({ client: bpClient, ctx, input }) => {
const zendeskClient = await getZendeskClient(bpClient, ctx)
const originalTicket = await zendeskClient.getTicket(input.ticketId)

const { ticket } = await getZendeskClient(ctx.configuration).updateTicket(input.ticketId, {
const { ticket } = await zendeskClient.updateTicket(input.ticketId, {
comment: {
body: input.comment,
author_id: originalTicket.requester_id,
Expand Down
8 changes: 6 additions & 2 deletions integrations/zendesk/src/actions/create-ticket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ import { transformTicket } from 'src/definitions/schemas'
import { getZendeskClient } from '../client'
import * as bp from '.botpress'

export const createTicket: bp.IntegrationProps['actions']['createTicket'] = async ({ ctx, input }) => {
const zendeskClient = getZendeskClient(ctx.configuration)
export const createTicket: bp.IntegrationProps['actions']['createTicket'] = async ({
client: bpClient,
ctx,
input,
}) => {
const zendeskClient = await getZendeskClient(bpClient, ctx)
const ticket = await zendeskClient.createTicket(input.subject, input.comment, {
name: input.requesterName,
email: input.requesterEmail,
Expand Down
2 changes: 1 addition & 1 deletion integrations/zendesk/src/actions/create-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const createUser: bp.IntegrationProps['actions']['createUser'] = async ({
input,
logger,
}) => {
const zendeskClient = getZendeskClient(ctx.configuration)
const zendeskClient = await getZendeskClient(bpClient, ctx)

const { name, email, pictureUrl } = input

Expand Down
9 changes: 7 additions & 2 deletions integrations/zendesk/src/actions/find-customer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { transformUser } from 'src/definitions/schemas'
import { getZendeskClient } from '../client'
import * as bp from '.botpress'

export const findCustomer: bp.IntegrationProps['actions']['findCustomer'] = async ({ ctx, input }) => {
const customers = await getZendeskClient(ctx.configuration).findCustomers(input.query)
export const findCustomer: bp.IntegrationProps['actions']['findCustomer'] = async ({
client: bpClient,
ctx,
input,
}) => {
const zendeskClient = await getZendeskClient(bpClient, ctx)
const customers = await zendeskClient.findCustomers(input.query)
return { customers: customers.map(transformUser) }
}
5 changes: 3 additions & 2 deletions integrations/zendesk/src/actions/get-ticket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { transformTicket } from 'src/definitions/schemas'
import { getZendeskClient } from '../client'
import * as bp from '.botpress'

export const getTicket: bp.IntegrationProps['actions']['getTicket'] = async ({ ctx, input }) => {
const ticket = await getZendeskClient(ctx.configuration).getTicket(input.ticketId)
export const getTicket: bp.IntegrationProps['actions']['getTicket'] = async ({ client: bpClient, ctx, input }) => {
const zendeskClient = await getZendeskClient(bpClient, ctx)
const ticket = await zendeskClient.getTicket(input.ticketId)
return { ticket: transformTicket(ticket) }
}
16 changes: 8 additions & 8 deletions integrations/zendesk/src/actions/hitl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import { getZendeskClient, type ZendeskClient } from '../client'
import * as bp from '.botpress'

export const startHitl: bp.IntegrationProps['actions']['startHitl'] = async (props) => {
const { ctx, input, client } = props
const { ctx, input, client: bpClient } = props

const downstreamBotpressUser = await client.getUser({ id: ctx.botUserId })
const downstreamBotpressUser = await bpClient.getUser({ id: ctx.botUserId })
const chatbotName = input.hitlSession?.chatbotName ?? downstreamBotpressUser.user.name ?? 'Botpress'
const chatbotPhotoUrl =
input.hitlSession?.chatbotPhotoUrl ??
downstreamBotpressUser.user.pictureUrl ??
'https://app.botpress.dev/favicon/bp.svg'

const { user } = await client.getUser({
const { user } = await bpClient.getUser({
id: input.userId,
})

Expand All @@ -23,7 +23,7 @@ export const startHitl: bp.IntegrationProps['actions']['startHitl'] = async (pro
throw new sdk.RuntimeError(`User ${user.id} not linked in Zendesk`)
}

const zendeskClient = getZendeskClient(ctx.configuration)
const zendeskClient = await getZendeskClient(bpClient, ctx)
await _updateZendeskBotpressUser(props, {
zendeskClient,
chatbotName,
Expand All @@ -42,7 +42,7 @@ export const startHitl: bp.IntegrationProps['actions']['startHitl'] = async (pro
)

const zendeskTicketId = `${ticket.id}`
const { conversation } = await client.getOrCreateConversation({
const { conversation } = await bpClient.getOrCreateConversation({
channel: 'hitl',
tags: {
id: zendeskTicketId,
Expand Down Expand Up @@ -89,8 +89,8 @@ const _buildTicketBody = async (
return description + (messageHistory.length ? `\n\n---\n\n${messageHistory}` : '')
}

export const stopHitl: bp.IntegrationProps['actions']['stopHitl'] = async ({ ctx, input, client }) => {
const { conversation } = await client.getConversation({
export const stopHitl: bp.IntegrationProps['actions']['stopHitl'] = async ({ ctx, input, client: bpClient }) => {
const { conversation } = await bpClient.getConversation({
id: input.conversationId,
})

Expand All @@ -99,7 +99,7 @@ export const stopHitl: bp.IntegrationProps['actions']['stopHitl'] = async ({ ctx
return {}
}

const zendeskClient = getZendeskClient(ctx.configuration)
const zendeskClient = await getZendeskClient(bpClient, ctx)

try {
await zendeskClient.updateTicket(ticketId, {
Expand Down
5 changes: 3 additions & 2 deletions integrations/zendesk/src/actions/list-agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { transformUser } from 'src/definitions/schemas'
import { getZendeskClient } from '../client'
import * as bp from '.botpress'

export const listAgents: bp.IntegrationProps['actions']['listAgents'] = async ({ ctx, input }) => {
const agents = await getZendeskClient(ctx.configuration).getAgents(input.isOnline)
export const listAgents: bp.IntegrationProps['actions']['listAgents'] = async ({ client: bpClient, ctx, input }) => {
const zendeskClient = await getZendeskClient(bpClient, ctx)
const agents = await zendeskClient.getAgents(input.isOnline)

return {
agents: agents.map(transformUser),
Expand Down
2 changes: 1 addition & 1 deletion integrations/zendesk/src/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const wrapChannel = bpCommon.createChannelWrapper<bp.IntegrationProps>()({
ticketId: ({ conversation, logger }) => Tags.of(conversation, logger).get('id'),
zendeskAuthorId: async ({ client, logger, payload, user }) =>
Tags.of((await client.getUser({ id: payload.userId ?? user.id })).user, logger).get('id'),
zendeskClient: ({ ctx }) => getZendeskClient(ctx.configuration),
zendeskClient: async ({ client, ctx }) => await getZendeskClient(client, ctx),
},
})

Expand Down
36 changes: 23 additions & 13 deletions integrations/zendesk/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,24 @@ export type Trigger = {
id: string
}

const makeBaseUrl = (organizationDomain: string) => {
const _makeBaseUrl = (organizationDomain: string) => {
return organizationDomain.startsWith('https') ? organizationDomain : `https://${organizationDomain}.zendesk.com`
}

const makeUsername = (email: string) => {
return email.endsWith('/token') ? email : `${email}/token`
}

type AxiosRetryClient = Parameters<typeof axiosRetry>[0]
type ZendeskConfig = {
type: 'OAuth'
accessToken: string
subdomain: string
}

class ZendeskApi {
private _client: AxiosInstance
public constructor(organizationDomain: string, email: string, password: string) {
public constructor(config: ZendeskConfig) {
this._client = axios.create({
baseURL: makeBaseUrl(organizationDomain),
withCredentials: true,
auth: {
username: makeUsername(email),
password,
baseURL: _makeBaseUrl(config.subdomain),
headers: {
Authorization: `Bearer ${config.accessToken}`,
},
})

Expand Down Expand Up @@ -247,5 +246,16 @@ class ZendeskApi {

export type ZendeskClient = InstanceType<typeof ZendeskApi>

export const getZendeskClient = (config: bp.configuration.Configuration): ZendeskApi =>
new ZendeskApi(config.organizationSubdomain, config.email, config.apiToken)
export const getZendeskClient = async (client: bp.Client, ctx: bp.Context): Promise<ZendeskApi> => {
const { accessToken, subdomain } = await client
.getState({ type: 'integration', name: 'credentials', id: ctx.integrationId })
.then((result) => result.state.payload)
if (accessToken === undefined) {
throw new sdk.RuntimeError('Failed to get the OAuth accessToken')
}
if (subdomain === undefined) {
throw new sdk.RuntimeError('Failed to get the subdomain')
}

return new ZendeskApi({ type: 'OAuth', accessToken, subdomain })
}
19 changes: 11 additions & 8 deletions integrations/zendesk/src/definitions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,14 @@ export const events = {
} satisfies IntegrationDefinitionProps['events']

export const configuration = {
identifier: {
linkTemplateScript: 'linkTemplate.vrl',
},
schema: z.object({
organizationSubdomain: z
.string()
.min(1)
.title('Organization Subdomain')
.describe('Your zendesk organization subdomain. e.g. botpress7281'),
email: z.string().email().title('Email').describe('Your zendesk account email. e.g. john.doe@botpress.com'),
apiToken: z.string().min(1).title('API Token').describe('Zendesk API Token'),
syncKnowledgeBaseWithBot: z
.boolean()
.optional()
.title('Sync Knowledge Base')
.title('Sync Knowledge Base With Bot')
.describe('Would you like to sync Zendesk Knowledge Base into Bot Knowledge Base?'),
knowledgeBaseId: z
.string()
Expand Down Expand Up @@ -64,6 +60,13 @@ export const states = {
.describe('Array of trigger IDs associated with the subscription'),
}),
},
credentials: {
type: 'integration',
schema: z.object({
accessToken: z.string().optional().title('Access token').describe('The access token obtained by OAuth'),
subdomain: z.string().optional().title('Subdomain').describe('The bot subdomain'),
}),
},
} satisfies IntegrationDefinitionProps['states']

export const user = {
Expand Down
Loading
Loading