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 frequently to stay 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)
Learning path:
- Learning path created (curriculum.created)
- Learning path updated (curriculum.updated)
- Learning path visibility updated (curriculum.updated.visibility)
- Learning path status updated (curriculum.updated.status)
- Learning path deleted (curriculum.deleted)
- Learning path assigned to a user (curriculum.user.assigned)
- A user made progress on a Learning path (curriculum.user.progress)
- A user completed a Learning path (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)
Skill:
- Skill created (skill.created)
- Skill updated (skill.updated)
- Skill deleted (skill.deleted)
- Skill assigned to a user (skill.user.assigned)
- Skill unassigned from a user (skill.user.unassigned)
Skill Association:
- Skill course association created (skill.course.association.created)
- Skill course association deleted (skill.course.association.deleted)
- Skill job association created (skill.job.association.created)
- Skill job association updated (skill.job.association.updated)
- Skill job association deleted (skill.job.association.deleted)
- Skill exam question association created (skill.exam.question.association.created)
- Skill exam question association deleted (skill.exam.question.association.deleted)
- Skill micro step association created (skill.micro.step.association.created)
- Skill micro step association updated (skill.micro.step.association.updated)
- Skill micro step association deleted (skill.micro.step.association.deleted)
- Skill micro course association created (skill.micro.course.association.created)
- Skill micro course association updated (skill.micro.course.association.updated)
- Skill micro course association deleted (skill.micro.course.association.deleted)
- Skill micro assessment association created (skill.micro.assessment.association.created)
- Skill micro assessment association updated (skill.micro.assessment.association.updated)
- Skill micro assessment association deleted (skill.micro.assessment.association.deleted)
- Skill micro assessment step association created (skill.micro.assessment.step. association.created)
- Skill micro assessment step association updated (skill.micro.assessment.step. association.updated)
- Skill micro assessment step association deleted (skill.micro.assessment.step. association.deleted)
Skill Group:
- Skill group created (skill.group.created)
- Skill group updated (skill.group.updated)
- Skill group deleted (skill.group.deleted)
- Skill added to group (skill.group.skill.added)
- Skill removed from group (skill.group.skill.removed)
Skill Category:
- Skill category created (skill.category.created)
- Skill category updated (skill.category.updated)
- Skill category deleted (skill.category.deleted)
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 have been enabled in your academy, select Security Policies.
From the Academy Workspace, select Academy Admin -> Configuration -> Webhooks
Go to the Security Policies tab and select Add New.
Fill in the fields provided, and select the desired authentication type from the drop-down menu.
Save when finished.
Authentication Types
The following authentication types are supported:
| Type | Description |
| BASIC | HTTP Basic Authentication using username and password |
| DIGEST | HTTP Digest Authentication using username and password |
| NTLM | NTLM Authentication using username and password |
| TOKEN | Token-based authentication with optional prefix (e.g., Bearer) |
| OAUTH | OAuth 2.0 authentication using client credentials grant |
OAuth Configuration Options
When configuring OAuth authentication, the following options are available:
| Field | Required | Description |
| OAuth URL | Yes | The token endpoint URL |
| Client ID | Yes | OAuth client identifier |
| Client Secret | Yes | OAuth client secret |
| Grant Type | Yes | OAuth grant type (supports client_credentials) |
| Audience | No | OAuth audience parameter |
| Scope | No | OAuth scope parameter |
| Resource | No | OAuth resource parameter |
| Extra Headers | No | Additional headers for the OAuth request as JSON |
Creating an Endpoint
From the Webhooks setting, go to the Endpoints tab.
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 you're ready to use it.
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, the 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
Learning path
id: int
title: string
description: string
externalId: string
image: string
status: {'ACTIVE', 'ARCHIVED'} visibilityStatus: {'PUBLIC', 'PRIVATE'}
Learning Path 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
Skill
id: int
status: {'ACTIVE', 'ARCHIVED', 'DELETED'}
externalId: string
academyId: int
externalIds: string array
createdAt: "DateTime as string in ISO 8601 format"
updatedAt: "DateTime as string in ISO 8601 format"
translations: array of Skill Translation objects
Skill Translation
languageId: int
languageName: string
skillId: int
name: string
description: string
Skill Group
id: int
name: string
externalId: string
academyId: int
Skill Category
id: int
name: string
externalId: string
academyId: int
Skill Course Association
sourceType: string (value: 'COURSE')
sourceId: int
academyId: int
skillId: int
createdAt: "DateTime as string in ISO 8601 format"
Skill Job Association
id: int
sourceId: int
skillId: int
weight: float
createdAt: "DateTime as string in ISO 8601 format"
Skill Exam Question Association
sourceType: string (value: 'EXAM_QUESTION')
sourceId: int
examQuestionId: int
metricId: int
academyId: int
Skill Entity Association
Used for micro step, micro course, micro assessment, and micro assessment step associations.
id: int
sourceType: {'MICRO_COURSE', 'COURSE', 'EXAM', 'MICRO_STEP', 'OJT', 'JOB',
'PERFORMANCE_REVIEW', 'MICRO_ASSESSMENT', 'MICRO_ASSESSMENT_STEP'}
sourceId: int
academyId: int
skillId: int
createdAt: "DateTime as string in ISO 8601 format"
creatorId: int
Job
id: int
title: string
externalId: string
deletable: boolean
createdAt: string
updatedAt: string
Exam
id: int
name: string
description: string
externalId: string
type: string
publish: string
createdAt: string
updatedAt: string
Exam Question
id: int
type: string
question: string
details: string
points: int
creatorId: int
academyId: int
modifiedAt: string
Micro Course
id: int
title: string
description: string
externalId: string
visibility: string
status: string
image: string
createdAt: string
updatedAt: string
Micro Step
id: int
microCourseId: int
stepType: string
stepOrder: int
visibility: string
createdAt: string
updatedAt: string
Micro Assessment
id: int
title: string
description: string
externalId: string
status: string
image: string
createdAt: string
updatedAt: string
Micro Assessment Step
id: int
microAssessmentId: int
stepType: string
stepOrder: int
visibility: string
createdAt: string
updatedAt: string
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-bit signing secret. The signature can be used to verify that the message was 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 the webhook service. Therefore, customers should consider implementing verification.
Verifying
Each webhook call includes three headers:
| Header | Description |
| 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 with version prefix (e.g., v1,base64signature) |
Webhook service generates a signature using a hash-based authentication code (HMAC) with SHA-256.
To verify that a message has been transmitted by Schoox, you should also sign the data and compare your signature with the signature in the wh-signature header.
Steps:
- Check that the timestamp has an acceptable tolerance.
- Create the signable content. The signable content is wh-id, wh-timestamp, request payload concatenated by a 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.
Signature Format
The signature in the wh-signature header follows this format:
v1,<base64_encoded_signature>Where v1 is the signature version.
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