Get a signed HTTP callback the moment something happens in your organisation, instead of polling the API.
A webhook endpoint is a URL of yours that EduShell calls with a whenever an event you subscribed to occurs. Register one in Administration > Webhooks: enter the URL, choose the events, and EduShell shows a signing secret once (it looks like ). Store it, you will need it to verify deliveries.
Each delivery is retried on failure and recorded in a delivery log you can inspect and replay.
Subscribe to any of these event types:
| Event | Fired when |
|---|---|
| A member's role changes | |
| A member is removed | |
| Org ownership is transferred | |
| Organisation settings change | |
| The organisation is closed | |
| An OIDC connection is removed | |
| A SAML connection is removed | |
| An LTI registration is removed | |
| An API key is created | |
| An API key is revoked | |
| A webhook endpoint is created | |
| A webhook endpoint is deleted |
EduShell s a JSON body. The envelope is the same for every event; carries the event-specific fields:
{
"id": "a3f1c9e2-7b40-4c1a-9e6d-2b8f0a1c4d5e",
"event": "member.removed",
"org_id": "3d2c1b0a-9f8e-7d6c-5b4a-3c2d1e0f9a8b",
"occurred_at": "2026-07-29T14:12:05Z",
"data": { "user_id": "7c6b5a49-2f10-4e8b-b1a2-6d5c4b3a2f10" }
}
Each request also carries headers:
| Header | Value |
|---|---|
| The event type, for example | |
| A unique delivery id (use it to dedupe) | |
| followed by the HMAC of the body |
Respond with any status to acknowledge. Anything else, or a timeout, counts as a failure and is retried.
Every delivery is signed so you can trust it came from EduShell. The header is followed by the hex HMAC-SHA256 of the raw request body, keyed with your endpoint's signing secret. Compute the same HMAC and compare, using a constant-time comparison.
func verify(secret string, body []byte, header string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(header))
}
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, body, header) {
const digest = createHmac("sha256", secret).update(body).digest("hex");
const expected = `sha256=${digest}`;
return expected.length === header.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}
import hashlib, hmac
def verify(secret: str, body: bytes, header: str) -> bool:
digest = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(f"sha256={digest}", header)
Sign over the raw bytes of the request body, before any JSON parsing or re-serialisation. Parsing and re-encoding can change whitespace or key order and break the signature.
If your endpoint does not return a , or times out, EduShell retries with an exponential backoff (starting at about 30 seconds and doubling up to a cap, several times over roughly a day). To stay correct under retries, make your handler idempotent: dedupe on .
Every attempt is recorded in the delivery log under the endpoint, with the response code and timing. You can resend any delivery by hand, and rotate the signing secret (the new secret is shown once). Rotate immediately if you suspect the secret leaked.