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.
type que define o que está sendo
enviado (product, client ou sale) e um objeto data com os campos.{companyId}). Os dados de
uma empresa nunca se misturam com os de outra.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=<hmac> |
| Corpo | JSON no formato do envelope (ver seção 4). |
Cada empresa recebe o seu
companyIde o seu segredo HMAC. Use sempre o par correto: o segredo de uma empresa só é válido para ocompanyIddela.
Cada requisição precisa provar que é autêntica e não foi adulterada. Para isso:
HMAC_SHA256(corpo, segredo) em hexadecimal.X-Signature, no formato sha256=<hex>.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.
--data). Qualquer diferença (espaço, quebra de linha, reordenação) muda a
assinatura. Assine exatamente o que enviar.sha256= é aceito (e recomendado); enviar só o hex também funciona.bash / openssl
SECRET="<SEU_SEGREDO>"
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://<host>/v1/webhooks/crm/3" \
-H "Content-Type: application/json" \
-H "X-Signature: sha256=$SIG" \
--data "$BODY"
Node.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
$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
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
Todo corpo segue o mesmo formato:
{
"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."" ou ser omitidos — nesse caso
assumem um valor neutro (string vazia, 0, ou um valor derivado).199.90 ou
"199.90"); valores não numéricos viram 0.2026-06-11T14:30:00).product — Produto / SKUCadastra 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: oproductsubstitui o contadorsoldpelo valor enviado. Se você omitirsold, ele vira0. Já o tiposaleincrementa esse contador a cada venda. Para não zerar a contagem sem querer, escolha uma estratégia: ou você gerenciasoldsó peloproduct(mandando sempre o total acumulado), ou deixa osoldpor conta dos eventossalee não enviasoldem atualizações de produto.
Exemplo
{
"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.
client — ClienteCadastra 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:<phone>. |
Exemplo
{
"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.
sale — VendaRegistra 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
S_CREATED com data.sale_id.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
{
"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.
Toda resposta segue o formato padrão:
{
"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:
{ "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" }
product e client antes das sale que os
referenciam, para que a venda já consiga vincular o produto/cliente.external_id. Em caso de
timeout ou erro de rede, pode reenviar a mesma venda sem medo de duplicar.product, client, sale). Mande apenas os dados
brutos.2026-06-11T14:30:00). Se o fuso
importar, inclua-o.companyId e o segredo HMAC da empresa.HMAC_SHA256(corpo, segredo).X-Signature: sha256=<hex> e Content-Type: application/json.type = product | client | sale com os campos essenciais.external_id em caso de falha.