> ## 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. Bound per-user via an API token.">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">
    You need to generate an API token, create a webhook, and create a trigger. All three live in [Zendesk Admin Center](https://admin.zendesk.com) — agent roles cannot reach them.
  </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>

## 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 **Verify & 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** — include at minimum:

    <CodeGroup>
      ```json Minimal theme={"dark"}
      {
        "ticket_id": "{{ticket.id}}",
        "comment": "{{ticket.latest_public_comment}}",
        "author_id": "{{ticket.latest_public_comment.author_id}}"
      }
      ```

      ```json With tags theme={"dark"}
      {
        "ticket_id": "{{ticket.id}}",
        "external_id": "{{ticket.external_id}}",
        "comment": "{{ticket.latest_public_comment}}",
        "author_id": "{{ticket.latest_public_comment.author_id}}",
        "tags": "{{ticket.tags}}",
        "status": "{{ticket.status}}"
      }
      ```
    </CodeGroup>

    <Note>
      Use `{{ticket.latest_public_comment}}`, not `{{ticket.latest_comment}}`. The second variant includes internal notes and will leak them to the customer.
    </Note>

    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.

## 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 **Zendesk AI 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 the API token

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

In OpenCX, open the Zendesk integration and click **Disconnect**. Then in Zendesk, delete the webhook and the trigger you created. Tickets created while the integration was active remain untouched.

***

## 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>
