# Integração — guia para o desenvolvedor
Este documento é tudo o que você precisa para integrar uma plataforma de vendas
ao **UTM Intelligence**. Não é preciso conhecer o sistema por dentro.
**Painel:** https://utmfy.hnvag0.easypanel.host
Ao final da integração, cada venda que entrar vai:
1. aparecer no painel em segundos, já atribuída à campanha/anúncio que a gerou;
2. disparar notificação no celular do dono (push), Telegram, Discord ou e-mail.
---
## Visão geral
São duas peças independentes. Você pode fazer só a primeira e o sistema já
funciona — a segunda é o que permite saber **de qual anúncio** veio cada venda.
| Peça | O que faz | Onde vive |
|---|---|---|
| **1. Webhook de vendas** | Manda a venda para o sistema | Servidor da plataforma de checkout |
| **2. Script de rastreamento** | Identifica o visitante e guarda a origem | Página de vendas do cliente |
---
## Onde pegar as credenciais
No painel, em **Configurações → Instalação**:
| Credencial | Formato | Para que serve |
|---|---|---|
| Chave pública | 32 caracteres | Vai no script de rastreamento (pode ficar exposta) |
| Chave secreta | 48 caracteres | Vai na URL do webhook e na API. **Nunca exponha no navegador.** |
---
# Parte 1 — Webhook de vendas
## Endpoint
```
POST https://utmfy.hnvag0.easypanel.host/webhook/generic/{CHAVE_SECRETA}
Content-Type: application/json
```
Responde `200 {"ok":true}` imediatamente e processa em segundo plano — a venda
aparece no painel em poucos segundos. Se receber qualquer coisa diferente de
`200`, **reenvie**.
## Corpo da requisição
```json
{
"order_id": "PED-12345",
"status": "approved",
"payment_method": "pix",
"currency": "BRL",
"total": 497.00,
"fee": 44.68,
"cost": 40.00,
"product": {
"id": "curso-completo",
"name": "Curso Completo"
},
"customer": {
"email": "cliente@email.com",
"name": "Maria Silva",
"country": "BR"
},
"tracking": {
"vid": "8f14e45f-ceea-467a-9d1a-2f3b4c5d6e7f",
"sid": "3c59dc04-8e88-4c04-9f31-b2f0e1a2c3d4",
"utm_source": "facebook",
"utm_medium": "cpc",
"utm_campaign": "Black Friday",
"utm_content": "criativo-video-01",
"click_id": "IwAR3xK9..."
},
"items": [
{ "id": "curso-completo", "name": "Curso Completo", "quantity": 1, "price": 497.00 },
{ "id": "bump-planilhas", "name": "Planilhas", "quantity": 1, "price": 47.00, "is_bump": true }
],
"created_at": "2026-08-01T14:32:10-03:00",
"approved_at": "2026-08-01T14:32:45-03:00"
}
```
### Campos
| Campo | Obrigatório | Observação |
|---|:---:|---|
| `order_id` | **sim** | ID do pedido na sua plataforma. É a chave de idempotência. |
| `status` | **sim** | Ver tabela de status abaixo. |
| `total` | **sim** | **Na unidade da moeda**: `497.00` = R$ 497,00. Ver aviso abaixo. |
| `payment_method` | não | `pix`, `credit_card`, `boleto`, `debit_card`, `paypal`… |
| `currency` | não | Padrão `BRL`. |
| `fee` | não | Taxa do gateway. Entra no cálculo de lucro. |
| `cost` | não | Custo do produto. Entra no cálculo de lucro. |
| `product.id` / `product.name` | não | Alimenta o ranking de produtos. |
| `customer.email` | não | Usado para reconhecer o comprador entre dispositivos. |
| `tracking.*` | não | Ver Parte 2. É o que liga a venda ao anúncio. |
| `items[]` | não | Detalhe por item; use `is_bump` para order bumps. |
| `created_at` / `approved_at` | não | ISO 8601 ou timestamp Unix. Padrão: agora. |
> ### ⚠️ O erro mais comum: valor em centavos
>
> `total` é lido **na unidade da moeda**. `497.00` e `497` são ambos
> **R$ 497,00** — não R$ 4,97.
>
> Se a sua plataforma trabalha em centavos, **divida por 100 antes de enviar**:
> `"total": 49700` seria interpretado como R$ 49.700,00.
### Status aceitos
| Envie | Significado | Sinônimos aceitos |
|---|---|---|
| `approved` | Pagamento confirmado | `paid`, `completed`, `succeeded`, `aprovado`, `pago` |
| `pending` | Aguardando pagamento | (padrão quando não reconhecido) |
| `refused` | Recusado | `declined`, `failed`, `recusado` |
| `refunded` | Reembolsado | `refund`, `estornado`, `reembolsado` |
| `chargeback` | Contestação | `dispute` |
| `canceled` | Cancelado | `cancelled`, `cancelado` |
| `expired` | Expirado | `expirado` |
Só `approved` conta como faturamento.
## Idempotência e ciclo de vida
**Reenvie o mesmo `order_id` a cada mudança de status.** O sistema atualiza o
pedido existente em vez de duplicar.
Fluxo típico de um Pix:
```
1. Pix gerado → {"order_id":"PED-1","status":"pending", "total":497.00}
2. Pix pago → {"order_id":"PED-1","status":"approved", "total":497.00}
3. Reembolso → {"order_id":"PED-1","status":"refunded", "total":497.00}
```
Duas proteções que você não precisa implementar do seu lado:
- **Reenvio duplicado** não gera venda dupla nem alerta repetido.
- **Webhook fora de ordem** não regride o status: se o "aprovado" chegar depois
do "reembolsado", o pedido continua reembolsado.
## Assinatura (recomendado)
Para o sistema recusar requisições forjadas, configure um segredo compartilhado
e envie o HMAC do corpo cru:
```
X-Signature: hmac_sha256(corpo_cru_da_requisicao, SEGREDO)
```
```php
$body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$signature = hash_hmac('sha256', $body, $segredo);
// envie $body como corpo e $signature no header X-Signature
```
Sem segredo configurado no painel, a verificação é ignorada — útil em testes.
## Testando
```bash
curl -X POST "https://utmfy.hnvag0.easypanel.host/webhook/generic/SUA_CHAVE_SECRETA" \
-H "Content-Type: application/json" \
-d '{
"order_id": "TESTE-001",
"status": "approved",
"payment_method": "pix",
"total": 497.00,
"product": { "id": "teste", "name": "Produto de Teste" },
"customer": { "email": "teste@exemplo.com" }
}'
```
Esperado: `{"ok":true}`, e em poucos segundos a venda aparece no painel com o
alerta/push correspondente.
---
# Parte 2 — Rastreamento e atribuição
Sem esta parte, todas as vendas aparecem como **tráfego direto**. Com ela,
você sabe qual anúncio pagou cada venda.
## Passo 1 — Instalar o script na página de vendas
```html
```
Sozinho, ele já:
- registra as visitas e as sessões;
- captura `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`;
- captura os IDs de clique: `fbclid`, `gclid`, `gbraid`, `wbraid`, `ttclid`,
`msclkid`, `twclid`, `epik`, `sccid`;
- guarda essa origem por até 13 meses (cookie de 1ª parte + localStorage);
- deduz a origem de tráfego orgânico e social quando não há UTM;
- **acrescenta `utmi_vid` e `utmi_sid` aos links que apontam para o checkout.**
## Passo 2 — Levar os identificadores até o webhook
É o passo que fecha o ciclo. Existem três cenários — use o que se aplica:
### Cenário A — O checkout é uma plataforma externa (Hotmart, Kiwify…)
O script já anexa `utmi_vid` e `utmi_sid` na URL do checkout. Configure a
plataforma para repassar esses parâmetros no webhook, no campo de rastreamento
que ela oferecer (`sck`, `src`, `tracking`, `custom_fields`…):
```json
"tracking": { "vid": "", "sid": "" }
```
### Cenário B — Checkout próprio
Leia os identificadores no navegador e envie junto com o pedido:
```js
const ids = utmi('ids'); // { vid: "...", sid: "..." }
const utms = utmi('params'); // { utm_source: "...", fbclid: "...", ... }
fetch('/api/criar-pedido', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...dadosDoPedido, tracking: { ...ids, ...utms } })
});
```
Guarde `tracking` junto do pedido no seu banco e repasse no webhook.
### Cenário C — Não dá para passar nada
Envie ao menos o e-mail do comprador em `customer.email`. O sistema tenta casar
com um visitante conhecido (funciona inclusive entre celular e desktop).
> **Se a venda chegar antes do rastreamento**, o sistema guarda os
> identificadores e corrige a atribuição sozinho minutos depois. Você não
> precisa controlar essa ordem.
## Eventos opcionais
Melhoram o funil e a detecção de queda de conversão:
```js
utmi('track', 'initiate_checkout'); // entrou no checkout
utmi('track', 'lead', { origem: 'formulario' }); // lead capturado
utmi('track', 'add_to_cart', {}, 197.00); // com valor
utmi('identify', 'cliente@email.com'); // liga o visitante ao e-mail
```
Ou sem JavaScript, por atributo:
```html
```
`initiate_checkout` também é detectado automaticamente em links cuja URL
contenha `checkout`, `carrinho`, `pagamento` ou `pay.`.
---
# Parte 3 — API (opcional)
Base `https://utmfy.hnvag0.easypanel.host/api/v1` ·
Header `Authorization: Bearer {CHAVE_SECRETA}`
| Endpoint | Uso |
|---|---|
| `POST /orders` | Criar/atualizar venda sem webhook (mesmo corpo da Parte 1) |
| `POST /ad-spend` | Importar investimento do Meta/Google Ads (lote de até 5.000 linhas) |
| `GET /metrics` | Métricas consolidadas do período |
| `GET /ranking` | Ranking por anúncio, campanha, produto, origem… |
| `GET /orders` | Listar vendas |
| `GET /health` | Monitoramento (sem autenticação) |
Importar gasto de mídia é o que habilita ROAS, ROI, CAC e a lista de anúncios
que estão queimando verba:
```bash
curl -X POST "https://utmfy.hnvag0.easypanel.host/api/v1/ad-spend" \
-H "Authorization: Bearer SUA_CHAVE_SECRETA" \
-H "Content-Type: application/json" \
-d '{ "rows": [{
"date": "2026-08-01", "source": "facebook",
"campaign_id": "1202100001", "campaign_name": "Black Friday",
"ad_id": "1202100001-ad1", "ad_name": "Criativo A",
"spend": 450.00, "impressions": 38000, "clicks": 620
}] }'
```
Idempotente por dia + anúncio: reenviar o mesmo dia atualiza, não duplica.
Referência completa em [`API.md`](API.md).
---
# Códigos de resposta
| Código | O que significa | O que fazer |
|---|---|---|
| `200` | Recebido | Nada |
| `401` | Chave inválida | Conferir a chave secreta na URL/header |
| `404` | Gateway não registrado | Usar `/webhook/generic/…` |
| `422` | Corpo inválido | Ler o campo `error` da resposta |
| `429` | Excesso de requisições | Aguardar e reenviar |
| `5xx` | Falha do servidor | **Reenviar** — o sistema é idempotente |
---
# Checklist de entrega
- [ ] Webhook disparando em **todas** as mudanças de status, não só na aprovação
- [ ] `total` na unidade da moeda (`497.00`), não em centavos
- [ ] Mesmo `order_id` reenviado a cada atualização
- [ ] Reenvio automático quando a resposta não for `200`
- [ ] Script de rastreamento na página de vendas
- [ ] `utmi_vid` / `utmi_sid` chegando no `tracking` do webhook
- [ ] `customer.email` enviado (atribuição entre dispositivos)
- [ ] `fee` e `cost` preenchidos, se houver (necessários para lucro real)
- [ ] Venda de teste aparecendo no painel
---
# Perguntas frequentes
**A venda apareceu como "direct". Por quê?**
Nenhum identificador chegou no `tracking`. Confira o Cenário A/B da Parte 2.
Aguarde alguns minutos: se o rastreamento chegar depois, o sistema corrige
sozinho.
**Posso mandar o mesmo pedido várias vezes?**
Sim — é o comportamento esperado. A chave é o `order_id`.
**O valor apareceu errado (100× maior ou menor).**
Unidade do `total`. Envie `497.00`, não `49700`.
**Preciso do script se o checkout é externo?**
Sim. É ele que identifica o visitante antes de a venda existir. Sem ele, não há
como saber de qual anúncio veio.
**Reembolso desconta do faturamento?**
Sim. Reenvie o mesmo `order_id` com `status: "refunded"`.