# Integração de CRM — Guia de Envio de Dados Este documento descreve como uma plataforma de CRM deve enviar dados para a nossa plataforma. O envio é feito por um único endpoint de webhook, autenticado por **HMAC-SHA256**, e os dados são gravados nas tabelas-fonte da empresa correspondente. --- ## 1. Visão geral - Existe **um único endpoint** que recebe todos os tipos de dado. - Cada requisição informa um campo `type` que define **o que** está sendo enviado (`product`, `client` ou `sale`) e um objeto `data` com os campos. - A autenticação é por **segredo HMAC exclusivo de cada empresa**. O segredo é entregue de forma segura para a empresa e **nunca trafega na requisição**. - A empresa de destino é identificada na **URL** (`{companyId}`). Os dados de uma empresa nunca se misturam com os de outra. --- ## 2. Endpoint ``` POST /v1/webhooks/crm/{companyId} ``` | Item | Valor | |---|---| | Método | `POST` | | Caminho | `/v1/webhooks/crm/{companyId}` | | `{companyId}` | Identificador numérico da empresa (fornecido por nós). | | `Content-Type` | `application/json` | | Header de assinatura | `X-Signature: sha256=` | | Corpo | JSON no formato do envelope (ver seção 4). | > Cada empresa recebe o seu `companyId` e o seu segredo HMAC. Use sempre o par > correto: o segredo de uma empresa só é válido para o `companyId` dela. --- ## 3. Autenticação (HMAC-SHA256) Cada requisição precisa provar que é autêntica e não foi adulterada. Para isso: 1. Monte o corpo JSON da requisição (a string exata que será enviada). 2. Calcule `HMAC_SHA256(corpo, segredo)` em hexadecimal. 3. Envie o resultado no header `X-Signature`, no formato `sha256=`. Nós recalculamos a assinatura do corpo recebido com o segredo da empresa e comparamos. Se não bater, a requisição é rejeitada com **401**. ### Regras importantes - A assinatura é calculada sobre o **corpo exato em bytes** (o mesmo que vai no `--data`). Qualquer diferença (espaço, quebra de linha, reordenação) muda a assinatura. **Assine exatamente o que enviar.** - O prefixo `sha256=` é aceito (e recomendado); enviar só o hex também funciona. - O segredo é secreto: nunca o coloque na URL, em logs ou no corpo. ### Exemplos de geração da assinatura **bash / openssl** ```bash SECRET="" BODY='{"type":"product","data":{"name":"Plano Pro","value":199.90}}' SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}') curl -X POST "https:///v1/webhooks/crm/3" \ -H "Content-Type: application/json" \ -H "X-Signature: sha256=$SIG" \ --data "$BODY" ``` **Node.js** ```js const crypto = require('crypto'); const body = JSON.stringify({ type: 'product', data: { name: 'Plano Pro', value: 199.9 } }); const sig = crypto.createHmac('sha256', SECRET).update(body).digest('hex'); // header: 'X-Signature': `sha256=${sig}` | enviar exatamente `body` no POST ``` **PHP** ```php $body = json_encode(['type' => 'product', 'data' => ['name' => 'Plano Pro', 'value' => 199.90]]); $sig = hash_hmac('sha256', $body, $secret); // header: "X-Signature: sha256=$sig" | enviar exatamente $body no POST ``` **Python** ```python import hmac, hashlib, json body = json.dumps({"type": "product", "data": {"name": "Plano Pro", "value": 199.90}}) sig = hmac.new(secret.encode(), body.encode(), hashlib.sha256).hexdigest() # header: {"X-Signature": f"sha256={sig}"} | enviar exatamente `body` no POST ``` --- ## 4. Formato do corpo (envelope) Todo corpo segue o mesmo formato: ```json { "type": "product | client | sale", "data": { ... campos conforme o type ... } } ``` - `type` (string, obrigatório): o tipo do dado. - `data` (objeto, obrigatório): os campos daquele tipo. --- ## 5. Regras gerais de campos - **Campos essenciais** são obrigatórios. Se faltarem (ou vierem vazios), a requisição é rejeitada com **400**. - **Campos opcionais** podem vir como `""` ou ser omitidos — nesse caso assumem um valor neutro (string vazia, `0`, ou um valor derivado). - Valores monetários aceitam número ou string numérica (ex.: `199.90` ou `"199.90"`); valores não numéricos viram `0`. - Datas devem estar em formato reconhecível (ISO 8601 recomendado, ex.: `2026-06-11T14:30:00`). --- ## 6. Tipos de dado ### 6.1 `product` — Produto / SKU Cadastra ou atualiza um produto. A identidade é o **nome** dentro da empresa: se já existe um produto com o mesmo `name`, ele é **atualizado**; senão, é criado. | Campo | Obrigatório | Tipo | Descrição / comportamento | |---|---|---|---| | `name` | ✅ | string | Nome do produto. Identidade do SKU. | | `value` | ❌ | número | Preço. Vazio/ausente → `0`. | | `line` | ❌ | string | Linha/categoria. Vazio/ausente → `""`. | | `sold` | ❌ | inteiro | Quantidade vendida. Vazio/ausente → `0`. **Sobrescreve** o valor atual (ver aviso abaixo). | > ⚠️ **Sobre `sold`:** o `product` **substitui** o contador `sold` pelo valor > enviado. Se você omitir `sold`, ele vira `0`. Já o tipo `sale` **incrementa** > esse contador a cada venda. Para não zerar a contagem sem querer, escolha uma > estratégia: ou você gerencia `sold` só pelo `product` (mandando sempre o total > acumulado), ou deixa o `sold` por conta dos eventos `sale` e **não** envia > `sold` em atualizações de produto. **Exemplo** ```json { "type": "product", "data": { "name": "Plano Pro", "value": 199.90, "line": "Assinaturas", "sold": 0 } } ``` Resposta: `S_CREATED` (criado) ou `S_OK` (atualizado), com `data.sku_id`. --- ### 6.2 `client` — Cliente Cadastra ou atualiza um cliente. A identidade é o **telefone** dentro da empresa (`company_id` + `phone`): mesmo telefone → atualiza; senão → cria. | Campo | Obrigatório | Tipo | Descrição / comportamento | |---|---|---|---| | `phone` | ✅ | string | Telefone. Chave única do cliente. | | `name` | ❌ | string | Nome. Vazio/ausente → `""`. | | `email` | ❌ | string | E-mail. Vazio/ausente → `""`. | | `segment` | ❌ | string | Segmento. Vazio/ausente → `""`. | | `provider_id` | ❌ | string | ID do cliente no CRM. Vazio/ausente → derivado como `crm:`. | **Exemplo** ```json { "type": "client", "data": { "phone": "+5511999990001", "name": "Maria Silva", "email": "maria@exemplo.com", "segment": "Premium", "provider_id": "CRM-CLI-4521" } } ``` Resposta: `S_OK`, com `data.client_id`. --- ### 6.3 `sale` — Venda Registra uma venda no histórico. Cada venda é um **evento com data**, o que permite apurar faturamento por período. É **idempotente** pela chave `company_id` + `external_id`: reenviar a mesma venda **não duplica** o faturamento. | Campo | Obrigatório | Tipo | Descrição / comportamento | |---|---|---|---| | `external_id` | ✅ | string | ID da venda no CRM. Garante a idempotência. | | `amount` | ✅ | número | Valor faturado. Deve ser numérico. | | `occurred_at` | ✅ | data/hora | Quando a venda ocorreu (ISO 8601). Base do histórico. | | `product_name` | ❌ | string | Nome do produto vendido. Se casar com um SKU, vincula e incrementa `sold`. Sem match → venda sem produto vinculado. | | `quantity` | ❌ | inteiro | Quantidade. Vazio/ausente → `1`. | | `client_phone` | ❌ | string | Telefone do cliente. Se casar, vincula a venda ao cliente. | | `operator_email` | ❌ | string | E-mail do operador/vendedor. Se casar, vincula ao operador. | **Comportamento da idempotência** - Venda nova → resposta `S_CREATED` com `data.sale_id`. - Reenvio do mesmo `external_id` → resposta `S_OK` ("Sale already recorded.") com `data.sale_id: 0`. O faturamento e o `sold` **não** são contados de novo. **Exemplo** ```json { "type": "sale", "data": { "external_id": "PEDIDO-2026-0001", "amount": 199.90, "occurred_at": "2026-06-11T14:30:00", "product_name": "Plano Pro", "quantity": 1, "client_phone": "+5511999990001", "operator_email": "vendedor@empresa.com" } } ``` Resposta: `S_CREATED` ou `S_OK`, com `data.sale_id`. --- ## 7. Respostas e códigos de status Toda resposta segue o formato padrão: ```json { "status": "ok | failed", "code": "S_OK | S_CREATED | E_VALIDATE | E_NOT_FOUND | E_GENERIC", "message": "mensagem legível", "data": { ... } // presente quando há dados } ``` | HTTP | `code` | Quando acontece | |---|---|---| | 200 | `S_OK` | Processado com sucesso (atualização ou reenvio idempotente). | | 200 | `S_CREATED` | Registro novo criado. | | 400 | `E_VALIDATE` | JSON inválido, falta `type`/`data`, campo essencial ausente, `type` desconhecido, ou `companyId` inválido. | | 401 | `E_VALIDATE` | Assinatura HMAC inválida ou ausente. | | 404 | `E_NOT_FOUND` | `companyId` não corresponde a nenhuma empresa ativa. | | 500 | `E_GENERIC` | Falha interna ao processar. Pode reenviar. | Exemplos de erro: ```json { "status": "failed", "code": "E_VALIDATE", "message": "Invalid signature" } { "status": "failed", "code": "E_VALIDATE", "message": "sale requires a numeric \"amount\"" } { "status": "failed", "code": "E_NOT_FOUND", "message": "Unknown company" } ``` --- ## 8. Boas práticas e observações - **Ordem de envio:** envie `product` e `client` antes das `sale` que os referenciam, para que a venda já consiga vincular o produto/cliente. - **Reenvio seguro:** vendas têm idempotência por `external_id`. Em caso de timeout ou erro de rede, pode reenviar a mesma venda sem medo de duplicar. - **O que NÃO enviar:** não enviamos/recebemos métricas calculadas (churn, LTV, ticket médio, faturamento consolidado). Essas são derivadas internamente a partir dos dados crus (`product`, `client`, `sale`). Mande apenas os dados brutos. - **Datas:** prefira ISO 8601 com hora (`2026-06-11T14:30:00`). Se o fuso importar, inclua-o. - **Segredo HMAC:** se suspeitar de vazamento, solicite a rotação do segredo da empresa. --- ## 9. Checklist de integração - [ ] Recebi o `companyId` e o segredo HMAC da empresa. - [ ] Monto o corpo JSON e calculo `HMAC_SHA256(corpo, segredo)`. - [ ] Envio `X-Signature: sha256=` e `Content-Type: application/json`. - [ ] Assino exatamente o corpo que envio (sem reserializar depois). - [ ] Uso `type` = `product` | `client` | `sale` com os campos essenciais. - [ ] Trato os códigos de status (200/400/401/404/500). - [ ] Reenvio vendas com o mesmo `external_id` em caso de falha.