Webhooks
Como avisar automaticamente outro sistema quando um aluno é criado, ganha acesso ou perde acesso na sua plataforma.
Nesta página
- Criar um webhook
- Gerenciar os webhooks
- Eventos disponíveis
- Formato do envio
- O objeto user
- Exemplo: user.created
- Exemplo: enrollment.created (por venda)
- Exemplo: enrollment.created (manual, gratuito ou importação)
- Exemplo: enrollment.revoked
- Dados pessoais no envio
- Verificar a assinatura
- Respostas, novas tentativas e histórico
- Testar um webhook
- Webhook legado
- Avisos de vendas recebidas (postback)
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.
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
- No painel, acesse Configurações › Webhooks.
- Clique em Novo webhook.
- Em Para onde enviar, preencha o Endpoint (o endereço que vai receber os avisos, começando com
http://ouhttps://) e, se quiser, uma Descrição (ex.: "automação de boas-vindas"). - Em Quando enviar, marque os eventos que devem gerar um aviso. Use Marcar todos para marcar um grupo inteiro.
- Em O que cada envio leva, escolha quanto dado pessoal vai em cada aviso (veja Dados pessoais no envio).
- 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).
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).
- 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 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 |
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.
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) |
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) |
name, email, document, phone | Dados do cadastro |
magic_login_url | Link de login automático 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
{
"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:
{
"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:
{
"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:
{
"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.
Se o seu sistema só precisa saber que algo aconteceu, fique com Só identificadores e busque o restante pela API 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):
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);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:
{
"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 é:
{
"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 |
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).
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 e, para integrações próprias, em Entrega Customizada.