Introduction
Webhooks service is a way for Schoox to provide third-party applications with real-time information. A webhook delivers data to third-party applications as it happens, whereas in typical APIs the customer must poll for data frequently in order to be close to real-time. This makes webhooks more efficient both for the producer (Schoox) and consumers (customer third-party applications).
Webhook Events
Webhook service relies on events. Customer third-party applications can subscribe to any of these events.
User:
- User created (user.created)
- User deactivated (user.deactivated)
Course:
- Course created (course.created)
- Course updated (course.updated)
- Course visibility updated (course.updated.visibility)
- Course status updated (course.updated.status)
- Course assigned to a user (course.user.assigned)
- A user made progress on a course (course.user.progress)
- A user completed a course (course.user.completed)
Curriculum:
- Curriculum created (curriculum.created)
- Curriculum updated (curriculum.updated)
- Curriculum visibility updated (curriculum.updated.visibility)
- Curriculum status updated (curriculum.updated.status)
- Curriculum deleted (curriculum.deleted)
- Curriculum assigned to a user (curriculum.user.assigned)
- A user made progress on a curriculum (curriculum.user.progress)
- A user completed a curriculum (curriculum.user.completed)
Event:
- ILT Event created (ilt.event.created)
- ILT Event updated (ilt.event.updated)
- ILT Event updated status (ilt.event.updated.status)
On the Job Training:
- On the Job training signed by the trainee (ojt.trainee.signed)
Webhook Endpoints
Endpoints are the URLs that messages will be sent to. When you configure a Webhook Endpoint you must provide the URL, name, associated security policy, and the subscribed events.
Every Webhook Endpoint is bound to one or many events. When an event is triggered, if the customer has configured an endpoint bonded with the triggered event, an external POST HTTP request will be made by the webhook service.
To enable Webhooks in your academy, please ask your Schoox representative to activate it.
Creating a Security Policy
Security Policies are internal configurations for endpoints in order to authenticate to an endpoint. One academy could have multiple security policies. An endpoint could be configured with one security policy. The configuration is available only for super admins.
After Webhooks has been enabled in your academy, select Admin in the top navigation bar. Under Webhooks on the left, select Security Policies.
Then Add New.
Fill in the fields provided, and select the desired authentication type from the drop-down menu.
Save when finished.
Creating an Endpoint
Select Admin in the top navigation bar. Under Webhooks on the left, select Endpoints.
Then Add New.
Complete the fields provided, and check the box next to each event that pertains.
Save when finished.
- Endpoint names are visual identifiers for easy separation of endpoints. The Endpoint Name field is mandatory.
- Endpoint URL is the URL of the third-party application that will receive webhook events. The Endpoint URL field is mandatory.
- The Webhook Endpoint will be triggered only for the selected endpoint events. Endpoint Events are mandatory.
Endpoints are inactive by default. Activate each when your are ready to use them.
Request Failure Handling
The Webhook Service expects a response status code in the range 2xx. If the response is not in the accepted range or a timeout occurs (more than 60 seconds to respond) it is marked as failed.
The system will retry sending these events 5 times, with a retry interval of 5 minutes.
Consuming Webhooks
Base Schema
Every triggered webhook have a base JSON body:
{
"event": "course.updated.visibility", "payload": {
...
}
}
“event” value is the triggered event.
“payload” value containing entity data based on the triggered event.
Payload
For every webhook trigger, webhook service builds the corresponding “payload” based on the triggered event. The “payload” may contain multiple entities. For example, if the event is the course.user.progress the system will fill the payload with three entities: course, user, and course progress.
Entity Payload Schema
Course
id: int
title: string
description: string
externalId: string
image: string
status: {'ACTIVE', 'ARCHIVED', 'RETIRED', 'DELETED'} visibilityStatus: {'PUBLIC', 'PRIVATE'}
Course Progress
progress: int
completionDate: "DateTime as string at ISO 8601 format"
certificationVerificationCode: string
Curriculum
id: int
title: string
description: string
externalId: string
image: string
status: {'ACTIVE', 'ARCHIVED'} visibilityStatus: {'PUBLIC', 'PRIVATE'}
Curriculum Progress
progress: int
completionDate: "DateTime as string at ISO 8601 format"
certificationVerificationCode: string
ILT Event
id: int
title: string
description: string
image: string
status: {'ACTIVE', 'ARCHIVED', 'CANCELED', 'DELETED'}
On-the-Job Training
id: int
title: string
User
id: int
firstName: string
lastName: string
email: string
externalIds: string array
Verifying Webhook Messages
Why
Attackers can impersonate webhook services by simply sending fake data to an endpoint. In order to prevent these attacks, the webhook service signs every webhook with a unique 256 signing secret. The signature can be used to verify that the message is produced by Schoox.
Attackers intercept a valid payload - signature and create a similar HTTP request. The payload will pass the verification process. In order to prevent these attacks, the webhook service includes a timestamp of the webhook attempt. The customer can specify a delay tolerance when verifying the webhook.
Those are potential security holes for third-party applications integrating with webhook service. Therefore customers should consider implementing verification.
Verifying
Each webhook call includes three headers:
wh-id: the unique identifier of the webhook to be sent
wh-timestamp: timestamp of request creation (number of seconds since the Unix Epoch January 1 1970 00:00:00 GMT)
wh-signature: base64 encoded signature
Webhook service generates a signature using a hash-based authentication code (HMAC) with SHA-256.
In order to verify that a message has been transmitted by Schoox, you should also sign the data and compare your signature with the signature in wh-signature header.
- Check timestamp has an acceptable tolerance.
- Create the signable content. The signable content is wh-id, wh-timestamp, request
payload concatenated by dot (.).
e.g. ‘61d39.1639960072.{"event": "course.created", “payload”: {}}‘ - Sign content using your endpoint secret, the secret has the structure whsec_xxxxxx you
should sign content with the key after whsec_
e.g for whsec_QEMBXPKpqJdcCNHgFqiFdz7G0apKrSNP you should sign with QEMBXPKpqJdcCNHgFqiFdz7G0apKrSNP - Compare signatures.
Code Samples
PHP
// Verifying timestamp
$tolerance = 5*60; // 5 minutes
$timestamp = (int)$_SERVER['HTTP_WH-TIMESTAMP'];
$now = time(); if ($timestamp < ($now - $tolerance) { throw new RuntimeException("Message timestamp too old"); }
if ($timestamp > ($now + $tolerance) {
throw new RuntimeException("Message timestamp too new");
}
// Verifying signature
$id = $_SERVER['HTTP_WH-ID'];
$incommingSignature = $_SERVER['HTTP_WH-SIGNATURE'];
$parsedSignature = explode(',', $incommingSignature, 2)[1]
$secret = 'QEMBXPKpqJdcCNHgFqiFdz7G0apKrSNP';
$signable = "{$id}.{$timestamp}.{$payload}";
$hexHash = hash_hmac('sha256', $signable, $secret);
$signature = base64_encode(pack('H*', $hexHash));
if (hash_equals($signature, $parsedSignature)) {
echo "Webhook message is verified and safe to use!"
}
NodeJS / Javascript
import * as utf8 from "@stablelib/utf8";
import * as base64 from "@stablelib/base64";
import * as sha256 from "fast-sha256";
// Verifying timestamp
const tolerance = 5 * 60; // 5 minutes
const timestamp = parseInt(req.headers['wh-timestamp'], 10);
const now = Math.floor(Date.now() / 1000);
if (timestamp < (now - tolerance) {
throw new Error("Message timestamp too old");
}
if (timestamp > (now + tolerance) {
throw new Error("Message timestamp too new");
}
// Verifying signature
const id = req.headers['wh-id'];
const incommingSignature = req.headers['wh-signature'];
const parsedSignature = incommingSignature.split(",")[1];
const secret = 'QEMBXPKpqJdcCNHgFqiFdz7G0apKrSNP';
const signable = utf8.encode(`${id}.${timestamp}.${payload}`);
const signature = base64.encode(sha256.hmac(secret, signable));
if (signature === parsedSignature)) {
console.log("Webhook message is verified and safe to use!");
}
C# Web Server
C# Webhook Controller