Desarrolladores · Documentación de la API

Avisos automáticos

Avisos automáticos (webhooks) de EduPay: eventos, firma HMAC, reintentos y ejemplos en PHP y Node.js.

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 con whsec_) una sola vez. Guárdelo: con él usted comprueba que el aviso vino de EduPay.
  • La dirección tiene que ser https y 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 id del 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.