Com os avisos automáticos (webhooks), o EduPay chama o seu endereço quando algo acontece na escola: aluno cadastrado, matrícula ativada, contrato assinado, cobrança emitida, parcela paga ou vencida. Assim o seu sistema fica em dia sem consultar a API de tempos em tempos.
Cadastrar o endereço
Pelo painel: Integrações → Avisos automáticos. Ou pela API, com uma chave que tenha webhooks:manage:
curl -X POST https://edupay.estudiosite.com/api/v1/webhooks \
-H "Authorization: Bearer $EDUPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://erp.escola.com.br/edupay/avisos",
"description": "ERP financeiro",
"events": ["installment.paid", "installment.overdue"]
}'
- A resposta traz o segredo (
secret, começa comwhsec_) uma vez só. Guarde-o: é com ele que você confere que o aviso veio do EduPay. - O endereço precisa ser
httpse público. Endereço de rede interna é recusado. - Até 10 endereços por escola. Em
events, liste os eventos ou use["*"]para todos. - Os avisos só saem com a API da escola ligada. O envio de teste funciona sempre.
Eventos
| Evento | Quando acontece | Vem em data |
|---|---|---|
student.created |
Aluno cadastrado (painel, página de vendas, API) | student |
student.updated |
Nome, documento, contato, endereço, situação ou código externo alterado | student |
enrollment.created |
Matrícula criada (painel, página de vendas, API) | enrollment, student |
enrollment.activated |
Matrícula ficou ativa (contrato assinado ou primeiro pagamento) | enrollment, student |
enrollment.suspended |
Acesso ao Moodle bloqueado por atraso | enrollment, student |
enrollment.reactivated |
Acesso ao Moodle liberado depois do pagamento | enrollment, student |
enrollment.cancelled |
Matrícula cancelada | enrollment, student |
enrollment.completed |
Matrícula concluída | enrollment, student |
contract.signed |
Aluno assinou, ou documento em papel aprovado | contract, student |
installment.charge_issued |
Boleto, PIX ou link de cartão gerado | installment, student |
installment.charge_cancelled |
Cobrança cancelada (a parcela continua em aberto) | installment, student |
installment.paid |
Pagamento confirmado: banco, cartório ou baixa manual | installment, student |
installment.overdue |
Parcela venceu sem pagamento (rotina diária, de madrugada) | installment, student |
webhook.test |
Botão "Enviar teste" ou POST /webhooks/{id}/test |
mensagem de teste |
enrollment, contract e installment vêm completos, no mesmo formato da API. Em student vêm id, external_id, full_name e email (nos eventos de aluno, o cadastro completo).
Formato do aviso
POST /edupay/avisos HTTP/1.1
Content-Type: application/json
User-Agent: EduPay-Webhooks/1.0
X-Edupay-Event: installment.paid
X-Edupay-Event-Id: evt_01jbx9m3k2t8f4c6v0w5y7z9qa
X-Edupay-Delivery: 8812
X-Edupay-Signature: t=1793197800,v1=5f2b9c...e81a
{
"id": "evt_01jbx9m3k2t8f4c6v0w5y7z9qa",
"type": "installment.paid",
"created_at": "2026-11-10T13:10:00Z",
"api_version": "v1",
"data": {
"installment": { "id": 9813, "status": "paid", "amount": 45000, "paid_amount": 45000, "currency": "BRL", "...": "..." },
"student": { "id": 532, "external_id": "ALU-0091", "full_name": "Maria Souza", "email": "maria@exemplo.com" }
}
}
Conferir a assinatura
O cabeçalho X-Edupay-Signature traz t (horário, em segundos) e v1: o HMAC-SHA256, com o segredo, de t + . + o corpo exatamente como chegou. Confira antes de qualquer outra coisa e recuse aviso com mais de 5 minutos.
PHP:
$secret = getenv('EDUPAY_WEBHOOK_SECRET');
$body = file_get_contents('php://input'); // o corpo cru, sem decodificar
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_EDUPAY_SIGNATURE'] ?? ''), $sig);
$expected = hash_hmac('sha256', ($sig['t'] ?? '') . '.' . $body, $secret);
if (! hash_equals($expected, $sig['v1'] ?? '') || abs(time() - (int) ($sig['t'] ?? 0)) > 300) {
http_response_code(400);
exit;
}
$event = json_decode($body, true);
// ... grave $event['id'] e processe ...
http_response_code(200);
Node.js (Express):
const crypto = require('crypto');
app.post('/edupay/avisos', express.raw({ type: 'application/json' }), (req, res) => {
const sig = Object.fromEntries(
(req.get('X-Edupay-Signature') || '').split(',').map((p) => p.split('='))
);
const expected = crypto
.createHmac('sha256', process.env.EDUPAY_WEBHOOK_SECRET)
.update(`${sig.t}.${req.body}`)
.digest('hex');
const ok = sig.v1 && sig.v1.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig.v1))
&& Math.abs(Date.now() / 1000 - Number(sig.t)) <= 300;
if (!ok) return res.sendStatus(400);
const event = JSON.parse(req.body);
// ... grave event.id e processe ...
res.sendStatus(200);
});
Responder e processar
- Responda 2xx em até 10 segundos. Redirecionamento conta como falha. Se o processamento é demorado, grave o aviso, responda 200 e processe depois.
- O mesmo aviso pode chegar mais de uma vez (nova tentativa, reenvio pelo painel). Use o
iddo evento para não processar duas vezes. - A ordem de chegada não é garantida. Na dúvida, consulte o registro na API (
GET /installments/{id}) para ter o estado atual.
Novas tentativas
Se o seu endereço não responder 2xx, o EduPay tenta de novo em 1 min, 5 min, 30 min, 2 h, 12 h e 24 h. Depois disso a entrega fica como falha e pode ser reenviada pelo painel.
Um endereço que passa 3 dias falhando, sem nenhum sucesso, é desligado, e o dono da conta da escola recebe um e-mail. Ele é religado pelo painel.
O histórico de entregas (situação, resposta e tentativas) fica 30 dias no painel e em GET /webhooks/{id}/deliveries.
Trocar o segredo
No painel, Trocar segredo gera um segredo novo, que passa a valer na hora. Atualize o seu sistema logo em seguida.