Skip to content

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 specified Url are coming from Cognassist. Each request will include a x-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();
}