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

# GoHighLevel

> Adaptador do GHL: OAuth2 por subconta, espelho de pipelines, estágios e campos, webhook e tools com registro.

Cada tenant liga a sua subconta (location) do GoHighLevel. Os tokens ficam no cofre (`ghl.access`, `ghl.refresh`, ...) e nunca saem pela API. Tudo abaixo, exceto o callback e o webhook, usa a chave admin (`X-Admin-Key`) e exige `ZAPMEOW_MASTER_KEY`.

## Variáveis

| Variável | Para quê |
| - | - |
| `GHL_CLIENT_ID`, `GHL_CLIENT_SECRET` | credenciais do app do Marketplace. Sem elas o OAuth fica desligado (`503`); o PIT, o espelho e o webhook seguem funcionando |
| `GHL_REDIRECT_URI` | padrão `PUBLIC_URL/ghl/oauth/callback`; o portal do Marketplace recusa "ghl" no caminho, então o app usa o alias `PUBLIC_URL/lc/oauth/callback` (webhook em `/lc/webhook`); precisa estar igual no app do Marketplace |
| `GHL_SCOPES` | padrão `contacts.readonly contacts.write opportunities.readonly opportunities.write locations/customFields.readonly` |
| `GHL_POST_INSTALL_URL` | para onde o navegador volta depois de instalar (padrão: página de "conectado") |
| `GHL_WEBHOOK_PUBKEY` | chave pública Ed25519 do `X-GHL-Signature` (padrão: a da documentação da GHL) |
| `GHL_API_BASE`, `GHL_AUTH_BASE` | só para teste (mock) |

## Conectar

1. `GET /admin/tenants/{id}/ghl/install-url?userType=Location` devolve o link de autorização (vale 30 min). `Company` serve para instalação pela agência: o tenant precisa ter `ghlLocationId`, o ZapMeow troca o token da agência por um da subconta e renova os dois sozinho.
2. O cliente abre o link e escolhe a subconta. A GHL volta em `GET /ghl/oauth/callback?code&state`. O `state` é assinado e amarra o retorno ao tenant; se a subconta autorizada não for a do tenant, nada é gravado.
3. O token de acesso dura cerca de 24 h; o de renovação gira a cada uso e o novo é gravado antes de usar o acesso. Há renovação sob demanda (uma por vez por tenant), 401 renova e repete a chamada, e uma rotina a cada 10 min renova o que está perto de vencer. Se a GHL recusa a renovação (`invalid_grant`), a conexão vai para `status: reauth` e o cliente precisa reinstalar.
4. `PUT /admin/tenants/{id}/ghl/pit` com `{"value","locationId"}` liga uma subconta por Private Integration Token (sem renovação). Serve para testar sem criar o app; não use para clientes.

| Rota | O que faz |
| - | - |
| `GET /admin/tenants/{id}/ghl` | estado da conexão (sem token), contagem do espelho, `redirectUri` e `webhookUrl` (`/lc/webhook`) para colar no app |
| `DELETE /admin/tenants/{id}/ghl` | desconecta: apaga tokens, conexão e espelho (`204`) |

## Espelho

| Rota | O que faz |
| - | - |
| `POST /admin/tenants/{id}/ghl/sync` | recarrega pipelines, estágios, campos e oportunidades (até 5000) |
| `GET /admin/tenants/{id}/ghl/pipelines` | pipelines com estágios |
| `GET /admin/tenants/{id}/ghl/fields` | campos personalizados (contato e oportunidade) com opções |
| `GET /admin/tenants/{id}/ghl/opportunities?contactId&pipelineId&stageId&limit` | oportunidades |

A carga inicial roda sozinha depois da instalação. **Webhook**: `POST /ghl/webhook` (URL única do app, em Advanced Settings). Confere o `X-GHL-Signature` (Ed25519, base64, sobre o corpo cru); sem assinatura válida, `401`. Trata `Opportunity*` (cria, atualiza, apaga) e `UNINSTALL` (desconecta). O estágio vindo do webhook atualiza o espelho e dispara o evento `GHLEvent` para quem assinou com `Manager.onGHL` (motor de fluxo), com o estágio anterior. Se o espelho já tinha esse estágio (eco de um movimento feito pelas nossas tools), não há evento. Subconta desconhecida responde `200`, para a GHL não repetir.

> [!WARNING]
>
> Pipelines e campos personalizados **não têm webhook documentado** pela GHL. Eles são recarregados no sync, e sozinhos (no máximo 1 vez por minuto) quando chega um estágio que o espelho não conhece. Campo novo só entra com `POST .../ghl/sync`.

## Tools

`GET /admin/tenants/{id}/ghl/tools` lista nome, descrição, `inputSchema` e o modo atual. `POST /admin/tenants/{id}/ghl/tools/{tool}` com `{"session","input","dryRun"}` chama.

| Tool | Chamada na GHL |
| - | - |
| `mover_oportunidade` `{opportunityId, stageId ou stageName, status?}` | `PUT /opportunities/{id}` |
| `gravar_campo` `{contactId, field, value}` (id, `fieldKey` ou nome; valor ajustado ao tipo e às opções) | `PUT /contacts/{id}` com `customFields` |
| `criar_followup` `{contactId, title, dueDate, body?, assignedTo?}` | `POST /contacts/{id}/tasks` |

**Modo** por tool, na seção `tools` da config (`PUT /admin/tenants/{id}/config/tools`, `{"modes":{"gravar_campo":"executa"}}`):

* `pede_ok` (**padrão**): a chamada é validada e fica em `zm_toolpending` (`202`, com `pendingId` e o plano). Nada vai à GHL até `POST .../ghl/pending/{pid}/approve` (`{"by"}`); o plano é refeito na hora de aprovar. `.../reject` descarta. Aprovar duas vezes executa uma só.
* `executa`: envia direto.
* `dryRun: true`: modo **sombra**; devolve o plano sem enviar e sem criar pendência.

Toda chamada (sombra, pendente, executada, aprovada, rejeitada ou com erro) entra em `zm_toollog` (`GET .../toollog`) com modo, entrada e resultado. `GET .../ghl/pending?status=pending` lista os pedidos.

## O que não é oficial ou não existe

* **Iniciar ligação pelo LC Phone: não há endpoint público** (só dá para registrar ligações já feitas). Por isso não existe tool `ligar_ghl`; a ligação continua pelo `/call` do ZapMeow.
* Criar pipeline ou estágio por API: não existe; o cliente monta na GHL e o ZapMeow só lê.
* WhatsApp por QR (whatsmeow) dentro do ecossistema GHL: nenhum documento da GHL o autoriza nem o proíbe por nome; o risco é da Meta e da revisão do Marketplace. Manter só em instalação privada (até 5 agências) até perguntar por escrito à GHL.
* Não testado contra uma subconta real: formato do `customFields` (`field_value`), eco do webhook e payload (plano ou dentro de `data`; os dois são aceitos) vêm da documentação e do OpenAPI, validados contra um mock.
* Um único processo: o travamento do refresh é em memória. Com mais de uma réplica, dois refresh simultâneos podem invalidar o token.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.