Con los avisos automáticos (webhooks), EduPay llama a su dirección cuando algo ocurre en la escuela: alumno registrado, matrícula activada, contrato firmado, cobro emitido, cuota pagada o vencida. Así su sistema se mantiene al día sin consultar la API de vez en cuando.
Registrar la dirección
Por el panel: Integraciones → Avisos automáticos. O por la API, con una clave que tenga 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"]
}'
- La respuesta trae el secreto (
secret, empieza conwhsec_) una sola vez. Guárdelo: con él usted comprueba que el aviso vino de EduPay. - La dirección tiene que ser
httpsy pública. Se rechaza una dirección de red interna. - Hasta 10 direcciones por escuela. En
events, liste los eventos o use["*"]para todos. - Los avisos solo salen con la API de la escuela activada. El envío de prueba funciona siempre.
Eventos
| Evento | Cuándo ocurre | Viene en data |
|---|---|---|
student.created |
Alumno registrado (panel, página de ventas, API) | student |
student.updated |
Nombre, documento, contacto, dirección, estado o código externo modificado | student |
enrollment.created |
Matrícula creada (panel, página de ventas, API) | enrollment, student |
enrollment.activated |
La matrícula quedó activa (contrato firmado o primer pago) | enrollment, student |
enrollment.suspended |
Acceso a Moodle bloqueado por atraso | enrollment, student |
enrollment.reactivated |
Acceso a Moodle liberado después del pago | enrollment, student |
enrollment.cancelled |
Matrícula cancelada | enrollment, student |
enrollment.completed |
Matrícula concluida | enrollment, student |
contract.signed |
El alumno firmó, o se aprobó un documento en papel | contract, student |
installment.charge_issued |
Boleto, PIX o enlace de tarjeta generado | installment, student |
installment.charge_cancelled |
Cobro cancelado (la cuota sigue pendiente) | installment, student |
installment.paid |
Pago confirmado: banco, notaría o registro manual | installment, student |
installment.overdue |
La cuota venció sin pago (rutina diaria, de madrugada) | installment, student |
webhook.test |
Botón "Enviar prueba" o POST /webhooks/{id}/test |
mensaje de prueba |
enrollment, contract e installment vienen completos, en el mismo formato de la API. En student vienen id, external_id, full_name y email (en los eventos de alumno, el registro completo).
Formato del 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" }
}
}
Verificar la firma
El encabezado X-Edupay-Signature trae t (hora, en segundos) y v1: el HMAC-SHA256, con el secreto, de t + . + el cuerpo exactamente como llegó. Verifíquela antes de cualquier otra cosa y rechace un aviso con más de 5 minutos.
PHP:
$secret = getenv('EDUPAY_WEBHOOK_SECRET');
$body = file_get_contents('php://input'); // el cuerpo crudo, sin 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);
// ... guarde $event['id'] y procese ...
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);
// ... guarde event.id y procese ...
res.sendStatus(200);
});
Responder y procesar
- Responda 2xx en hasta 10 segundos. Una redirección cuenta como falla. Si el procesamiento tarda, guarde el aviso, responda 200 y procese después.
- El mismo aviso puede llegar más de una vez (nuevo intento, reenvío desde el panel). Use el
iddel evento para no procesarlo dos veces. - El orden de llegada no está garantizado. Ante la duda, consulte el registro en la API (
GET /installments/{id}) para tener el estado actual.
Nuevos intentos
Si su dirección no responde 2xx, EduPay intenta de nuevo en 1 min, 5 min, 30 min, 2 h, 12 h y 24 h. Después de eso la entrega queda como fallida y se puede reenviar desde el panel.
Una dirección que pasa 3 días fallando, sin ningún éxito, se desactiva, y el titular de la cuenta de la escuela recibe un e-mail. Se reactiva desde el panel.
El historial de entregas (estado, respuesta e intentos) queda 30 días en el panel y en GET /webhooks/{id}/deliveries.
Cambiar el secreto
En el panel, Cambiar secreto genera un secreto nuevo, que pasa a valer en el acto. Actualice su sistema enseguida.