> ## Documentation Index
> Fetch the complete documentation index at: https://docs.open.cx/llms.txt
> Use this file to discover all available pages before exploring further.

# Ticketing API

> Connect Zendesk so email handoffs become tickets your reps work from the inbox they already live in.

export const ZendeskTicketingFlow = () => {
  const CUSTOMER_X = 90;
  const ZENDESK_X = 400;
  const OPENCX_X = 710;
  const BASELINE = 130;
  const NODE_R = 42;
  const BEAM_COLOR = "#22c55e";
  const Beam = ({id, d, duration = 2.6, delay = 0, reverse = false}) => {
    const values = reverse ? {
      x1: "110%;-10%",
      x2: "120%;0%"
    } : {
      x1: "-10%;110%",
      x2: "0%;120%"
    };
    return <g>
        <path d={d} stroke="currentColor" strokeOpacity="0.18" strokeWidth="1.5" fill="none" strokeLinecap="round" />
        <path d={d} stroke={`url(#${id})`} strokeWidth="2" fill="none" strokeLinecap="round" />
        <defs>
          <linearGradient id={id} gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="0" y2="0">
            <stop offset="0%" stopColor={BEAM_COLOR} stopOpacity="0" />
            <stop offset="45%" stopColor={BEAM_COLOR} stopOpacity="1" />
            <stop offset="55%" stopColor={BEAM_COLOR} stopOpacity="1" />
            <stop offset="100%" stopColor={BEAM_COLOR} stopOpacity="0" />
            <animate attributeName="x1" values={values.x1} dur={`${duration}s`} begin={`${delay}s`} repeatCount="indefinite" />
            <animate attributeName="x2" values={values.x2} dur={`${duration}s`} begin={`${delay}s`} repeatCount="indefinite" />
          </linearGradient>
        </defs>
      </g>;
  };
  const beam1D = `M ${CUSTOMER_X + NODE_R},${BASELINE} C ${CUSTOMER_X + 140},${BASELINE + 50} ${ZENDESK_X - 140},${BASELINE + 50} ${ZENDESK_X - NODE_R},${BASELINE}`;
  const beam2D = `M ${ZENDESK_X + NODE_R},${BASELINE} C ${ZENDESK_X + 140},${BASELINE - 50} ${OPENCX_X - 140},${BASELINE - 50} ${OPENCX_X - NODE_R},${BASELINE}`;
  return <div className="zendesk-flow not-prose my-8 w-full">
      <div className="zendesk-flow-frame relative w-full overflow-hidden rounded-md border border-black/10 bg-white px-4 py-6 dark:border-white/10 dark:bg-zinc-950">
        <div className="zendesk-flow-grid pointer-events-none absolute inset-0" aria-hidden />

        <svg viewBox="0 0 800 260" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" className="relative z-[1] block w-full text-zinc-500 dark:text-zinc-400" role="img" aria-label="Zendesk Ticketing flow: customer email arrives in Zendesk as a ticket, OpenCX drafts or sends the reply">
          {}
          <Beam id="zt-beam-1" d={beam1D} duration={2.6} delay={0} />
          <Beam id="zt-beam-2a" d={beam2D} duration={2.6} delay={0.2} />
          <Beam id="zt-beam-2b" d={beam2D} duration={2.6} delay={1.5} reverse />

          {}
          <g className="fill-zinc-500 dark:fill-zinc-400" style={{
    font: "500 11px ui-sans-serif, system-ui, sans-serif"
  }}>
            <text x={(CUSTOMER_X + ZENDESK_X) / 2} y={BASELINE + 70} textAnchor="middle">email</text>
            <text x={(ZENDESK_X + OPENCX_X) / 2} y={BASELINE - 60} textAnchor="middle">ticket · AI reply</text>
          </g>

          {}
          <g>
            <circle cx={CUSTOMER_X} cy={BASELINE} r={NODE_R} className="fill-white stroke-black/10 dark:fill-zinc-900 dark:stroke-white/10" strokeWidth="1" />
            {}
            <g transform={`translate(${CUSTOMER_X - 14} ${BASELINE - 10})`} className="fill-none stroke-zinc-700 dark:stroke-zinc-200" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
              <rect x="0" y="0" width="28" height="20" rx="2" />
              <path d="M 0,3 L 14,13 L 28,3" />
            </g>
            <text x={CUSTOMER_X} y={BASELINE + NODE_R + 22} textAnchor="middle" className="fill-zinc-700 dark:fill-zinc-200" style={{
    font: "600 12px ui-sans-serif, system-ui, sans-serif"
  }}>Customer</text>
          </g>

          {}
          <g>
            <circle cx={ZENDESK_X} cy={BASELINE} r={NODE_R} className="fill-white stroke-black/10 dark:fill-zinc-900 dark:stroke-white/10" strokeWidth="1" />
            <foreignObject x={ZENDESK_X - 22} y={BASELINE - 22} width="44" height="44">
              <div className="flex h-full w-full items-center justify-center">
                <img src="/images/integrations/zendesk.svg" alt="" className="block h-7 w-7 object-contain dark:hidden" />
                <img src="/images/integrations/zendesk-dark.svg" alt="" className="hidden h-7 w-7 object-contain dark:block" />
              </div>
            </foreignObject>
            <text x={ZENDESK_X} y={BASELINE + NODE_R + 22} textAnchor="middle" className="fill-zinc-700 dark:fill-zinc-200" style={{
    font: "600 12px ui-sans-serif, system-ui, sans-serif"
  }}>Zendesk</text>
            <text x={ZENDESK_X} y={BASELINE + NODE_R + 36} textAnchor="middle" className="fill-zinc-500 dark:fill-zinc-400" style={{
    font: "400 10.5px ui-sans-serif, system-ui, sans-serif"
  }}>Ticketing API</text>
          </g>

          {}
          <g>
            <circle cx={OPENCX_X} cy={BASELINE} r={NODE_R + 4} className="fill-none" stroke={BEAM_COLOR} strokeOpacity="0.25" strokeWidth="1.5">
              <animate attributeName="r" values={`${NODE_R + 2};${NODE_R + 10};${NODE_R + 2}`} dur="3.6s" repeatCount="indefinite" />
              <animate attributeName="stroke-opacity" values="0.35;0;0.35" dur="3.6s" repeatCount="indefinite" />
            </circle>
            <circle cx={OPENCX_X} cy={BASELINE} r={NODE_R} className="fill-white stroke-black/10 dark:fill-zinc-900 dark:stroke-white/10" strokeWidth="1" />
            <foreignObject x={OPENCX_X - 32} y={BASELINE - 16} width="64" height="32">
              <div className="flex h-full w-full items-center justify-center">
                <img src="/logo-light.svg" alt="" className="block h-full w-full object-contain dark:hidden" />
                <img src="/logo.svg" alt="" className="hidden h-full w-full object-contain dark:block" />
              </div>
            </foreignObject>
            <text x={OPENCX_X} y={BASELINE + NODE_R + 22} textAnchor="middle" className="fill-zinc-700 dark:fill-zinc-200" style={{
    font: "600 12px ui-sans-serif, system-ui, sans-serif"
  }}>OpenCX</text>
            <text x={OPENCX_X} y={BASELINE + NODE_R + 36} textAnchor="middle" className="fill-zinc-500 dark:fill-zinc-400" style={{
    font: "400 10.5px ui-sans-serif, system-ui, sans-serif"
  }}>AI agent</text>
          </g>
        </svg>
      </div>
    </div>;
};

The Ticketing API path wires OpenCX into Zendesk's standard <Tooltip tip="Zendesk's REST API for tickets, users, groups, and webhooks. OpenCX supports OAuth and legacy per-user API tokens.">support API</Tooltip>. Use it for **email** — the async channel where every handoff should become a trackable <Tooltip tip="A unit of work in Zendesk with a status, assignee, comments, and custom fields.">ticket</Tooltip>. Chat, SMS, WhatsApp, phone, and social belong on [Sunshine Conversations](/integrations/zendesk/sunshine-conversations) instead.

<ZendeskTicketingFlow />

<Info>
  Setup takes about 15 minutes. You need a Zendesk admin role on both sides — your OpenCX organization and your Zendesk account.
</Info>

## Before you start

<AccordionGroup>
  <Accordion title="Zendesk plan with API access" icon="circle-check">
    Suite Team or higher, or Support Professional. Suite Growth and above is recommended so you can use <Tooltip tip="Automated rules that fire on ticket events — e.g. 'when a public comment is added, notify the OpenCX webhook.'">triggers</Tooltip> for webhook delivery.
  </Accordion>

  <Accordion title="Admin access in Zendesk" icon="user-shield">
    OAuth requires an agent or admin who can authorize the OpenCX global OAuth client. You also need permission to create a webhook and trigger in [Zendesk Admin Center](https://admin.zendesk.com). Legacy setup additionally requires permission to generate an API token.
  </Accordion>

  <Accordion title="Owner or admin in OpenCX" icon="key">
    Required to save integration settings in [Settings → Integrations](https://platform.open.cx/settings/integrations).
  </Accordion>
</AccordionGroup>

## Choose authorization

Open [**Settings → Integrations**](https://platform.open.cx/settings/integrations), select **Zendesk → Ticketing**, then choose:

* **OAuth — recommended:** authorize OpenCX in Zendesk. No API token is stored or rotated.
* **Legacy credentials:** enter an admin email and API token. Existing installations continue to work and can switch to OAuth without losing their legacy fallback until it is removed.

## OAuth setup (recommended)

<Steps>
  <Step title="Choose Ticketing OAuth">
    In OpenCX, open **Settings → Integrations → Zendesk → Ticketing → OAuth**.
  </Step>

  <Step title="Enter your Zendesk subdomain">
    Enter only the `acme` part of `acme.zendesk.com`, then click **Continue to Zendesk**. Do not include `https://` or `.zendesk.com`.
  </Step>

  <Step title="Authorize OpenCX">
    Sign in to the matching Zendesk account, review the requested Support API access, and click **Allow**. Zendesk returns you to OpenCX, which verifies the account and displays **Connected**.

    <Warning>
      The Zendesk account you authorize must match the subdomain you entered. OpenCX rejects cross-account callbacks rather than attaching one customer's OAuth grant to another customer.
    </Warning>
  </Step>

  <Step title="Register the reply webhook and trigger">
    Back in the OAuth panel, copy the signed **Reply webhook URL** and open its trigger instructions. In Zendesk, create a `POST` JSON webhook with no additional authentication, then create a trigger with:

    * **Meet ANY:** Ticket is Created; Ticket is Updated.
    * **Meet ALL:** Comment is Present (public).
    * **Action:** Notify active webhook using the OpenCX webhook.
    * **JSON body:**

    ```json theme={"dark"}
    {
      "ticketId": "{{ticket.id}}",
      "event_type": "new_comment"
    }
    ```

    OAuth authenticates OpenCX's calls to Zendesk; the trigger is still required for Zendesk to deliver public agent replies back to OpenCX.
  </Step>

  <Step title="Verify end to end">
    Escalate a test email, confirm the Zendesk ticket has the `opencx` tag, then post a public agent reply. The reply should appear on the matching OpenCX session. Internal notes must remain private.
  </Step>
</Steps>

## Legacy API token setup

<Steps>
  <Step title="Generate a Zendesk API token">
    In Zendesk, open [**Admin Center → Apps and integrations → APIs → Zendesk API → API Tokens → Add API token**](https://support.zendesk.com/hc/en-us/articles/4408889192858-Managing-API-token-access-to-the-Zendesk-API).

    1. Toggle **Token access** on.
    2. Click **Add API token**.
    3. Label it `OpenCX` (or anything recognizable).
    4. **Copy the token immediately** — Zendesk hides it after you close the dialog.

    <Warning>
      The token is bound to the user whose email you provide in the next step. If that user is deactivated or loses permission, the integration stops working.
    </Warning>
  </Step>

  <Step title="Open Zendesk Ticketing in OpenCX">
    In OpenCX, go to [**Settings → Integrations**](https://platform.open.cx/settings/integrations), find **Zendesk**, and click **Zendesk Ticketing**.
  </Step>

  <Step title="Enter your credentials">
    | Field                | Value                                                                                                                                                                                                                                                                                                                                                                                           |
    | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | **Subdomain**        | The `acme` part of `acme.zendesk.com`. Do not include `.zendesk.com` or `https://`.                                                                                                                                                                                                                                                                                                             |
    | **Email**            | The admin email that owns the API token.                                                                                                                                                                                                                                                                                                                                                        |
    | **API Token**        | The token you just copied.                                                                                                                                                                                                                                                                                                                                                                      |
    | **Default Agent ID** | Required. The numeric Zendesk user ID used as the fallback assignee when no routing rule applies. To find it: in Zendesk, open the agent's profile page (**Admin Center → People → Team members → click the agent**). The ID is the number at the end of the URL — e.g. in `https://your-subdomain.zendesk.com/agent/users/1234567890/tickets/requested`, the Default Agent ID is `1234567890`. |

    Click **Test & Save**. OpenCX hits the Zendesk API with your credentials before persisting — a wrong subdomain or token returns an error and nothing saves.
  </Step>

  <Step title="Copy your webhook URL">
    After saving, OpenCX displays a **Webhook URL** unique to your organization. Keep this tab open — you'll paste it into Zendesk next.

    <Warning>
      The webhook URL is signed and secret. Anyone who has it can post events into your OpenCX session pipeline. Treat it like a password.
    </Warning>
  </Step>

  <Step title="Register the webhook in Zendesk">
    In Zendesk, follow [Zendesk's guide to creating a webhook](https://support.zendesk.com/hc/en-us/articles/4408839108378-Creating-webhooks-to-interact-with-third-party-systems) to open **Admin Center → Apps and integrations → Webhooks**, then click **Create webhook → Trigger or automation**.

    | Field              | Value                               |
    | ------------------ | ----------------------------------- |
    | **Name**           | `OpenCX`                            |
    | **Endpoint URL**   | Paste the OpenCX webhook URL.       |
    | **Request method** | `POST`                              |
    | **Request format** | `JSON`                              |
    | **Authentication** | `None` — the URL is already signed. |

    Save the webhook.
  </Step>

  <Step title="Create the trigger that fires the webhook">
    Follow [Zendesk's guide to creating a trigger](https://support.zendesk.com/hc/en-us/articles/4408886797466-Creating-ticket-triggers-for-automatic-ticket-updates-and-notifications) to open **Admin Center → Objects and rules → Triggers** and click **Create trigger**.

    Configure:

    * **Trigger name:** `OpenCX webhook` (or anything recognizable).
    * **Category:** pick an existing category such as `Notifications`, or create a new one (e.g. `OpenCX`).
    * **Conditions → Meet ANY of:** `Ticket → Is → Created`; `Ticket → Is → Updated`.
    * **Conditions → Meet ALL of:** `Comment → Is → Present (public)`.

    <Warning>
      You need **both** `Created` and `Updated` under **Meet ANY of**. `Is → Updated` alone does not fire on ticket creation, so brand-new tickets would never reach OpenCX.
    </Warning>

    * **Actions → Notify by → Active webhook**, then select `OpenCX` from the dropdown.
    * **JSON body** — paste exactly:

    ```json theme={"dark"}
    {
      "ticketId": "{{ticket.id}}",
      "event_type": "new_comment"
    }
    ```

    <Warning>
      Both keys are required and case-sensitive: `ticketId` (camelCase) and `event_type` set to the literal string `new_comment`. OpenCX rejects any other shape with `400 ticketId is required`. Do **not** put the comment text in the body — OpenCX fetches the comment from Zendesk itself, and only ever reads the ticket's public comments, so internal notes never leak to the customer.
    </Warning>

    Click **Create trigger** and save.
  </Step>

  <Step title="Verify end to end">
    Send a test email to an address you have connected to OpenCX email handoff. Escalate the conversation. Within a few seconds:

    1. A ticket appears in Zendesk, tagged `opencx`.
    2. Reply publicly from Zendesk.
    3. The reply appears as an agent message on the matching session in your [OpenCX Inbox](https://platform.open.cx/inbox).

    <Info>
      **Every OpenCX ticket is traceable back to its session.** The session UUID is written to the ticket's `external_id`, and the first comment includes a **View session in OpenCX** link that opens the full AI reasoning, tool calls, and transcript in your inbox. Filter Zendesk views by `external_id` or any `opencx_*` tag described below.
    </Info>

    If any step fails, jump to [Troubleshooting](/integrations/zendesk/troubleshooting).
  </Step>
</Steps>

## How handoff lands in Zendesk

```mermaid theme={"dark"}
flowchart LR
  A[Contact message] --> B{AI can resolve?}
  B -- Yes --> C[AI replies on channel]
  B -- No --> D[Handoff]
  D --> E[Create or update Zendesk ticket]
  E --> F[Rep replies publicly in Zendesk]
  F --> G[Reply syncs back to contact on the original channel]
```

When <Tooltip tip="The decision point where the AI escalates a conversation to a human agent." cta="Handoff settings" href="/handoff/introduction">handoff</Tooltip> triggers, OpenCX does the following in Zendesk:

1. Looks up the contact by email. If not found, creates a Zendesk user with the contact's email and name.
2. Creates a ticket with the session ID as <Tooltip tip="A Zendesk field that stores your own reference ID on a ticket. OpenCX writes the session ID here so future events match back to the same conversation.">`external_id`</Tooltip>, the conversation summary as the description, and the full transcript appended as the first comment.
3. Applies tags (see [Tags OpenCX applies](#tags-opencx-applies) below) so your views and triggers can filter by mode, channel, sentiment, and language.
4. Auto-fills any custom fields you've mapped (see [AI Fields](#ai-fields) below).
5. Assigns to your **Default Agent ID** if configured; otherwise leaves unassigned for Zendesk routing to handle.

<Note>
  The session is linked to the ticket by `external_id`. If the same contact re-engages before the ticket is closed, OpenCX appends to the existing ticket instead of opening a new one.
</Note>

### Create a ticket only on handoff

By default OpenCX only creates a ticket when the AI escalates. If you want every conversation to create a ticket up front (for audit or SLA reasons), reach out to support — this flag is set per organization.

<Note>
  With this flag on, tickets created at handoff are assigned to your **Default Agent Group** (if set) rather than the **Default Agent ID** — the individual-agent assignment in step 5 above applies only to the standard, ticket-created-early flow.
</Note>

## Rep replies sync back

When a rep posts a **public** reply in Zendesk, the webhook fires and OpenCX:

1. Matches the ticket to the original session using `external_id`.
2. Sends the reply to the contact on the original channel — email, SMS, WhatsApp, web widget, etc.
3. Marks the session as having an active human agent so the AI stops auto-responding.

<Tooltip tip="Comments visible only to agents inside the ticket. Used for private collaboration between your team.">Internal notes</Tooltip> are ignored — your reps can take private notes in Zendesk without triggering outbound messages.

<Warning>
  Only **public** comments sync. If a rep accidentally posts to the wrong ticket, the message still reaches that ticket's linked contact. Treat Zendesk tickets as customer-visible threads.
</Warning>

## AI Fields

Map Zendesk <Tooltip tip="Fields you've defined in Zendesk beyond the defaults — Reason, Product Area, Severity, etc.">custom fields</Tooltip> to AI-generated values so triage, routing, and reporting work on day one. Configure at [**Settings → Ticketing**](https://platform.open.cx/settings/ticketing) under **Auto fill Zendesk fields**.

<Steps>
  <Step title="Pick a Zendesk field">
    OpenCX loads every custom field on your Zendesk account. Pick one — dropdown (<Tooltip tip="A custom field with a predefined list of values, rendered as a dropdown in Zendesk.">tagger</Tooltip>) and free-text fields are supported.
  </Step>

  <Step title="Describe what the AI should fill">
    Write a short description of what the field represents. For dropdowns, OpenCX picks from existing options only. For text, OpenCX writes a short value.

    Example description for a **Reason** dropdown: *"The primary reason the contact is writing. Pick the closest match from the allowed values."*
  </Step>

  <Step title="Enable">
    Toggle the mapping on. The next ticket OpenCX creates (or updates on handoff) includes the auto-filled value.
  </Step>
</Steps>

<Tip>
  Keep descriptions short and concrete. Fields with vague descriptions ("customer feedback") produce vague values.
</Tip>

## Tags OpenCX applies

| Tag                                | When                                                                                                                                                                         |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `opencx`                           | Always, on any ticket OpenCX creates or writes to.                                                                                                                           |
| `opencx_autopilot`                 | The AI was in autopilot (resolved or handed off without a rep present).                                                                                                      |
| `opencx_assist_mode`               | The AI posted an internal note on an agent-owned ticket (Assist Mode — a [discouraged fallback](/integrations/zendesk/troubleshooting#assist-mode-doesnt-post-suggestions)). |
| `opencx_handedoff`                 | The AI escalated to a human rep.                                                                                                                                             |
| `opencx_channel_<name>`            | The source channel — `email`, `whatsapp`, `sms`, `web`, `phone`.                                                                                                             |
| `opencx_sentiment_<value>`         | Detected sentiment on handoff — `happy`, `neutral`, `negative`.                                                                                                              |
| `opencx_language_<code>`           | Detected conversation language.                                                                                                                                              |
| `opencx_ticket_created_on_handoff` | Applied when the ticket was created at handoff (vs updated on a returning contact).                                                                                          |

Use these tags in Zendesk views, triggers, and reports.

## Rotating Legacy credentials

OAuth access is refreshed automatically and has no API token to rotate. For a Legacy connection, regenerating the Zendesk API token does not affect the webhook — the webhook URL is signed with a separate key. To rotate:

1. Create a new token in Zendesk.
2. Paste it into [Settings → Integrations → Zendesk](https://platform.open.cx/settings/integrations).
3. Delete the old token in Zendesk.

## Disconnecting

For OAuth, open **Settings → Integrations → Zendesk → Ticketing → OAuth**, click **Disconnect**, and type `DELETE`. This revokes and removes only the OAuth connection; complete Legacy credentials remain available. Delete or deactivate the Zendesk trigger and webhook as well if you want public-comment delivery to stop. Tickets created while the integration was active remain untouched.

For Legacy authorization, remove or replace the saved credentials through the Legacy panel. Deactivating the Zendesk trigger and webhook stops new inbound events immediately.

***

## Related Documentation

<CardGroup cols={2}>
  <Card title="AI Email in Zendesk" icon="envelope" href="/integrations/zendesk/channels/email">
    Per-channel implementation details for email handoff.
  </Card>

  <Card title="Sunshine Conversations" icon="messages" href="/integrations/zendesk/sunshine-conversations">
    The path for chat, SMS, WhatsApp, phone, and social channels.
  </Card>

  <Card title="Troubleshooting" icon="wrench" href="/integrations/zendesk/troubleshooting">
    Webhook 4xx, trigger not firing, credentials failing.
  </Card>
</CardGroup>
