EduShell
Webhooks

Webhooks

Get a signed HTTP callback the moment something happens in your organisation, instead of polling the API.


Overview

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.

Events

Subscribe to any of these event types:

EventFired 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

Payload

EduShell s a JSON body. The envelope is the same for every event; carries the event-specific fields:

json
{
  "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:

HeaderValue
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.

Verify the signature

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.

go
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))
}
javascript
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));
}
python
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.

Retries and the delivery log

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.