Developers · API documentation

Webhooks

EduPay webhooks: events, HMAC signature, retries and examples in PHP and Node.js.

With webhooks, EduPay calls your endpoint when something happens at the school: a student is created, an enrollment is activated, a contract is signed, a charge is issued, an installment is paid or becomes overdue. This keeps your system up to date without polling the API.

Register the endpoint

In the panel: Integrations → Webhooks. Or through the API, with a key that has 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"]
  }'
  • The response contains the secret (secret, starts with whsec_) only once. Keep it: it is how you check that a notification came from EduPay.
  • The endpoint must be https and public. Internal network addresses are rejected.
  • Up to 10 endpoints per school. In events, list the events or use ["*"] for all of them.
  • Notifications are only sent while the school's API is turned on. Test sends always work.

Events

Event When it happens Comes in data
student.created Student created (panel, sales page, API) student
student.updated Name, document, contact details, address, status or external code changed student
enrollment.created Enrollment created (panel, sales page, API) enrollment, student
enrollment.activated Enrollment became active (contract signed or first payment) enrollment, student
enrollment.suspended Moodle access blocked for late payment enrollment, student
enrollment.reactivated Moodle access restored after payment enrollment, student
enrollment.cancelled Enrollment cancelled enrollment, student
enrollment.completed Enrollment completed enrollment, student
contract.signed Student signed, or paper document approved contract, student
installment.charge_issued Boleto, PIX or card link generated installment, student
installment.charge_cancelled Charge cancelled (the installment stays open) installment, student
installment.paid Payment confirmed: bank, registry office or manual payment entry installment, student
installment.overdue Installment became overdue without payment (daily routine, overnight) installment, student
webhook.test "Send test" button or POST /webhooks/{id}/test test message

enrollment, contract and installment come complete, in the same format as the API. student has id, external_id, full_name and email (in student events, the full record).

Notification format

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" }
  }
}

Verify the signature

The X-Edupay-Signature header has t (time, in seconds) and v1: the HMAC-SHA256, with the secret, of t + . + the body exactly as it arrived. Check it before anything else and reject notifications older than 5 minutes.

PHP:

$secret    = getenv('EDUPAY_WEBHOOK_SECRET');
$body      = file_get_contents('php://input');           // the raw body, not decoded
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);
// ... store $event['id'] and process ...
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);
  // ... store event.id and process ...
  res.sendStatus(200);
});

Respond and process

  • Respond 2xx within 10 seconds. A redirect counts as a failure. If processing takes long, store the notification, respond 200 and process it later.
  • The same notification may arrive more than once (retry, resend from the panel). Use the event id so you don't process it twice.
  • Arrival order is not guaranteed. When in doubt, fetch the record from the API (GET /installments/{id}) to get its current state.

Retries

If your endpoint does not respond 2xx, EduPay tries again after 1 min, 5 min, 30 min, 2 h, 12 h and 24 h. After that, the delivery is marked as failed and can be resent from the panel.

An endpoint that keeps failing for 3 days, with no success, is turned off, and the school's account owner gets an e-mail. It is turned back on in the panel.

The delivery history (status, response and attempts) is kept for 30 days in the panel and in GET /webhooks/{id}/deliveries.

Rotate the secret

In the panel, Rotate secret generates a new secret, which takes effect immediately. Update your system right after.