# Connecting Integrations (GHL, HubSpot, Kommo, Calendly, ManyChat)

> How each integration is connected **from the MCP**: when you hand out a magic link, what
> you still have to ask afterwards, and which Ninjo screen to name when the user has to do
> something by hand. For "where do this agent's messages actually live?" (direct Meta vs
> CRM) read `knowledge/messaging-channels.md`.

## Never ask for these

None of these flows uses a credential the user dictates to you. If you are about to ask for
one, you stopped at the wrong step:

- **GoHighLevel private-integration token** — no longer used (since 2026-06-30 the channel
  reuses the OAuth account).
- **Kommo API key**, or a GHL/Kommo username and password.
- **ManyChat API key** — this one does exist, but the user types it into Ninjo; it never
  passes through the model.

## Reference table

| Integration | How it connects | Ninjo screen | What you ask next |
|---|---|---|---|
| GoHighLevel CRM | `create_connect_link(provider:"gohighlevel_crm")` | `automation/crm-sync` | nothing |
| HubSpot CRM | `create_connect_link(provider:"hubspot_crm")` | `automation/crm-sync` | nothing |
| Kommo CRM | `create_connect_link(provider:"kommo_crm")` | `automation/crm-sync` | nothing |
| GoHighLevel booking | `create_connect_link(provider:"gohighlevel_booking")` | `integrations/gohighlevel` | **which calendars** |
| Calendly | `create_connect_link(provider:"calendly")` | `integrations/calendly` | **which event types** |
| ManyChat | no link — API key in the UI | `integrations/manychat` | nothing (verify with status) |

Routes are `/studio/{influencerId}/…`.

**GHL appears twice and they are not the same thing.** The CRM (contact and opportunity
sync) lives on the automation screen; booking (tracking scheduled calls) lives under
integrations. They are two different OAuth flows writing to different tables. If the user
says "connect GoHighLevel", ask which one before sending the link.

## The link flow (Meta, CRM and booking)

1. `create_connect_link(provider, influencer_id?)` → returns `{ url, expires_at }`.
2. **Hand the URL to the user.** Do not open it yourself. It is single-use and lasts a few
   minutes; if it expires, mint another.
3. The user authorizes at the provider.
4. `get_connection_status` to confirm. **Read `setup_complete`, not `connected`.**

`pending_step` tells you what is missing, so you never have to guess:

| `pending_step` | What you do |
|---|---|
| `authorize_with_connect_link` | send (another) link |
| `reconnect` | link with `reconnect: true` |
| `select_calendars` | `list_booking_calendars` + `set_booking_calendars` |
| `enable_integration_in_studio` | the user re-enables the integration on the screen |
| `paste_api_key_in_studio` | the user fills in the channel and API key (ManyChat only) |
| `null` | genuinely done |

## Booking is fail-closed

An authorized GHL/Calendly account **with no calendars selected tracks not one single
booking**. OAuth alone leaves the integration connected but dead. So:

1. `list_booking_calendars(platform)` → read the **names** back to the user.
2. The user picks in the chat.
3. `set_booking_calendars(platform, calendar_ids)` — replaces the whole selection (it does
   not add to it), and the ids must come from the listing: an invented id is rejected.
4. Only then does `get_connection_status` report `setup_complete: true`.

Never say "it's connected" before step 3. An empty array switches tracking off: send it only
when the user explicitly asks for that.

If `load_failed: true`, the list could not be read (expired token, provider down) — an empty
list there does **not** mean the account has no calendars.

## CRM needs nothing after the OAuth

The callback syncs the pipelines by itself. **Choosing a pipeline and stage is not part of
connecting** — that only comes up when you wire a property action, with `list_crm_pipelines`
/ `list_crm_stages` / `upsert_property_action`.

## ManyChat: the only one typed into Ninjo

It has no OAuth. Send the user to `/studio/{influencerId}/integrations/manychat` and tell
them what they will find there: channel (WhatsApp / Instagram / Facebook), API key (`mc_…`),
base URL (`https://api.manychat.com`) and shared secret. **Do not ask for the key, do not
repeat it, and do not accept it if they send it anyway.**

**One step you cannot verify.** That screen also shows a **webhook URL** the user has to copy
and paste **on the ManyChat side**. Without it not a single message comes in. Since that
happens inside ManyChat's own panel, Ninjo cannot see it: for `manychat`,
`setup_complete: true` means "the channel and key are filled in", not "messages are
arriving". Remind them of that step explicitly, and tell them you cannot confirm it yourself.

With no channel selected the integration routes nothing, so a key on its own still reports
`pending_step: "paste_api_key_in_studio"`.

## Out of scope for now

The **messaging channels** for GHL and Kommo (the agent answering the messages the CRM
relays) are not configured from the MCP: they need a channel type chosen, an agent assigned
and, on GHL, a `webhookUrl` pasted by hand into a GHL Workflow. That happens on the
integration's screen. Airtable has no link yet either.
