Webhooks
About Webhooks
Webhooks provide a way for notifications to be delivered to your server whenever certain system events occur within the Cognassist Platform.
Currently, the available system events include:
- "Assessment Completed", which is triggered when a learner who belongs to your organisation completes their assessment.
- "Assessment Report Created", which is triggered after a request to generate the assessment report for a learner has successfully completed.
- "Assessment Report Request Failed", which is triggered after a request to generate the assessment report for a learner has failed.
When you create or update a webhook, you can choose which system events you would like to subscribe to. Subscriptions to different system events will trigger different amounts of requests; to limit the number of requests made to your server, please ensure to only subscribe to system events you want to handle.
When we make a request to your server, we record it as an "invocation log". You can use the Cognassist API to retrieve invocation logs for a webhook to check if all of the requests we made were successful.
API Endpoints
Version 1 of the Cognassist API includes endpoints that can be used for managing webhooks.
Getting a List of Available System Events
GET /systemevents
This endpoint will return a list of available system events that can be subscribed to.
Creating a Webhook
POST /webhooks
This endpoint will create a new webhook for your organisation. The following details must be specified in the request body:
- The
SystemEventIds
list allows you to specify one or more system events you would like the webhook to subscribe to. Please use the Getting a List of Available System Events endpoint to get a list of system event ids webhooks can subscribe to. - The
Url
is the URL of the server that will receive the requests from Cognassist when a system event subscription is triggered. - The
Secret
allows you to ensure that the POST requests sent to the specifiedUrl
are coming from Cognassist. Each request will include ax-cognassist-sha256
header with a hash signature. Please see the Request Signature section for more information about the header. - The
Description
property simply allows you to give the webhook a description or an identifier to help you remember what the purpose of the webhook is.
Updating a Webhook
PUT /webhooks/{webhookId}
This endpoint will update an existing webhook. It expects the same details in the request body as the Creating a Webhook endpoint.
Getting a Webhook or a List of Webhooks
GET /webhooks/{webhookId}
This endpoint will return details about the specified webhook.
GET /webhooks
This endpoint will return all the webhooks that were created for your organisation.
Deleting a Webhook
DELETE /webhooks/{webhookId}
This endpoint will delete a single, existing webhook. Please note that all of the invocation logs for the specified webhook will also be deleted.
Getting a Webhook Invocation Log or a List of Webhook Invocation Logs
GET /webhooks/{webhookId}/invocationlogs/{invocationLogId}
This endpoint will return a single invocation log for the specified webhook, including the data that was sent to your server.
GET /webhooks/{webhookId}/invocationlogs
This endpoint will return a paginated list of all the invocation logs for the specified webhook. The list will not include the data that was sent to your server.
Request from Cognassist
Cognassist will send an HTTP POST request to the URL that was provided when a webhook was created or updated when any of the system events that the webhook is subscribed to occur.
The request from Cognassist will use the application/json
content type.
Request Payload
The payload of the HTTP POST request will have the following properties:
Data
is an object. Its properties will differ for each system event.SystemEvent
is an object that includes the id and name of the system event that triggered the HTTP POST request.DataId
is a globally unique identifier of the HTTP POST request. It can be used to avoid processing the same piece of data twice as we use retry strategies, which could cause the same piece of data to be sent more than once.
{
"Data": {
// The structure of this object will depend on the system event
},
"SystemEvent": {
"Id": "int",
"DisplayName": "string"
},
"DataId": "guid"
}
"Assessment Completed" Payload
The payload for the "Assessment Completed" system event has the following structure:
LearnerId
is the globally unique identifier of the learner who has just completed their assessment.
{
"Data": {
"LearnerId": "00000000-0000-0000-0000-000000000000"
},
"SystemEvent": {
"Id": 100,
"DisplayName": "Assessment Completed"
},
"DataId": "00000000-0000-0000-0000-000000000000"
}
"Assessment Report Created" Payload
The payload for the "Assessment Report Created" system event is multipart/form-data
request.
It has two named parts to it:
- "data", which contains information about the "Assessment Report Created" system event, and is described below.
- "file", which is the binary content that forms the PDF assessment report.
The "Assessment Report Created" system event data has the following structure:
LearnerId
is the globally unique identifier of the learner for whom the assessment report has been created.RequestId
is the globally unique identifier provided when the request to generate the report was made via the Cognassist API.
{
"Data": {
"LearnerId": "00000000-0000-0000-0000-000000000000",
"RequestId": "00000000-0000-0000-0000-000000000000"
},
"SystemEvent": {
"Id": 200,
"DisplayName": "Assessment Report Created"
},
"DataId": "00000000-0000-0000-0000-000000000000"
}
"Assessment Report Request Failed" Payload
The payload for the "Assessment Report Request Failed" system event has the following structure:
LearnerId
is the globally unique identifier of the learner for whom the assessment report has been requested.RequestId
is the globally unique identifier provided when the request to generate the report was made via the Cognassist API.
{
"Data": {
"LearnerId": "00000000-0000-0000-0000-000000000000",
"RequestId": "00000000-0000-0000-0000-000000000000"
},
"SystemEvent": {
"Id": 300,
"DisplayName": "Assessment Report Request Failed"
},
"DataId": "00000000-0000-0000-0000-000000000000"
}
Request Signature
We use the Secret
property of your webhook to create a hash signature with each payload. This hash signature is included with the headers of each request as x-cognassist-sha256
.
You should calculate a hash signature using the same Secret
and ensure that the result matches the hash from Cognassist. If the two signatures do not match, please do not process the request.
Verifying the Request Signature
Here is an example of how the signature can be verified:
[HttpPost]
public async Task<IActionResult> Index()
{
// First, check if the signature is included in the request headers.
var headerExists = Request.Headers.TryGetValue("x-cognassist-sha256", out var signatureHeader);
if (!headerExists)
{
// Do not process the request if there is no signature.
throw new Exception("Header 'x-cognassist-sha256' was not provided in the request.");
}
var requestBody = await Request.GetRawBodyAsync();
VerifySignature(requestBody, signatureHeader.ToString(), Environment.GetEnvironmentVariable("WebhookSecret"));
var data = JsonSerializer.Deserialize<WebhookRequest>(requestBody);
// Do something with the data...
return Ok();
}
// Use the request body, the request signature and the webhook secret to verify the signature.
private static void VerifySignature(string requestBody, string requestSignature, string secret)
{
// Create a SHA256 hash signature using the webhook secret and the request body.
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var payloadBytes = Encoding.UTF8.GetBytes(requestBody);
var hashBytes = hmac.ComputeHash(payloadBytes);
var signature = Convert.ToBase64String(hashBytes);
if (!CompareSignatures(signature, requestSignature))
{
// Do not process the request if the signatures do not match.
throw new Exception("Signatures did not match.");
}
}
// Check if the signature from the request and the calculated signature are the same.
private static bool CompareSignatures(string signature1, string signature2)
{
var bytes1 = Encoding.UTF8.GetBytes(signature1);
var bytes2 = Encoding.UTF8.GetBytes(signature2);
if (bytes1.Length != bytes2.Length)
{
return false;
}
for (int i = 0; i < bytes1.Length; i++)
{
if (bytes1[i] != bytes2[i])
{
return false;
}
}
return true;
}
private sealed record WebhookRequest(object Data, SystemEvent SystemEvent, Guid DataId);
private sealed record SystemEvent(int? Id, string? DisplayName);
The signature needs to be verified differently for a multipart payload:
[HttpPost]
public async Task<IActionResult> Index()
{
// First, check if the signature is included in the request headers.
var headerExists = Request.Headers.TryGetValue("x-cognassist-sha256", out var signatureHeader);
if (!headerExists)
{
// Do not process the request if there is no signature.
throw new Exception("Header 'x-cognassist-sha256' was not provided in the request.");
}
if (Request.HasFormContentType)
{
// verify the signatures
Request.EnableBuffering();
using (var ms = new MemoryStream())
{
await Request.Body.CopyToAsync(ms);
var contentBytes = ms.ToArray();
VerifySignature(contentBytes, signatureHeader.ToString(), secret);
Request.Body.Position = 0;
}
var form = await Request.ReadFormAsync();
var formFile = form.Files["file"];
// Do something with the file...
var formData = form["data"];
var data = JsonSerializer.Deserialize<WebhookRequest>(formData!);
// Do something with the data...
}
return Ok();
}