Webhooks
Receive real-time events from Campus One
Webhooks let Campus One push events to your app as they happen — no polling required. Configure a webhook URL in the developer dashboard under your app's Webhooks tab.
Setup
- Expose an HTTPS endpoint on your server (e.g.
https://ct.campusone.com.ng/webhooks/campus-one) - Open the developer dashboard → your app → Webhooks tab
- Paste the URL and select the events you want to receive
- Copy your Webhook Secret — it is generated when your app is created and shown once. Store it as
CAMPUS_ONE_WEBHOOK_SECRET. Rotate via Rotate webhook secret if leaked.
Request format
Each event is an HTTP POST with a JSON body and these headers:
Content-Type: application/json
X-Campus-One-Event: user.role_changed
X-Campus-One-Signature: sha256=abc123...
X-Campus-One-Delivery: 6f4d2c3a-...Example payload:
{
"id": "6f4d2c3a-1a2b-4c5d-9e8f-0a1b2c3d4e5f",
"event": "user.role_changed",
"occurredAt": "2026-05-21T14:22:00.000Z",
"data": {
"user_id": "user_abc123",
"email": "256240001@nileuniversity.edu.ng",
"previous_role": "student",
"new_role": "mentor"
}
}Signature verification
Always verify the X-Campus-One-Signature header before processing a webhook. The signature is an HMAC-SHA256 of the raw request body, keyed with your CAMPUS_ONE_WEBHOOK_SECRET.
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyWebhook(rawBody: string, secret: string, header: string): boolean {
const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`;
const a = Buffer.from(header);
const b = Buffer.from(expected);
if (a.length !== b.length) {
return false;
}
return timingSafeEqual(a, b);
}
app.post(
"/webhooks/campus-one",
express.raw({ type: "application/json" }),
(req, res) => {
const sig = req.headers["x-campus-one-signature"] as string;
if (!verifyWebhook(req.body.toString(), process.env.CAMPUS_ONE_WEBHOOK_SECRET!, sig)) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body.toString());
// handle event...
res.status(200).send("ok");
}
);Use timingSafeEqual to prevent timing attacks. Campus One also publishes a ready-made verifier you can use directly:
import { verifyWebhook } from "@campus-one/auth/webhooks";Events reference
| Event | When it fires |
|---|---|
user.created | A new user is provisioned in Campus One. |
user.updated | The user's profile (name, email, programme, etc.) changes. |
user.deleted | The user's account is permanently removed. |
user.role_changed | The user's primary role changes — re-fetch the id_token or call /userinfo to re-authorise. |
session.signed_in | The user signed in to Campus One — useful for analytics/audit. |
session.signed_out | The user signed out. |
Payloads include a data object with the relevant fields for the event. All events share the envelope shown above (id, event, occurredAt, data).
Single logout (SLO)
When a user signs out of Campus One, you should sign them out of your app on the same device. Campus One drives this two ways:
- Central token revocation. On sign-out (and on RP-initiated logout via the
end_session_endpoint), Campus One deletes all of the user's access and refresh tokens. Any attempt to refresh an expired token afterwards will fail — so even apps that don't subscribe to webhooks lose access at the next token refresh. - The
session.signed_outwebhook. Subscribe to this event to clear the user's session in your app immediately. Thedata.user_ididentifies who signed out. This fires both when the user signs out of Campus One and when any connected app initiates logout, so every subscribed app is notified.
To let your users sign out of Campus One from your app (and thereby propagate to their other apps), redirect them to the OIDC end_session_endpoint (advertised in /.well-known/openid-configuration):
GET https://auth.campusone.com.ng/api/auth/oauth2/endsession
?id_token_hint=<the id_token you received at login>
&client_id=<your client id>This clears the Campus One session (an ecosystem-wide logout), revokes the user's tokens, and fans out session.signed_out to other apps.
Where the user lands after logout
Most connected apps have no login screen of their own, so after logout there's nowhere sensible to send the user. Campus One gives you two options:
1. Opt-in redirect (recommended — no URL to register). In the developer dashboard, open your app → Sign-In → Sign-out redirect and choose Return to Campus One directory (or a Custom URL). Then just send users to the end-session endpoint without a post_logout_redirect_uri — Campus One clears the session and redirects to your chosen destination automatically:
// In your app's "Log out" handler:
const idToken = getStoredIdToken();
window.location.href =
"https://auth.campusone.com.ng/api/auth/oauth2/endsession" +
`?id_token_hint=${idToken}&client_id=${CAMPUS_ONE_CLIENT_ID}`;
// → Campus One signs the user out everywhere, then redirects to the
// Campus One app directory (or your configured custom URL).Because logout clears the whole Campus One session, a user sent to the directory who is now signed out will see the Campus One sign-in page — expected, since they've left the ecosystem.
2. Standard OIDC post_logout_redirect_uri. If you want to send users to a specific page of your own, register that URL in your app's Redirect URLs first, then pass it:
GET …/oauth2/endsession?id_token_hint=<id_token>&post_logout_redirect_uri=<a registered URL>[!WARNING] Passing a
post_logout_redirect_urithat isn't in your registered Redirect URLs returnsinvalid_request: post_logout_redirect_uri is not registered for this client. Either register the URL (option 2) or use the opt-in redirect (option 1), which needs nopost_logout_redirect_uriat all.
[!NOTE] Campus One does not implement OIDC back-channel or front-channel logout. Immediate cross-app sign-out relies on your app honouring the
session.signed_outwebhook; without it, access ends when the current token expires and cannot be refreshed.
Delivery semantics
- Deliveries time out after 5 seconds. Endpoints should respond
2xxquickly and process asynchronously if needed. - The current implementation is fire-and-forget — failed deliveries increment your app's
errorCount(shown on the developer dashboard) but are not retried automatically. A retry queue is on the roadmap; in the meantime, design your handler to be tolerant of missed events and reconcile via the REST API on reconnect. - Deliveries are not ordered. Use the
idheader to deduplicate and theoccurredAttimestamp to resolve conflicts.
Testing
While the dashboard's Send test event button is being built, you can verify your endpoint locally by signing a sample payload with your webhook secret and POSTing it yourself:
BODY='{"id":"test","event":"user.created","occurredAt":"2026-05-21T00:00:00Z","data":{}}'
SIG="sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$CAMPUS_ONE_WEBHOOK_SECRET" -hex | cut -d' ' -f2)"
curl -X POST https://ct.campusone.com.ng/webhooks/campus-one \
-H "Content-Type: application/json" \
-H "X-Campus-One-Event: user.created" \
-H "X-Campus-One-Signature: $SIG" \
-H "X-Campus-One-Delivery: $(uuidgen)" \
-d "$BODY"Public App API Reference
Access live interactive OpenAPI documentation, raw specification files, and standard HTTP headers for the Campus One Connected App API.
Programmatic Notifications
Learn how to programmatically dispatch secure, real-time push notifications to Campus One users from your connected applications.