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

# Sincronizar membros do CRM

> Mantenha sua comunidade Cativa em sync com HubSpot, RD Station ou qualquer CRM via API Key.

A maioria dos parceiros já tem um CRM (HubSpot, RD Station, Pipedrive, ActiveCampaign, Salesforce) que é a fonte de verdade dos contatos. Este guia mostra como manter a comunidade Cativa em sincronia com o CRM — o que dá pra fazer hoje, o que está em desenvolvimento e quais as decisões de design (chave natural, conflitos, idempotência) que você precisa tomar.

## Cenário

Seu time de marketing usa HubSpot. Cada vez que um lead é qualificado e ganha a tag **`Pago Anual`** lá, você quer que essa pessoa apareça na sua comunidade Cativa com o badge `Premium` (que dá acesso ao grupo VIP, ao curso pago, etc.). Quando a tag é removida (ou o cliente cancela), o badge precisa sair.

A Cativa é a fonte de verdade da **comunidade** (quem está no grupo, quem completou qual lição, quem postou o quê). O CRM é a fonte de verdade da **relação comercial** (lead, oportunidade, cliente, churn). A integração faz a ponte entre os dois.

## Pré-requisitos

1. **API Key da Cativa** — gerada no Console (Developers > API Keys). Veja [Quick Start: API Key](/pt-BR/get-started/quickstart-api-key).
2. **Acesso de API ao seu CRM** — token do HubSpot, da RD Station, etc. A Cativa não fornece SDK pra esses CRMs; use os SDKs oficiais (`@hubspot/api-client`, `pipedrive-node-sdk`, etc.).
3. **Um servidor de integração** rodando do seu lado — pode ser um worker Node, um Cloud Function/Lambda, um job no Airflow, qualquer coisa que execute código com acesso aos dois lados.

## Mapeamento conceitual

A pergunta central é: **como reconhecer que o contato `1234` no HubSpot é o mesmo usuário `01HQ7Z3X4Y...` na Cativa?**

| HubSpot          | Cativa                |
| ---------------- | --------------------- |
| Contact ID       | User ID               |
| Email            | Email (chave natural) |
| Tag `Pago Anual` | Badge `Premium`       |

A **chave natural recomendada é o `email`** — ela está dos dois lados desde o cadastro. Você pode opcionalmente criar uma propriedade custom no HubSpot chamada `cativa_user_id` pra cachear o User ID Cativa após o primeiro casamento e evitar lookup por email a cada execução.

<Note>
  A Cativa **não tem hoje um campo `external_id` no User** que permita associar diretamente a chave do seu CRM. A chave natural é o `email`. Quando esse campo for adicionado ao modelo público, esta página será atualizada com a opção de upsert por `external_id`.
</Note>

## Dois fluxos possíveis

<Tabs>
  <Tab title="Pull-based (worker periódico)">
    **Quando usar:** você não tem webhook do CRM disponível, ou os volumes são pequenos (até alguns milhares de contatos), ou um delay de minutos/horas é aceitável.

    Um worker do seu lado roda numa frequência (ex: a cada 15 minutos), lê o estado do CRM, lê o estado da Cativa, calcula o diff e aplica.

    ```
    cron 15 min ─→ pull HubSpot ─→ pull Cativa ─→ diff ─→ apply
    ```

    **Prós:** simples de implementar, fácil de debugar, fácil de fazer backfill.
    **Contras:** delay (não é real-time), gasto desnecessário de API calls quando nada mudou.
  </Tab>

  <Tab title="Webhook-based (event-driven)">
    **Quando usar:** o CRM oferece webhook quando uma propriedade muda (HubSpot Workflow, RD Station Automation), e você quer sincronia em segundos.

    O CRM dispara webhook pro seu servidor quando uma tag muda → seu servidor chama a API da Cativa pra atribuir/remover o badge.

    ```
    HubSpot tag muda ─→ webhook seu servidor ─→ API Cativa
    ```

    **Prós:** baixo delay, eficiente.
    **Contras:** mais código (HMAC verification do webhook do CRM, retry/backoff, dedupe), e ainda assim você quer **um worker pull-based de reconciliação semanal** pra cobrir webhooks perdidos.

    Você também pode subscrever os webhooks **da Cativa** (`user_received_badge`, `user_created`) pra refletir mudanças do lado da comunidade de volta no CRM. A página [Cadastrando e verificando webhooks](/pt-BR/webhooks/subscribing-and-verifying) mostra como verificar a assinatura HMAC dos disparos da Cativa.
  </Tab>
</Tabs>

## Descobrir um usuário existente na Cativa

Hoje há **um endpoint público** que você pode chamar com sua API Key:

`GET /social/v1/auth/me` — retorna o usuário **dono da chave** (não busca usuário arbitrário). Use isso para sanity-check da sua credencial e descobrir o `customer` (tenant) ao qual a chave pertence.

```bash theme={null}
curl https://apis.cativalab.digital/tenant/v1/auth/me \
  -H "Authorization: Bearer cativa_live_..."
```

<Note>
  Para **buscar um usuário arbitrário pelo email** (que é o caso da sincronização CRM), o endpoint público usando API Key está em desenvolvimento. Hoje, esse lookup usa um endpoint admin (`/social/v1/admin/users/email/{email}`) que **não está disponível para API Keys de parceiro** — apenas para o admin do tenant logado no Console.

  **Workaround atual:** alinhe com o time da Cativa em [dev@cativa.digital](mailto:dev@cativa.digital) para liberar o lookup do seu cenário, ou use o evento de webhook `user_created` (vide abaixo) para popular sua própria tabela de mapeamento email → Cativa User ID conforme novos usuários entram.
</Note>

### Estratégia recomendada hoje: tabela de mapeamento populada via webhook

Em vez de fazer lookup por email a cada sync, mantenha uma tabela do seu lado:

```
crm_cativa_mapping
─────────────────
hubspot_contact_id  TEXT
cativa_user_id      UUID
email               TEXT
last_synced_at      TIMESTAMP
```

E popule:

1. **Cadastre um listener Cativa** pro evento `user_created` — toda vez que alguém entra na comunidade, o webhook chega com `User.Id` e `User.Email`. Insere/atualiza essa linha.
2. **Cadastre um listener pro evento `user_received_badge`** — toda mudança de badge atualiza a linha.

Depois disso, seu worker de sync resolve `email` → `cativa_user_id` localmente, sem precisar bater na API da Cativa.

## Criar usuário a partir do CRM

<Note>
  O endpoint público para parceiros **criarem usuários via API Key** está em desenvolvimento. Hoje, a criação acontece quando o próprio usuário se cadastra na comunidade (via Login com a Cativa, convite, ou link público de cadastro do tenant).

  **Workarounds atuais:**

  * **Convite por email** — gere um link de cadastro do tenant e envie via seu CRM (HubSpot Email, RD Station Email). Quando o lead clica e completa o cadastro, o evento `user_created` chega no seu webhook e você consegue dar o badge logo em seguida.
  * **Importação em lote** — para grandes massas iniciais (milhares de contatos), o time da Cativa consegue importar uma planilha via Console. Alinhe em [dev@cativa.digital](mailto:dev@cativa.digital).

  Quando o endpoint público for liberado, esta página será atualizada com cURL e exemplos.
</Note>

## Atribuir badge

<Note>
  A atribuição de badge via API Key de parceiro está em desenvolvimento. Hoje, atribuição/remoção de badges é feita:

  * Manualmente no Console do tenant (admin), ou
  * Via importação em massa (planilha), ou
  * Em produção, via fluxos automáticos da plataforma (compra paywall, conclusão de curso) — não via API direta de parceiro.

  Quando o endpoint público estiver disponível, esta página vai mostrar o cURL completo. Pra liberar o seu caso enquanto isso, abra ticket em [dev@cativa.digital](mailto:dev@cativa.digital).
</Note>

Em código, planeje seu worker assumindo que a chamada vai existir. Esquema mental do que vai parecer:

```js theme={null}
// Quando o endpoint estiver disponível:
async function assignBadge(cativaUserId, badgeId) {
  const res = await fetch(
    `https://apis.cativalab.digital/tenant/v1/.../badges/${badgeId}/users/${cativaUserId}`,
    {
      method: 'POST',
      headers: { Authorization: `Bearer ${process.env.CATIVA_API_KEY}` }
    }
  );
  if (!res.ok) throw new Error(`Badge assign failed: ${res.status}`);
}

async function removeBadge(cativaUserId, badgeId) {
  const res = await fetch(
    `https://apis.cativalab.digital/tenant/v1/.../badges/${badgeId}/users/${cativaUserId}`,
    {
      method: 'DELETE',
      headers: { Authorization: `Bearer ${process.env.CATIVA_API_KEY}` }
    }
  );
  if (!res.ok) throw new Error(`Badge remove failed: ${res.status}`);
}
```

A Cativa garante que **atribuir o mesmo badge duas vezes é idempotente** — o estado final é o mesmo. Mesma coisa pra remover badge que já não está atribuído. Isso simplifica retries no seu worker (ver [Badges como permissão](/pt-BR/concepts/badges-as-permissions)).

## Esqueleto de worker pull-based

Mesmo com os endpoints de write em desenvolvimento, vale modelar o worker hoje. O que está dentro de `assignBadge`/`removeBadge` é o que precisa esperar.

```js theme={null}
import { Client as HubSpotClient } from '@hubspot/api-client';

const hubspot = new HubSpotClient({ accessToken: process.env.HUBSPOT_TOKEN });
const PREMIUM_BADGE_ID = process.env.CATIVA_PREMIUM_BADGE_ID;

async function syncContact(contact) {
  const email = contact.properties.email;
  if (!email) return; // sem email, não tem como casar

  // 1. Resolve email → Cativa User ID (tabela local populada via webhook)
  const mapping = await db.crmCativaMapping.findOne({ email });
  if (!mapping) {
    // Usuário ainda não está na Cativa. Envie um convite via CRM.
    await sendInviteEmail(email);
    return;
  }

  // 2. Estado desejado (HubSpot é a fonte de verdade)
  const shouldHavePremium = (contact.properties.lifecyclestage === 'customer'
    && contact.properties.deal_status === 'paid_annual');

  // 3. Estado atual (sua tabela local, atualizada via webhook user_received_badge)
  const hasPremium = await db.userBadges.exists({
    userId: mapping.cativaUserId,
    badgeId: PREMIUM_BADGE_ID
  });

  // 4. Aplica diff
  if (shouldHavePremium && !hasPremium) {
    await assignBadge(mapping.cativaUserId, PREMIUM_BADGE_ID);
  } else if (!shouldHavePremium && hasPremium) {
    await removeBadge(mapping.cativaUserId, PREMIUM_BADGE_ID);
  }
}

async function runSync() {
  let after;
  do {
    const page = await hubspot.crm.contacts.basicApi.getPage(100, after, [
      'email', 'lifecyclestage', 'deal_status'
    ]);
    for (const contact of page.results) {
      try {
        await syncContact(contact);
      } catch (err) {
        console.error(`Failed to sync ${contact.id}`, err);
        // Não interrompa o loop — registre e siga.
      }
    }
    after = page.paging?.next?.after;
  } while (after);
}
```

## Tratar conflitos

| Cenário                                                             | O que fazer                                                                                                                                                                                                                                                                                               |
| ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Email mudou no CRM, ainda não mudou na Cativa**                   | Cativa é fonte do `cativa_user_id`. Se o email do contato HubSpot mudou e você não consegue mais casar pelo novo email, mantenha o `cativa_user_id` cacheado (no `crm_cativa_mapping`) e atualize a coluna `email` quando for diferente. O usuário Cativa ainda existe — o que mudou foi o ângulo do CRM. |
| **Email do CRM nunca apareceu na Cativa**                           | O contato HubSpot não tem (ainda) um usuário Cativa correspondente. Envie convite ou ignore até ele se cadastrar. Não tente "criar" forçadamente até o endpoint público estar disponível.                                                                                                                 |
| **Mesmo email aparece em dois contatos no CRM**                     | Decisão do seu lado de qual contato é canônico (geralmente o mais recente, ou o que tem `lifecyclestage` mais avançado). Trate antes de chamar a Cativa.                                                                                                                                                  |
| **Cliente cancelou no CRM mas tem badge ativo na Cativa**           | Esse é o caso que o sync de remoção resolve. O fluxo `runSync` acima cobre.                                                                                                                                                                                                                               |
| **Badge atribuído por outra fonte (compra paywall) e CRM não sabe** | A Cativa pode atribuir badges por outros caminhos (compra paywall, conclusão de curso). Se seu CRM **não** for fonte de verdade pra esse badge específico, **não remova** o badge no diff — adicione um filtro do lado seu (ex: só sincronize badges que vieram via tag CRM).                             |

## Erros comuns

<AccordionGroup>
  <Accordion title="O sync derrubou badges atribuídos por outras fontes">
    Quando o seu worker é a única autoridade sobre um badge e ele não vê motivo no CRM, ele remove. Mas se o badge foi atribuído por **outra fonte** (compra paywall, importação manual), o sync apaga indevidamente.

    **Solução:** mantenha uma lista de "badges gerenciados pelo CRM" no seu worker. Só atribua e remova esses. Badges fora da lista são ignorados pelo diff.
  </Accordion>

  <Accordion title="Rate limit ao processar muitos contatos">
    Em syncs grandes (milhares de contatos), você pode bater rate limit no CRM ou na Cativa. Implemente:

    * Backoff exponencial em 429 (a Cativa respeita `Retry-After`).
    * Paginação no CRM (ex: `getPage(100, after, ...)` no HubSpot).
    * Paralelismo controlado (ex: `p-limit(5)` no Node).
  </Accordion>

  <Accordion title="Webhook do user_created não chegou — usuário ficou fora da tabela de mapeamento">
    Se você só popula `crm_cativa_mapping` via webhook, qualquer disparo perdido vira lacuna. Por isso recomendamos:

    * Um **reconcile job semanal** que pega todos os contatos do CRM com email e tenta dar match com os usuários conhecidos.
    * Logging do `X-Cativa-Execution-Id` em todo webhook recebido — se faltar, dá pra abrir ticket de investigação.
  </Accordion>

  <Accordion title="Email diferente entre CRM e Cativa do mesmo usuário">
    Acontece quando o usuário usa email pessoal pra comprar e email corporativo pra entrar na comunidade (ou vice-versa). Não tem solução automática — você precisa **uma propriedade adicional** no CRM (ex: `community_email`) e usar essa pro lookup em vez do email principal.
  </Accordion>
</AccordionGroup>

## Próximos passos

<CardGroup cols={2}>
  <Card title="Subscrever webhooks Cativa" icon="webhook" href="/pt-BR/webhooks/subscribing-and-verifying">
    Configure listeners para `user_created` e `user_received_badge` para manter sua tabela de mapeamento atualizada.
  </Card>

  <Card title="Liberar acesso via compra externa" icon="cart-shopping" href="/pt-BR/guides/grant-access-via-purchase">
    O caso específico de "compra externa → badge" tem padrões e cuidados próprios.
  </Card>
</CardGroup>
