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

> Connect HubSpot with the OpenCX OAuth Public App, select the HubSpot teammate the AI replies as, and verify sync end to end.

export const HubspotFlow = () => {
  const BEAM_COLOR = "#22c55e";
  const renderBeam = ({id, d, duration = 2.8, delay = 0, reverse = false}) => {
    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" 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 CUST_X = 80, HUB_X = 420, OCX_X = 760;
  const INBOX_Y = 100, TICKET_Y = 220, CUST_Y = 160, OCX_Y = 160;
  const TILE_W = 170, TILE_H = 68;
  const b1 = `M ${CUST_X + 40},${CUST_Y - 18} C ${CUST_X + 160},${INBOX_Y} ${HUB_X - TILE_W / 2 - 40},${INBOX_Y} ${HUB_X - TILE_W / 2},${INBOX_Y}`;
  const b2 = `M ${CUST_X + 40},${CUST_Y + 18} C ${CUST_X + 160},${TICKET_Y} ${HUB_X - TILE_W / 2 - 40},${TICKET_Y} ${HUB_X - TILE_W / 2},${TICKET_Y}`;
  const b3 = `M ${HUB_X + TILE_W / 4},${INBOX_Y + TILE_H / 2} C ${HUB_X + TILE_W / 2},${(INBOX_Y + TICKET_Y) / 2} ${HUB_X + TILE_W / 2},${(INBOX_Y + TICKET_Y) / 2} ${HUB_X + TILE_W / 4},${TICKET_Y - TILE_H / 2}`;
  const b4 = `M ${HUB_X + TILE_W / 2},${INBOX_Y} C ${HUB_X + TILE_W / 2 + 60},${INBOX_Y} ${OCX_X - 80},${OCX_Y - 20} ${OCX_X - 44},${OCX_Y}`;
  const b5 = `M ${HUB_X + TILE_W / 2},${TICKET_Y} C ${HUB_X + TILE_W / 2 + 60},${TICKET_Y} ${OCX_X - 80},${OCX_Y + 20} ${OCX_X - 44},${OCX_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="HubSpot flow: conversations land in the HubSpot Inbox; escalations spawn Tickets; both sync bidirectionally with OpenCX">
          {renderBeam({
    id: "hs-b1",
    d: b1,
    duration: 2.8,
    delay: 0
  })}
          {renderBeam({
    id: "hs-b2",
    d: b2,
    duration: 3.2,
    delay: 0.5
  })}
          {renderBeam({
    id: "hs-b3",
    d: b3,
    duration: 3.2,
    delay: 1.0
  })}
          {renderBeam({
    id: "hs-b4a",
    d: b4,
    duration: 2.6,
    delay: 0
  })}
          {renderBeam({
    id: "hs-b4b",
    d: b4,
    duration: 2.6,
    delay: 1.3,
    reverse: true
  })}
          {renderBeam({
    id: "hs-b5a",
    d: b5,
    duration: 2.6,
    delay: 0.4
  })}
          {renderBeam({
    id: "hs-b5b",
    d: b5,
    duration: 2.6,
    delay: 1.7,
    reverse: true
  })}

          <g className="fill-zinc-500 dark:fill-zinc-400" style={{
    font: "500 10.5px ui-sans-serif, system-ui, sans-serif"
  }}>
            <text x={(CUST_X + HUB_X - TILE_W / 2) / 2 + 40} y={INBOX_Y - 12} textAnchor="middle">chat · email</text>
            <text x={(CUST_X + HUB_X - TILE_W / 2) / 2 + 40} y={TICKET_Y + 30} textAnchor="middle">escalation</text>
            <text x={HUB_X + TILE_W / 2 + 14} y={(INBOX_Y + TICKET_Y) / 2 + 4} className="fill-zinc-500 dark:fill-zinc-400" textAnchor="start">linked</text>
          </g>

          {}
          <g>
            <circle cx={CUST_X} cy={CUST_Y} r="40" className="fill-white stroke-black/10 dark:fill-zinc-900 dark:stroke-white/10" strokeWidth="1" />
            <g transform={`translate(${CUST_X - 14} ${CUST_Y - 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={CUST_X} y={CUST_Y + 58} 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>
            <rect x={HUB_X - TILE_W / 2} y={INBOX_Y - TILE_H / 2} width={TILE_W} height={TILE_H} rx="8" className="fill-white stroke-black/10 dark:fill-zinc-900 dark:stroke-white/10" strokeWidth="1" />
            <foreignObject x={HUB_X - TILE_W / 2 + 12} y={INBOX_Y - 14} width="28" height="28">
              <div className="flex h-full w-full items-center justify-center">
                <img src="/images/integrations/hubspot.svg" alt="" className="h-7 w-7 object-contain" />
              </div>
            </foreignObject>
            <text x={HUB_X - TILE_W / 2 + 48} y={INBOX_Y - 4} className="fill-zinc-800 dark:fill-zinc-100" style={{
    font: "600 12.5px ui-sans-serif, system-ui, sans-serif"
  }}>Inbox</text>
            <text x={HUB_X - TILE_W / 2 + 48} y={INBOX_Y + 12} className="fill-zinc-500 dark:fill-zinc-400" style={{
    font: "400 10.5px ui-sans-serif, system-ui, sans-serif"
  }}>live conversation thread</text>
          </g>

          {}
          <g>
            <rect x={HUB_X - TILE_W / 2} y={TICKET_Y - TILE_H / 2} width={TILE_W} height={TILE_H} rx="8" className="fill-white stroke-black/10 dark:fill-zinc-900 dark:stroke-white/10" strokeWidth="1" />
            <foreignObject x={HUB_X - TILE_W / 2 + 12} y={TICKET_Y - 14} width="28" height="28">
              <div className="flex h-full w-full items-center justify-center">
                <img src="/images/integrations/hubspot.svg" alt="" className="h-7 w-7 object-contain" />
              </div>
            </foreignObject>
            <text x={HUB_X - TILE_W / 2 + 48} y={TICKET_Y - 4} className="fill-zinc-800 dark:fill-zinc-100" style={{
    font: "600 12.5px ui-sans-serif, system-ui, sans-serif"
  }}>Tickets</text>
            <text x={HUB_X - TILE_W / 2 + 48} y={TICKET_Y + 12} className="fill-zinc-500 dark:fill-zinc-400" style={{
    font: "400 10.5px ui-sans-serif, system-ui, sans-serif"
  }}>transcript + summary</text>
          </g>

          <text x={HUB_X} y={TICKET_Y + TILE_H / 2 + 22} textAnchor="middle" className="fill-zinc-500 dark:fill-zinc-400" style={{
    font: "400 11px ui-sans-serif, system-ui, sans-serif"
  }}>HubSpot Service Hub</text>

          {}
          <g>
            <circle cx={OCX_X} cy={OCX_Y} r="48" className="fill-none" stroke={BEAM_COLOR} strokeOpacity="0.25" strokeWidth="1.5">
              <animate attributeName="r" values="46;56;46" 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={OCX_Y} r="44" className="fill-white stroke-black/10 dark:fill-zinc-900 dark:stroke-white/10" strokeWidth="1" />
            <foreignObject x={OCX_X - 34} y={OCX_Y - 18} width="68" height="36">
              <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={OCX_Y + 66} 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={OCX_Y + 80} 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>;
};

One-time setup to connect HubSpot to OpenCX. Use **OAuth Public App** unless your OpenCX admin tells you to use the legacy Private App path.

<HubspotFlow />

<Info>
  OAuth setup usually takes less than a minute. You need HubSpot admin access and OpenCX admin access.
</Info>

## Before you start

<AccordionGroup>
  <Accordion title="HubSpot account with Service Hub" icon="circle-check">
    The integration uses HubSpot's conversations and ticketing APIs. Service Hub (Starter or above) is required.
  </Accordion>

  <Accordion title="HubSpot admin access" icon="user-shield">
    You need permission to approve connected apps for the HubSpot portal you want OpenCX to use.
  </Accordion>

  <Accordion title="Owner or admin in OpenCX" icon="key">
    Only organization owners and admins can save integration credentials in the OpenCX dashboard.
  </Accordion>
</AccordionGroup>

## What OAuth handles for you

The OAuth Public App replaces the manual Private App setup:

|                      | OAuth Public App                                      |
| -------------------- | ----------------------------------------------------- |
| **Connection**       | Approve OpenCX on HubSpot's consent screen            |
| **Tokens**           | Issued and refreshed automatically                    |
| **Webhooks**         | Registered by OpenCX during install                   |
| **AI sender**        | Picked from a HubSpot teammate dropdown after connect |
| **Workflow actions** | Adds OpenCX actions to HubSpot workflows              |

## Setup

<Steps>
  <Step title="Open HubSpot integration settings">
    Open [**Settings → Integrations**](https://platform.open.cx/settings/integrations) in your OpenCX dashboard and select **HubSpot**.

    The modal asks which connection method to use. Choose **OAuth Public App**.

    <Warning>
      If you already connected with a Private App, pick **OAuth Public App** to migrate. Existing tickets, sessions, and email signatures stay intact.
    </Warning>
  </Step>

  <Step title="Connect with HubSpot">
    Click **Connect with HubSpot**. OpenCX redirects you to HubSpot's consent screen.

    Choose the HubSpot portal to connect and approve the requested permissions.

    <Note>
      The permissions cover conversations, tickets, contacts, companies, deals, owners, files, lists, and HubSpot workflow actions. OpenCX requests them through HubSpot's standard app consent screen; you do not paste a token into OpenCX.
    </Note>
  </Step>

  <Step title="Confirm the connection in OpenCX">
    After approval, HubSpot redirects you back to **Settings → Integrations**. The HubSpot card shows **Connected**, and the modal shows **Connected via OAuth**.

    If the redirect shows an error, re-open the modal and try again. If HubSpot says the app is not available for the selected portal, confirm you are an admin for that portal.
  </Step>

  <Step title="Pick who the AI posts as">
    In the OAuth view, use **AI posts as** to select the HubSpot teammate OpenCX should attribute outgoing replies to.

    Customers see this teammate's name on email replies. Use a dedicated AI teammate if your team wants replies to be clearly labeled.
  </Step>

  <Step title="Enable Autopilot for the channel">
    Before testing, enable **Autopilot** for the channel you'll use in [**Settings → Autopilot**](https://platform.open.cx/settings/autopilot).

    Autopilot is configured per channel. If it is off for Email, Web, SMS, WhatsApp, or Phone, OpenCX stores the conversation but does not auto-reply on that channel.
  </Step>

  <Step title="Send a test message">
    For a HubSpot conversation to appear automatically, the conversation must originate from a channel HubSpot owns — either a HubSpot chat widget or an email address routed through a HubSpot inbox.

    1. Send a test email to a HubSpot-connected inbox, or send a message through the HubSpot chat widget.
    2. Confirm the conversation appears in HubSpot.
    3. Confirm a matching session appears in the [OpenCX inbox](https://platform.open.cx/inbox).
    4. Let the AI reply and confirm the reply posts back to the same HubSpot thread.
    5. Trigger a handoff and confirm the handoff note appears in HubSpot with a link back to the OpenCX session.
  </Step>
</Steps>

<Warning>
  The test above is for HubSpot-owned channels. OpenCX-native channels can still create HubSpot tickets on handoff through the `create-hubspot-ticket-from-session` workflow action; use the channel pages for verification steps.
</Warning>

## Workflow actions

OAuth installs add OpenCX actions to HubSpot workflows. Use them when HubSpot rules should control the AI:

| Action                     | Use it when                                                                   |
| -------------------------- | ----------------------------------------------------------------------------- |
| **Pause OpenCX AI**        | A HubSpot workflow claims the conversation for a human.                       |
| **Resume OpenCX AI**       | A workflow returns a handed-off conversation to the AI.                       |
| **Send to OpenCX AI**      | A workflow decides the latest customer message should be processed by the AI. |
| **Request OpenCX handoff** | HubSpot should ask OpenCX to hand the session to a human.                     |

<Info>
  Workflow actions are available only on the OAuth Public App path. They do not appear for legacy Private App connections.
</Info>

## Email signatures

Append branded email signatures to outgoing replies sent through HubSpot. Signatures are matched to the sender's email domain, so different domains can have different signatures.

<Steps>
  <Step title="Open HubSpot settings">
    Go to [**Settings → Integrations → HubSpot**](https://platform.open.cx/settings/integrations) in your OpenCX dashboard.
  </Step>

  <Step title="Add a signature">
    In the OAuth view, scroll to **Email Signatures** and click **Add**. Enter the domain pattern and paste your signature HTML.
  </Step>

  <Step title="Add more domains (optional)">
    Repeat for each domain that needs a different signature.
  </Step>
</Steps>

### Domain matching

| Pattern   | Matches                                    |
| --------- | ------------------------------------------ |
| `open.cx` | Emails from `open.cx` only (exact match)   |
| `*.nl`    | All emails ending with `.nl` (wildcard)    |
| `*.co.uk` | All emails ending with `.co.uk` (wildcard) |

<Note>
  Exact domain matches always take priority over wildcard patterns. If you have both `open.cx` and `*.cx`, emails from `open.cx` use the exact-match signature.
</Note>

## Advanced: Private App (legacy)

Use **Private App (legacy)** only when OAuth is unavailable for your organization or support asks you to use the fallback path.

<AccordionGroup>
  <Accordion title="Create the Private App" icon="key">
    In HubSpot, go to **Settings → Integrations → Private Apps → Legacy Apps → Create Legacy App** and choose **Private**.

    Enable these scopes: `crm.objects.contacts.read`, `crm.objects.contacts.write`, `crm.objects.companies.read`, `crm.objects.deals.read`, `crm.objects.owners.read`, `tickets`, `conversations.read`, and `conversations.write`.

    Copy the **Private App Access Token** after creation. HubSpot shows it only once.
  </Accordion>

  <Accordion title="Configure the legacy webhook" icon="webhook">
    In OpenCX, choose **Private App (legacy)** and copy the **Webhook URL**.

    In HubSpot, paste that URL into the Private App's **Webhooks** tab, set event throttling high enough for your volume, and subscribe to:

    | Object type  | Event                 |
    | ------------ | --------------------- |
    | Conversation | New message           |
    | Conversation | Conversation creation |
    | Ticket       | Property change       |

    Click **Commit changes** in HubSpot after editing subscriptions.
  </Accordion>

  <Accordion title="Save legacy credentials in OpenCX" icon="floppy-disk">
    In OpenCX, enter:

    | Field                        | Value                                                  |
    | ---------------------------- | ------------------------------------------------------ |
    | **Private App Access Token** | The `pat-...` token from HubSpot.                      |
    | **Default User ID**          | The numeric HubSpot user ID only, with no `A-` prefix. |

    Find the user ID in HubSpot under **Settings → Users & Teams**. Open the teammate profile and copy the number at the end of the URL.
  </Accordion>
</AccordionGroup>

## Disconnecting

To disconnect an OAuth install, click **Disconnect** in [**Settings → Integrations → HubSpot**](https://platform.open.cx/settings/integrations), then type `DELETE` to confirm. Existing OpenCX sessions remain available.

If you use the legacy Private App path, also remove the webhook subscriptions from your HubSpot Legacy App.

***

## Related Documentation

<CardGroup cols={2}>
  <Card title="Handoff & Sync" icon="arrow-right-arrow-left" href="/integrations/hubspot/handoff">
    Ticket creation, contact sync, CRM context.
  </Card>

  <Card title="Troubleshooting" icon="wrench" href="/integrations/hubspot/troubleshooting">
    Connection failures, missing tickets, reply sync issues.
  </Card>

  <Card title="HubSpot Overview" icon="hubspot" href="/integrations/hubspot/overview">
    Capabilities, supported channels, observability.
  </Card>

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