# Webhooks

> Como avisar automaticamente outro sistema quando um aluno é criado, ganha acesso ou perde acesso na sua plataforma.

Um webhook é um aviso automático: quando algo acontece na sua plataforma (um aluno novo, um acesso liberado, um acesso retirado), ela envia na hora uma requisição `POST` com os dados do evento para um endereço do seu sistema. Assim o seu CRM, sua automação de WhatsApp ou seu ERP ficam sabendo sem precisar consultar a [API](/configuracoes/api).

:::aviso Exige conhecimento técnico
Para receber webhooks você precisa de um endereço na internet preparado para receber requisições (um sistema próprio ou uma ferramenta de automação que ofereça "webhook de entrada").
:::

## Criar um webhook

1. No painel, acesse **Configurações › Webhooks**.
2. Clique em **Novo webhook**.
3. Em **Para onde enviar**, preencha o **Endpoint** (o endereço que vai receber os avisos, começando com `http://` ou `https://`) e, se quiser, uma **Descrição** (ex.: "automação de boas-vindas").
4. Em **Quando enviar**, marque os eventos que devem gerar um aviso. Use **Marcar todos** para marcar um grupo inteiro.
5. Em **O que cada envio leva**, escolha quanto dado pessoal vai em cada aviso (veja [Dados pessoais no envio](#dados-pessoais-no-envio)).
6. Clique em **Criar webhook**.

Logo após criar, a plataforma mostra o **Segredo de assinatura** (começa com `whsec_`). Ele aparece **só nessa hora**: copie e guarde, porque é com ele que o seu sistema confere se o aviso veio mesmo da sua plataforma (veja [Verificar a assinatura](#verificar-a-assinatura)).

### Gerenciar os webhooks

Na aba **Endpoints**, cada webhook mostra o endereço, os eventos marcados e o status (**Ativo** ou **Desativado**). No menu **···** de cada um você pode:

- **Editar**: mudar endereço, descrição, eventos, nível de dados e ligar ou desligar o webhook (caixa **Ativo**). Na mesma tela, **Gerar novo segredo de assinatura** troca o segredo (o anterior deixa de valer na hora).
- **Enviar teste**: manda um aviso de teste para o endereço (veja [Testar](#testar-um-webhook)).
- **Desativar** / **Ativar**: pausa os envios sem apagar a configuração.
- **Excluir**: remove o webhook.

## Eventos disponíveis

Hoje a plataforma dispara estes eventos:

| Evento | Nome na tela | Quando dispara |
|---|---|---|
| `user.created` | Aluno criado | Um aluno novo é cadastrado por uma venda (postback de qualquer gateway ou a rota de [Entrega](/configuracoes/api-entrega) da API) ou pelo cadastro gratuito |
| `enrollment.created` | Acesso liberado | O aluno ganha acesso: por uma venda aprovada, pelo cadastro gratuito, por liberação manual no painel ou por importação |
| `enrollment.revoked` | Acesso revogado | Um acesso é encerrado: venda cancelada, reembolsada, com chargeback ou em disputa, assinatura cancelada ou em atraso, ou retirada manual no painel |

:::info
Alunos criados à mão no painel ou pela importação ainda não disparam `user.created`. O acesso liberado para eles dispara `enrollment.created` normalmente.
:::

:::embreve Outros eventos da tela
A tela de criação já lista os eventos abaixo, mas eles **ainda não são enviados**. Se você marcar algum, nada é disparado por enquanto.

- **Cadastro:** Tags do aluno atualizadas (`user.tags_updated`), Campo personalizado atualizado (`user.custom_field_updated`).
- **Acesso:** Primeiro acesso (`user.first_accessed`), Acesso suspenso ou reativado (`enrollment.updated`), Acesso restaurado (`enrollment.restored`).
- **Progresso:** Curso iniciado (`product_progress.started`), Aula concluída (`lesson_progress.completed`), Marco de progresso atingido (`product_progress.milestone_reached`), Progresso atualizado (`product_progress.updated`), Curso concluído (`product_progress.completed`).
- **Provas:** Prova concluída (`exam_attempt.completed`), Tentativa de prova reaberta (`exam_attempt.reopened`).
- **Certificados:** Certificado emitido (`certificate.issued`), Certificado revogado (`certificate.revoked`).
- **Termos:** Termo aceito (`term_acceptance.created`).
- **Pontos:** Pontos creditados (`score.created`), Pontos ajustados (`score.adjusted`).
- **Atendimento:** Chamado aberto (`ticket.created`), Chamado respondido (`ticket.reply_created`), Chamado encerrado (`ticket.closed`), Dúvida enviada (`question.created`), Dúvida respondida (`question.reply_published`), Comentário enviado (`comment.created`), Comentário respondido (`comment.reply_created`).
:::

## Formato do envio

Cada aviso é um `POST` com corpo JSON e estes cabeçalhos:

| Cabeçalho | Valor |
|---|---|
| `Content-Type` | `application/json` |
| `User-Agent` | `ASALA-Hub-Webhooks/1.0` |
| `X-Webhook-Signature` | `sha256=<assinatura>` (veja [Verificar a assinatura](#verificar-a-assinatura)) |

O corpo sempre tem o mesmo envelope:

| Campo | Descrição |
|---|---|
| `id` | Identificador único do envio. Use para ignorar avisos repetidos |
| `type` | Nome do evento (ex.: `enrollment.created`) |
| `created_at` | Quando o evento aconteceu (ISO-8601, horário de Brasília) |
| `instance` | Identificador da sua plataforma (o subdomínio) |
| `data` | Os dados do evento |

### O objeto user

Os três eventos trazem o aluno em `data.user`:

| Campo | Descrição |
|---|---|
| `id` | ID do aluno (o mesmo usado na [API](/configuracoes/api-usuario)) |
| `name`, `email`, `document`, `phone` | Dados do cadastro |
| `magic_login_url` | Link de [login automático](/configuracoes/api#login-automatico-do-aluno) do aluno |
| `access.first_at` / `access.last_at` | Primeiro e último login (`null` se nunca entrou), cada um com `unix` e `iso` |
| `created.on` | Origem do cadastro: `sale` (venda), `api`, `import`, `free` (cadastro gratuito) ou `manual` |
| `created.at` | Data do cadastro, com `unix` e `iso` |

### Exemplo: user.created

```json
{
  "id": "01JA9K3V2M4F7C1D8E2B5A6C9D",
  "type": "user.created",
  "created_at": "2026-10-15T10:42:07-03:00",
  "instance": "escola",
  "data": {
    "user": {
      "id": 1234,
      "name": "Maria Silva",
      "email": "maria@email.com",
      "document": "242.863.810-64",
      "phone": "+5521999990000",
      "magic_login_url": "https://escola.asalahub.com/auth/login?crstk=Xy7...",
      "access": { "first_at": null, "last_at": null },
      "created": {
        "on": "sale",
        "at": { "unix": "1792064527", "iso": "2026-10-15T10:42:07-03:00" }
      }
    },
    "created": { "on": "sale" }
  }
}
```

No cadastro gratuito, `data.created` vem como `{ "on": "free", "method": "form" }`.

### Exemplo: enrollment.created (por venda)

Quando o acesso vem de uma venda, o aviso é enviado **uma vez por entrega** e traz a entrega e a venda:

```json
{
  "id": "01JA9K3V2N8H2K4M6P1Q3R5S7T",
  "type": "enrollment.created",
  "created_at": "2026-10-15T10:42:07-03:00",
  "instance": "escola",
  "data": {
    "user": {
      "id": 1234,
      "name": "Maria Silva",
      "email": "maria@email.com",
      "document": "242.863.810-64",
      "phone": "+5521999990000",
      "magic_login_url": "https://escola.asalahub.com/auth/login?crstk=Xy7...",
      "access": { "first_at": null, "last_at": null },
      "created": {
        "on": "sale",
        "at": { "unix": "1792064527", "iso": "2026-10-15T10:42:07-03:00" }
      }
    },
    "delivery": {
      "id": 21,
      "type": "single",
      "name": "Atendimento que Encanta - Kiwify",
      "config_url": "https://escola.asalahub.com/dashboard/sales/deliveries/21/edit",
      "external_ids": ["a1b2c3d4"]
    },
    "created": { "on": "sale", "by": null },
    "sale": { "id": 845, "code": "PEDIDO-1001", "engine": "kiwify" }
  }
}
```

`delivery.type` é `single` (venda única) ou `subscription` (assinatura). `created.on` pode ser `sale` ou `api` (quando a venda veio pela rota `/entrega/enviar`).

### Exemplo: enrollment.created (manual, gratuito ou importação)

Quando o acesso é liberado no painel, pelo cadastro gratuito ou pela importação, o aviso é enviado **uma vez por curso** e traz o curso e o acesso:

```json
{
  "id": "01JA9K7B4C6D8E0F2G4H6J8K0M",
  "type": "enrollment.created",
  "created_at": "2026-10-15T11:05:30-03:00",
  "instance": "escola",
  "data": {
    "user": { "id": 1234, "name": "Maria Silva", "email": "maria@email.com", "...": "..." },
    "product": {
      "id": 12,
      "order": 1,
      "name": "Atendimento que Encanta",
      "offer_url": null,
      "group": { "id": 4, "order": 1, "name": "Treinamentos" }
    },
    "enrollment": {
      "id": 3310,
      "product_id": 12,
      "delivery_id": null,
      "source": "manual",
      "started_at": "2026-10-15T11:05:30-03:00",
      "ends_at": "2027-10-15T11:05:30-03:00"
    },
    "created": { "on": "manual", "by": 1 }
  }
}
```

`enrollment.source` (e `created.on`) é `manual`, `free` ou `import`. `ends_at` nulo significa acesso vitalício. Em `created.by` vem o ID do administrador que liberou à mão. `product.group` é a vitrine do curso.

### Exemplo: enrollment.revoked

Enviado uma vez para cada acesso encerrado:

```json
{
  "id": "01JA9M2Q8R0S2T4V6W8X0Y2Z4A",
  "type": "enrollment.revoked",
  "created_at": "2026-10-20T09:12:44-03:00",
  "instance": "escola",
  "data": {
    "user": { "id": 1234, "name": "Maria Silva", "email": "maria@email.com", "...": "..." },
    "product": {
      "id": 12,
      "order": 1,
      "name": "Atendimento que Encanta",
      "offer_url": null,
      "group": { "id": 4, "order": 1, "name": "Treinamentos" }
    },
    "enrollment": {
      "id": 3309,
      "product_id": 12,
      "delivery_id": 21,
      "ended_at": "2026-10-20T09:12:44-03:00",
      "reason": "refunded"
    }
  }
}
```

Valores comuns de `reason`: `refunded` (reembolso), `canceled` (cancelamento), `chargeback`, `dispute` (reembolso solicitado ou disputa), `subscription_canceled` (assinatura cancelada), `subscription_late` (assinatura em atraso) e `admin` (retirado à mão no painel).

## Dados pessoais no envio

Em **O que cada envio leva** você escolhe, para todos os eventos daquele webhook, como os dados do aluno chegam:

| Opção | O que acontece em `data.user` |
|---|---|
| **Só identificadores, sem dados pessoais** (padrão) | Nome, e-mail, documento, telefone e link de login **não são enviados**. Chegam o `id`, as datas de acesso e a origem do cadastro |
| **Com dados pessoais mascarados** | Nome, e-mail, documento e telefone chegam escondidos em parte (ex.: `ma**@e**.com`, `Ma** Si**`). O link de login não é enviado |
| **Completo, com dados pessoais** | Todos os dados como estão no cadastro, incluindo `magic_login_url` |

Os dados que não são pessoais (nome do curso, da entrega, códigos da venda) sempre vão completos. Os exemplos acima usam a opção **Completo**.

:::dica
Se o seu sistema só precisa saber *que* algo aconteceu, fique com **Só identificadores** e busque o restante pela [API](/configuracoes/api-usuario) usando o `id` do aluno. É a opção mais segura.
:::

## Verificar a assinatura

Todo envio leva o cabeçalho `X-Webhook-Signature` com o valor `sha256=` seguido de um HMAC-SHA256 do **corpo da requisição exatamente como chegou**, calculado com o seu segredo de assinatura e escrito em hexadecimal. Se a assinatura que você calcular for igual à recebida, o aviso veio da sua plataforma e não foi alterado no caminho.

Exemplo em Node.js (Express):

```js
import express from 'express';
import crypto from 'node:crypto';

const app = express();
const SEGREDO = process.env.WEBHOOK_SECRET; // o whsec_... mostrado ao criar o webhook

// Importante: ler o corpo cru, antes de qualquer conversão para JSON
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const recebida = String(req.get('X-Webhook-Signature') || '');
  const esperada = 'sha256=' + crypto.createHmac('sha256', SEGREDO).update(req.body).digest('hex');

  const a = Buffer.from(recebida);
  const b = Buffer.from(esperada);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(401).send('assinatura inválida');
  }

  const evento = JSON.parse(req.body.toString('utf8'));
  // Use evento.id para não processar o mesmo aviso duas vezes
  console.log(evento.type, evento.data);

  res.sendStatus(200);
});

app.listen(3000);
```

:::aviso
Calcule a assinatura sobre o corpo **bruto**. Se você converter para objeto e depois para texto de novo, a ordem ou os espaços podem mudar e a assinatura não vai bater. A assinatura não inclui data e hora, por isso guarde os `id` já processados e ignore repetidos.
:::

## Respostas, novas tentativas e histórico

- O seu endereço deve responder com um código **2xx** (por exemplo, `200`) em até **10 segundos**. Qualquer outro código, falta de resposta ou redirecionamento (3xx) conta como falha.
- Em caso de falha, a plataforma tenta de novo automaticamente, até **5 tentativas** no total: a 2ª cerca de 30 segundos depois, a 3ª após 2 minutos, a 4ª após 10 minutos e a 5ª após 1 hora. Os envios pendentes ficam guardados e continuam mesmo se o servidor reiniciar.
- Desativar ou excluir o webhook cancela os envios que ainda estavam esperando nova tentativa.

Na aba **Histórico** ficam os envios dos **últimos 60 dias**, com data, evento, endereço, código de resposta e número de tentativas. Filtre por **Sucesso**, **Pendentes** ou **Com falha** e clique em **Ver envio** para ver o corpo enviado e a resposta do seu sistema.

## Testar um webhook

No menu **···** do webhook, clique em **Enviar teste**. A plataforma manda um aviso para o endereço com o primeiro evento marcado, assinado com o seu segredo, e o resultado aparece no **Histórico**. O teste é enviado uma vez só (sem novas tentativas) e tem este formato:

```json
{
  "id": "TESTE1792064527000",
  "type": "user.created",
  "test": true,
  "data": {}
}
```

Use o campo `test` para não tratar esse aviso como um evento real.

## Webhook legado

O botão **Novo webhook legado** cria um webhook no formato antigo (versão 2), para integrações que já esperam esse formato. Nele os dados vão sempre completos e o corpo é:

```json
{
  "version": "2",
  "event": "delivery.add",
  "event_id": "01JA9K3V2N8H2K4M6P1Q3R5S7T",
  "payload": { "user": { }, "delivery": { }, "created": { }, "sale": { } }
}
```

O conteúdo de `payload` é o mesmo descrito nos exemplos acima. A assinatura, as tentativas e o histórico funcionam do mesmo jeito.

Eventos legados que são enviados hoje:

| Evento legado | Corresponde a |
|---|---|
| `user.created` (Aluno criado) | `user.created` |
| `user.created.free` (Aluno gratuito criado) | `user.created` |
| `delivery.add` (Entrega adicionada) | `enrollment.created` |

:::aviso Aluno gratuito criado
No momento, `user.created.free` é enviado para **todo** aluno novo, não só os do cadastro gratuito. Para saber se o aluno é gratuito, confira `payload.user.created.on` (vale `free` no cadastro gratuito).
:::

:::embreve Outros eventos legados
Aluno progresso (`user.progress`), Aula concluída (`class.concluded`), Curso iniciado (`course.initiated`), Curso concluído (`course.concluded`), Curso progresso (`course.progress`), Curso 50%, 75% e 90% (`course.progress.50`, `.75`, `.90`), Prova aprovado (`exam.approved`), Prova reprovado (`exam.reproved`), Certificado emitido (`certificado.emitido`) e Termo assinado (`user.term.signed`) ainda não são enviados.
:::

## Avisos de vendas recebidas (postback)

Os webhooks desta tela são de **saída**: a sua plataforma avisando outro sistema. Para o caminho contrário, quando um gateway de pagamento avisa a sua plataforma sobre uma venda, cada entrega tem o seu próprio endereço de postback, mostrado na tela da entrega:

```
https://<sua-plataforma>/api/postback/<gateway>/<token>
```

Veja como configurar em [Entregas e integrações](/entregas/entregas-e-integracoes) e, para integrações próprias, em [Entrega Customizada](/entregas/custom).
