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

# Connect Freshdesk ticketing

> Create a dedicated OpenCX admin agent in Freshdesk, verify its API key, choose intake and handoff routing, and add two automation rules.

export const FreshdeskFlow = () => {
  const BEAM_COLOR = "#22c55e";
  const renderBeam = ({id, d, duration = 2.8, delay = 0, reverse = false, opacityMult = 1}) => {
    const values = reverse ? {
      x1: "110%;-10%",
      x2: "120%;0%"
    } : {
      x1: "-10%;110%",
      x2: "0%;120%"
    };
    return <g key={id}>
        <path d={d} stroke="currentColor" strokeOpacity={0.18 * opacityMult} strokeWidth="1.5" fill="none" strokeLinecap="round" />
        <path d={d} stroke={`url(#${id})`} strokeWidth="2" fill="none" strokeLinecap="round" strokeOpacity={opacityMult} />
        <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 CUST_X = 70, FD_X = 290, FORK_X = 500, OCX_X = 760, AGENT_X = 760;
  const MID = 180, R = 40;
  const AI_Y = 110, AGENT_Y = 250;
  const b1 = `M ${CUST_X + R},${MID} L ${FD_X - R},${MID}`;
  const b2 = `M ${FD_X + R},${MID} L ${FORK_X - 22},${MID}`;
  const bAI = `M ${FORK_X + 22},${MID} C ${FORK_X + 80},${MID} ${OCX_X - R - 80},${AI_Y} ${OCX_X - R},${AI_Y}`;
  const bAIr = `M ${OCX_X - R},${AI_Y} C ${OCX_X - R - 80},${AI_Y + 30} ${FORK_X + 80},${MID + 30} ${FORK_X + 22},${MID}`;
  const bAgent = `M ${FORK_X + 22},${MID} C ${FORK_X + 80},${MID} ${AGENT_X - 50 - 60},${AGENT_Y} ${AGENT_X - 50},${AGENT_Y}`;
  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 860 340" 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="Freshdesk flow: Customer tickets land in Freshdesk, then an intake fork sends each ticket to either OpenCX or a human agent">
          {renderBeam({
    id: "fd-1",
    d: b1,
    duration: 2.4,
    delay: 0
  })}
          {renderBeam({
    id: "fd-2",
    d: b2,
    duration: 2.4,
    delay: 0.4
  })}
          {renderBeam({
    id: "fd-ai",
    d: bAI,
    duration: 2.6,
    delay: 0.8
  })}
          {renderBeam({
    id: "fd-air",
    d: bAIr,
    duration: 2.6,
    delay: 2.1,
    reverse: true
  })}
          {renderBeam({
    id: "fd-ag",
    d: bAgent,
    duration: 2.6,
    delay: 1.4
  })}

          <g className="fill-zinc-500 dark:fill-zinc-400" style={{
    font: "500 10.5px ui-sans-serif, system-ui, sans-serif"
  }}>
            <text x={(FORK_X + OCX_X) / 2} y={AI_Y - 12} textAnchor="middle">Auto-handle all</text>
            <text x={(FORK_X + AGENT_X) / 2} y={AGENT_Y + 28} textAnchor="middle">Assigned only</text>
          </g>

          {}
          <g>
            <circle cx={CUST_X} cy={MID} r={R} className="fill-white stroke-black/10 dark:fill-zinc-900 dark:stroke-white/10" strokeWidth="1" />
            <g transform={`translate(${CUST_X - 10} ${MID - 10})`} className="fill-zinc-700 dark:fill-zinc-200">
              <path d="M 10 1 C 4.5 1 0 5 0 10 c 0 2 1 4 2.6 5.5 L 2 19 l 3.8 -1.4 c 1.3 0.5 2.7 0.7 4.2 0.7 c 5.5 0 10 -4 10 -9 S 15.5 1 10 1 Z" fill="currentColor" />
            </g>
            <text x={CUST_X} y={MID + 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={FD_X} cy={MID} r={R} className="fill-white stroke-black/10 dark:fill-zinc-900 dark:stroke-white/10" strokeWidth="1" />
            <foreignObject x={FD_X - 20} y={MID - 20} width="40" height="40">
              <div className="flex h-full w-full items-center justify-center">
                <img src="/images/integrations/freshdesk.svg" alt="" className="h-7 w-7 object-contain" />
              </div>
            </foreignObject>
            <text x={FD_X} y={MID + R + 22} textAnchor="middle" className="fill-zinc-700 dark:fill-zinc-200" style={{
    font: "600 12px ui-sans-serif, system-ui, sans-serif"
  }}>Freshdesk</text>
            <text x={FD_X} y={MID + R + 36} textAnchor="middle" className="fill-zinc-500 dark:fill-zinc-400" style={{
    font: "400 10.5px ui-sans-serif, system-ui, sans-serif"
  }}>tickets</text>
          </g>

          {}
          <g>
            <polygon points={`${FORK_X - 22},${MID} ${FORK_X},${MID - 26} ${FORK_X + 22},${MID} ${FORK_X},${MID + 26}`} className="fill-white stroke-black/10 dark:fill-zinc-900 dark:stroke-white/10" strokeWidth="1" />
            <text x={FORK_X} y={MID - 2} textAnchor="middle" className="fill-zinc-700 dark:fill-zinc-200" style={{
    font: "700 10px ui-sans-serif, system-ui, sans-serif"
  }}>intake</text>
            <text x={FORK_X} y={MID + 10} textAnchor="middle" className="fill-zinc-700 dark:fill-zinc-200" style={{
    font: "700 10px ui-sans-serif, system-ui, sans-serif"
  }}>gate</text>
          </g>

          {}
          <g>
            <circle cx={OCX_X} cy={AI_Y} r="44" className="fill-none" stroke={BEAM_COLOR} strokeOpacity="0.25" strokeWidth="1.5">
              <animate attributeName="r" values="42;52;42" dur="3.6s" repeatCount="indefinite" />
              <animate attributeName="stroke-opacity" values="0.35;0;0.35" dur="3.6s" repeatCount="indefinite" />
            </circle>
            <circle cx={OCX_X} cy={AI_Y} r="40" className="fill-white stroke-black/10 dark:fill-zinc-900 dark:stroke-white/10" strokeWidth="1" />
            <foreignObject x={OCX_X - 30} y={AI_Y - 16} width="60" 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={OCX_X} y={AI_Y + 56} 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={OCX_X} y={AI_Y + 70} textAnchor="middle" className="fill-zinc-500 dark:fill-zinc-400" style={{
    font: "400 10.5px ui-sans-serif, system-ui, sans-serif"
  }}>AI picks up every ticket</text>
          </g>

          {}
          <g>
            <rect x={AGENT_X - 50} y={AGENT_Y - 24} width="100" height="48" rx="10" className="fill-white stroke-black/10 dark:fill-zinc-900 dark:stroke-white/10" strokeWidth="1" strokeDasharray="3 3" />
            <g transform={`translate(${AGENT_X - 12} ${AGENT_Y - 12})`} className="fill-none stroke-zinc-700 dark:stroke-zinc-200" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
              <circle cx="12" cy="8" r="5" />
              <path d="M 2 24 C 2 16 22 16 22 24" />
            </g>
            <text x={AGENT_X} y={AGENT_Y + 40} textAnchor="middle" className="fill-zinc-700 dark:fill-zinc-200" style={{
    font: "600 12px ui-sans-serif, system-ui, sans-serif"
  }}>Human team</text>
            <text x={AGENT_X} y={AGENT_Y + 54} textAnchor="middle" className="fill-zinc-500 dark:fill-zinc-400" style={{
    font: "400 10.5px ui-sans-serif, system-ui, sans-serif"
  }}>Freshdesk routes first</text>
          </g>
        </svg>
      </div>
    </div>;
};

The Freshdesk integration uses the <Tooltip tip="The API key from the agent's profile in Freshdesk (Profile settings → Your API Key). Bound to that agent's permissions.">API key</Tooltip> of a dedicated OpenCX agent to read and write tickets, plus two Freshdesk automation rules that push ticket events back to OpenCX.

<FreshdeskFlow />

<Info>
  Setup takes about 15 minutes. You need admin access in both Freshdesk and your
  OpenCX organization, and a Freshdesk **Growth, Pro, or Enterprise** plan — the
  webhook automation action isn't available on the free plan.
</Info>

## Before you start

<AccordionGroup>
  <Accordion title="A dedicated OpenCX agent with admin privileges" icon="user-shield">
    Create a dedicated agent in Freshdesk (for example *OpenCX AI*) and give it **admin** privileges. The AI posts replies as this agent, so customers see its name on every AI message — don't reuse a person's account. Admin is a hard requirement: OpenCX needs it to tell agents and contacts apart at runtime, and **verification rejects non-admin API keys**.
  </Accordion>

  <Accordion title="Ticket scope: Global Access (all tickets)" icon="lock-open">
    Set the agent's **Scope** to **Global Access** (all tickets) under **Admin → Agents → \[the OpenCX agent] → Scope**. This is *separate* from the admin role and is **not** checked at connect time — a restricted-scope admin key verifies fine, then silently fails: OpenCX reads each ticket back from the API, and Freshdesk returns **403 "You are not authorized to perform this action"** for any ticket outside the agent's scope, so those tickets never appear in your inbox.

    * **Auto-handle-all intake** → Global Access is **required** (the AI must read every new ticket).
    * **Assigned-only intake** → Global Access is still strongly recommended. "Group Access" works only if the AI agent belongs to every group whose tickets it receives; "Restricted (assigned tickets only)" reads only tickets already assigned to the agent and 403s on anything routed by group or default-group rules.
  </Accordion>

  <Accordion title="Permission to create automation rules" icon="gears">
    Webhook delivery runs through two automation rules you create under **Admin →
    Workflows → Automations**. Only Freshdesk admins can create 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>

## Required permissions

The API key inherits the OpenCX agent's role and scope. The agent must be able to do everything below — granting **admin** + **Global Access** scope covers all of it in one step.

| Capability                              | Why OpenCX needs it                                                                                       | Enforced at connect?                                          |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| **Admin role**                          | Read agents (`/agents`) to tell agents from contacts and attribute replies/notes correctly                | ✅ Yes — non-admin keys are rejected                           |
| **Global Access** ticket scope          | Read every ticket the AI must handle (`GET /tickets/{id}`); a narrower scope 403s on out-of-scope tickets | ❌ No — verify silently passes, then tickets vanish at runtime |
| Reply to tickets & add notes            | Post AI replies (autopilot) and private drafts (assist mode)                                              | ❌ No                                                          |
| Edit ticket properties & assign tickets | Sync tags/status, set the handoff marker field, and route handoffs to an agent or group                   | ❌ No                                                          |
| Read contacts                           | Resolve the requester's name/email for the session                                                        | ❌ No                                                          |
| Create automation rules                 | Build the two **Trigger Webhook** rules that push events to OpenCX                                        | ❌ No (Freshdesk-side)                                         |

<Warning>
  Only the **admin** check runs at connect — **ticket scope is not verified**. A Global-Access admin is the safe default; anything narrower connects successfully and then silently drops tickets with a 403. If tickets stop appearing, check the agent's Scope first.
</Warning>

## Setup

<Steps>
  <Step title="Copy the OpenCX agent's API key">
    Log in to Freshdesk **as the dedicated OpenCX agent**, open **Profile settings**, and copy **Your API Key**.

    See [Freshdesk's API key guide](https://support.freshdesk.com/en/support/solutions/articles/215517) if the page has moved.

    <Warning>
      The key is bound to that agent. If the agent is deactivated or loses admin privileges, the integration stops working.
    </Warning>
  </Step>

  <Step title="Open Freshdesk in OpenCX">
    In your OpenCX dashboard, go to [**Settings →
    Integrations**](https://platform.open.cx/settings/integrations) and select
    **Freshdesk**.
  </Step>

  <Step title="Enter and verify your credentials">
    | Field       | Value                                                                                                                    |
    | ----------- | ------------------------------------------------------------------------------------------------------------------------ |
    | **Domain**  | Your Freshdesk domain — for example `yourcompany.freshdesk.com`. Include the full domain, do not prefix with `https://`. |
    | **API Key** | The OpenCX agent's API key you just copied.                                                                              |

    Click **Verify**. OpenCX calls the Freshdesk API, confirms the key belongs to an **admin** agent, and shows the connected agent's name and email so you can confirm it's the right account. A wrong domain, bad key, or non-admin agent returns an error and nothing persists.
  </Step>

  <Step title="Pick an intake mode">
    The **Auto Handle All Incoming Tickets** toggle controls which new tickets the AI picks up.

    <AccordionGroup>
      <Accordion title="Auto-handle all incoming tickets (toggle on)" icon="bolt">
        The AI answers every new ticket regardless of assignment. Use this when the AI is the front door to your helpdesk.
      </Accordion>

      <Accordion title="Assigned-only (toggle off)" icon="filter">
        The AI only answers new tickets that Freshdesk has assigned to the OpenCX agent, or that land in the **Default Group** you select. Use this when your existing Freshdesk routing rules decide which tickets go to the AI.
      </Accordion>
    </AccordionGroup>

    The gate applies to **new** tickets only — once OpenCX is tracking a ticket, status and replies keep syncing even after handoff.
  </Step>

  <Step title="Choose the handoff routing">
    Choose what OpenCX should do in Freshdesk after the AI hands off to a human.

    | Routing                            | What happens                                                                                                                             | When to use it                                                                        |
    | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
    | **Unassign ticket** *(default)*    | OpenCX removes the OpenCX agent from the ticket and reopens it.                                                                          | Use this when your team works from the unassigned queue or manually triages handoffs. |
    | **Let Freshdesk automation route** | OpenCX unassigns the ticket and sets the handoff marker field — routing only happens if you have an automation rule keyed on the marker. | Use this when routing should stay inside Freshdesk rules and queues.                  |
    | **Assign to a specific agent**     | OpenCX assigns the ticket to the **Handoff Agent ID** you enter and sets the marker.                                                     | Use this for a fixed fallback agent or small teams without routing automation.        |
    | **Assign to a group**              | OpenCX assigns the ticket to the **Handoff Group** you select and sets the marker.                                                       | Use this when a team queue should receive handoffs.                                   |

    For the marker-based routings, OpenCX sets a custom checkbox ticket field on handoff. Create it in Freshdesk under **Admin → Workflows → Ticket fields**:

    | Field                 | Value                  |
    | --------------------- | ---------------------- |
    | **Label**             | `OpenCX handed off`    |
    | **Internal/API name** | `cf_opencx_handed_off` |
    | **Type**              | Checkbox               |

    Then select it as the **Handoff Marker Field** in OpenCX. If you point your automation at the marker, make sure those rules never route marked tickets back to the OpenCX agent.

    <Note>
      If a configured handoff target becomes invalid later (agent deleted, group removed), OpenCX never leaves a handed-off ticket assigned to the AI — it falls back to unassigning and setting the marker so your rules can still catch it.
    </Note>
  </Step>

  <Step title="Optional: assist-mode footer">
    If you run{" "}

    <Tooltip tip="A per-channel mode where the AI drafts answers as private notes instead of replying to the customer directly.">
      assist mode
    </Tooltip>

    {" "}

    on any channel, the AI posts drafts as private notes. The **Assist Mode
    Footer** text is appended to each draft so reps can tell AI drafts from human
    notes at a glance.
  </Step>

  <Step title="Save and copy your webhook URL">
    Click **Save**. OpenCX validates the credentials again, stores the settings, and displays a **Webhook URL** unique to your organization, plus the two JSON payloads for the next step.

    On first connect, OpenCX also runs a one-time **backfill**: open and pending tickets from the last \~180 days are imported into your inbox as history. The backfill never triggers AI replies on old tickets.

    <Warning>
      The webhook URL contains a signed token tied to your organization. Anyone with it can post events into your OpenCX pipeline. Treat it like a password.
    </Warning>
  </Step>

  <Step title="Create the two automation rules in Freshdesk">
    In Freshdesk, open **Admin → Workflows → Automations** and create two rules. Both use the action **Trigger Webhook** with request type **POST**, encoding **JSON**, content **Advanced**, pointing at your OpenCX webhook URL.

    **Rule 1 — Ticket Creation tab.** Trigger on ticket creation and paste Payload 1 from the OpenCX dialog:

    ```json theme={"dark"}
    {
      "ticketId": "{{ticket.id}}",
      "triggered_event": "ticket_created",
      "assigneeId": "{{ticket.agent.id}}",
      "groupId": "{{ticket.group.id}}",
      "requesterEmail": "{{ticket.contact.email}}",
      "requesterName": "{{ticket.contact.name}}",
      "requesterPhone": "{{ticket.contact.phone}}"
    }
    ```

    **Rule 2 — Ticket Updates tab.** Trigger on **Agent is changed**, **Reply is sent**, **Note is added**, and **Ticket property changed (Status)**, and paste Payload 2:

    ```json theme={"dark"}
    {
      "ticketId": "{{ticket.id}}",
      "triggered_event": "{{Triggered event}}",
      "assigneeId": "{{ticket.agent.id}}",
      "groupId": "{{ticket.group.id}}",
      "requesterEmail": "{{ticket.contact.email}}",
      "requesterName": "{{ticket.contact.name}}",
      "requesterPhone": "{{ticket.contact.phone}}"
    }
    ```

    These optional fields are performance hints, not requirements:

    * `assigneeId` / `groupId` let OpenCX skip tickets that aren't for the AI agent without an extra API call.
    * `requesterEmail` / `requesterName` / `requesterPhone` let OpenCX identify the customer without a separate contact lookup.

    They're safe to include even if your Freshdesk plan doesn't expose a placeholder — unresolved or empty values are ignored, and OpenCX always re-reads the ticket from the API before acting.

    <Warning>
      Without Rule 1, the AI never sees new tickets. Without Rule 2, customer replies don't reach the AI and status changes in Freshdesk don't sync back to OpenCX.
    </Warning>

    <Warning>
      **Assigned-only intake with a shared queue?** If the AI agent receives tickets through Freshdesk's automatic assignment (round-robin / load-balanced queue) rather than being assigned at creation, the **Agent is changed** trigger on Rule 2 is what tells OpenCX the ticket is now the AI's. Without it, newly assigned tickets only surface when a later reply or status change happens. Note that Freshdesk may not fire automation rules for assignments performed by *automatic* assignment — if auto-assigned tickets still don't appear, assign the AI agent from a rule that also triggers the webhook, or have that rule fire on creation. Verify with a manual reassignment first: it should produce a ticket in your OpenCX inbox within seconds.
    </Warning>
  </Step>

  <Step title="Create a test ticket">
    Open a ticket as an end user (or use the Freshdesk portal). Within a few seconds:

    1. A new session appears in your [OpenCX Inbox](https://platform.open.cx/inbox) — email tickets land on the **Email** channel, portal/chat tickets on **Web**, phone tickets on **Phone**.
    2. The AI's answer appears on the Freshdesk ticket as the OpenCX agent — a public reply in autopilot, a private note in assist mode.

    If nothing appears, jump to [Troubleshooting](/integrations/freshdesk/troubleshooting).
  </Step>
</Steps>

## Rotating the API key

If you regenerate the OpenCX agent's API key in Freshdesk, paste the new key into [Settings → Integrations → Freshdesk](https://platform.open.cx/settings/integrations) and save. The saved key is never displayed back — the field is blank on edit and you re-enter it to change credentials. The webhook URL does not change when you rotate the key.

## Disconnecting

In OpenCX, open the Freshdesk integration and click **Disconnect**. Then in Freshdesk, delete the two automation rules. Tickets already imported stay in your OpenCX inbox; no new Freshdesk events flow in, and the AI stops posting on tickets.

***

## Related Documentation

<CardGroup cols={2}>
  <Card title="Ticket flow" icon="diagram-project" href="/integrations/freshdesk/tickets">
    How tickets match to sessions, how handoff routes them, how resolutions
    sync.
  </Card>

  <Card title="Troubleshooting" icon="wrench" href="/integrations/freshdesk/troubleshooting">
    Verification failing, webhook silent, handoff not routing.
  </Card>

  <Card title="Overview" icon="ticket" href="/integrations/freshdesk/overview">
    What the Freshdesk integration does and how it fits with your team.
  </Card>

  <Card title="Human handoff" icon="user-group" href="/handoff/introduction">
    Global handoff rules and office hours.
  </Card>
</CardGroup>
