Webhooks¶
Webhooks are how Cognassist tells your system that something happened, the moment it happens, so you never have to poll for it. When a learner finishes their assessment, a report is ready, or an evidence export completes, Cognassist sends an HTTP POST to a URL you own. You verify the signature, treat the payload as a signal, and re-fetch the current record from the API.
Webhooks are the primary, event-driven way to stay in step with Cognassist. This page is their canonical home: the delivery model, the eight events, and how to verify a request. For where webhooks sit against polling and the one-way SFTP file, and when each applies, see Keep in sync. For the exact request and response schemas, use the API reference; this page adds only the delivery model and the code tables the reference does not carry.
All webhook paths are version 1 (POST /v1/webhooks, GET /v1/systemevents, and so on), relative to the base URL https://api.uk.cognassist.com, and require a bearer token (see Getting started).
How delivery works¶
You register a URL once, tell Cognassist which events you care about, and from then on Cognassist POSTs to that URL whenever one of those events fires.
sequenceDiagram
participant C as Cognassist
participant Y as Your endpoint
C->>Y: POST event payload with x-cognassist-sha256 header
Y->>Y: Verify the signature over the raw body
Y->>Y: Skip if DataId already seen (dedupe)
Y-->>C: 200 OK
Y->>C: Re-fetch the full record via the API
Two properties shape everything you build on top:
- A payload is a signal, not the whole record. Most events carry just a learner id or a request id. Receive the event, then re-fetch the full, current record from the API (for example, retrieve the assessment result). This also keeps you correct when events arrive out of order.
- The same event can arrive more than once. Cognassist retries deliveries, so treat the
DataIdGUID on every request as an idempotency key: record the ones you have processed and skip any you have already seen.
The eight events¶
You choose which of these to subscribe to when you create or update a webhook. Subscribe only to what you will act on.
| Id | Event | Fires when |
|---|---|---|
| 100 | Assessment Completed | A learner completes their assessment. |
| 200 | Assessment Report Created | A requested assessment report has been generated (delivered as multipart/form-data). |
| 300 | Assessment Report Request Failed | A requested assessment report could not be generated. |
| 400 | Modules Assigned | A learner is assigned new modules. |
| 500 | Module First Accessed | A learner opens a module for the first time. |
| 600 | Module First Completed | A learner completes a module for the first time. |
| 700 | Learner Export Completed | A requested evidence export has been generated. |
| 800 | Learner Export Failed | A requested evidence export could not be generated. |
GET /v1/systemevents returns the live list. Each event's payload shape, and the code tables used inside the export payload, are in the appendix at the foot of this page.
Register a webhook¶
Create a webhook with POST /v1/webhooks, giving the systemEventIds you want, your public HTTPS url, a secret you choose (Cognassist signs every request with it so you can prove the request is genuine), and a description to recognise it later. See the API reference for the full request and response schemas.
curl -X POST https://api.uk.cognassist.com/v1/webhooks \
-H "Authorization: Bearer eyJhbGciOiJI..." \
-H "Content-Type: application/json" \
-d '{
"systemEventIds": [100, 700],
"url": "https://your-endpoint.example.com/cognassist/webhook",
"secret": "a-long-random-string-you-keep-secret",
"description": "Assessment completions and evidence exports"
}'
PUT /v1/webhooks/{webhookId} updates a webhook with the same body. GET /v1/webhooks lists all webhooks for your organisation, GET /v1/webhooks/{webhookId} returns one, and DELETE /v1/webhooks/{webhookId} deletes one (its invocation logs go with it).
To rotate the signing secret, PUT /v1/webhooks/{webhookId} with a new secret. Confirm the cutover behaviour with your Cognassist contact at onboarding, since deliveries in flight may briefly still be signed with the old secret.
Can't host an inbound endpoint?
Not being able to expose an inbound HTTPS endpoint is common on-premises, but it does not push you off webhooks. A low-code receiver terminates the webhook for you: Power Automate's When a HTTP request is received trigger, or an Azure Logic App, gives you a hosted HTTPS URL you register in the url field above and verifies the same signature. The tool is the receiver, not a different integration method. For the build, see Automate with low-code tools and the worked example in Power Automate.
Verify the signature¶
Every request from Cognassist carries an x-cognassist-sha256 header. This is your proof the request is genuine, so verify it before you act on any payload. There is no subscription-time challenge or handshake: a webhook is active on creation, and this signature plus DataId idempotency is your whole security model.
The recipe, in any language:
- Read the raw request body exactly as received, before any parsing or re-serialisation. Re-encoding the body changes the hash and the check fails.
- Compute HMAC-SHA256 over that raw body, keyed with your webhook
secret. - Base64-encode the result.
- Compare it to the
x-cognassist-sha256header using a constant-time comparison. If they do not match, reject the request and do not process it.
The report event is multipart, so verify over the whole body first
Assessment Report Created (200) arrives as multipart/form-data (a data part plus a file part), and the signature is computed over the whole raw body. Buffer the entire request and verify the signature before you read the form parts. Verifying against a re-serialised or partially read body is the most common cause of a signature that will not match.
Verify a JSON payload in C# (ASP.NET Core)
For teams hosting their own endpoint. Read the raw body once, HMAC-SHA256 it with the secret, and compare in constant time.
[HttpPost]
public async Task<IActionResult> Index()
{
// First, check the signature header is present.
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 raw request body, the request signature and the webhook secret to verify.
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.");
}
}
// Constant-time comparison so the check does not leak timing information.
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;
}
var result = 0;
for (int i = 0; i < bytes1.Length; i++)
{
result |= bytes1[i] ^ bytes2[i];
}
return result == 0;
}
private sealed record WebhookRequest(object Data, SystemEvent SystemEvent, Guid DataId);
private sealed record SystemEvent(int? Id, string? DisplayName);
The multipart delta (the report event)
Assessment Report Created (200) is multipart/form-data, so the only change from the JSON example is buffering the whole raw body and hashing the bytes before ReadFormAsync. The header check, CompareSignatures, and throw-on-mismatch are identical. Replace the body-read and hash step with:
// Buffer and verify over the whole raw body BEFORE reading the form parts.
Request.EnableBuffering();
using (var ms = new MemoryStream())
{
await Request.Body.CopyToAsync(ms);
VerifySignature(ms.ToArray(), signatureHeader.ToString(), secret);
Request.Body.Position = 0;
}
var form = await Request.ReadFormAsync();
var file = form.Files["file"]; // the PDF report
var data = JsonSerializer.Deserialize<WebhookRequest>(form["data"]!);
// This overload hashes the raw bytes directly (the JSON one takes a string);
// both key HMAC-SHA256 with the same secret over the same raw body.
private static void VerifySignature(byte[] requestBody, string requestSignature, string secret)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var signature = Convert.ToBase64String(hmac.ComputeHash(requestBody));
if (!CompareSignatures(signature, requestSignature))
{
throw new Exception("Signatures did not match.");
}
}
Delivery, retries and security¶
The three properties above (verify, dedupe, re-fetch) are the whole contract you build on. The operational detail behind them, and the specifics you confirm with us at onboarding, are below.
Delivery, retries and security in detail
What the API contract tells you today:
- Retries happen, so deliveries can repeat. Dedupe on
DataId. - Order is not guaranteed. Do not reconstruct state from the event sequence; re-fetch the current record from the API when you handle an event.
- Verify every request. Reject any whose
x-cognassist-sha256header does not match (see Verify the signature). - You can audit deliveries.
GET /v1/webhooks/{webhookId}/invocationlogsreturns the delivery history, andGET /v1/webhooks/{webhookId}/invocationlogs/{invocationLogId}returns a single log including the data that was sent. Reconcile against the API for anything that failed.
Confirm these delivery specifics with Cognassist before you go live. They are not published in the API contract, so do not hard-code an assumption about any of them; confirm them with your Cognassist contact as part of onboarding:
- the number of retry attempts and the backoff window between them
- the timeout Cognassist applies to your endpoint, and the exact success status code it expects
- whether a source IP allow-list is available for your firewall
If a delivery is missing or you suspect a gap, do not rely on redelivery alone: check the invocation logs and run a scheduled reconcile against the API, as described in Keep in sync. If a subscription is not firing at all, see Troubleshooting.
Next steps¶
- Keep in sync places webhooks against polling and the one-way SFTP file, and shows the reconcile that backs them up.
- Retrieve and render results is what you call after an Assessment Completed event.
- Export evidence and data is the end-to-end flow behind the Learner Export Completed event.
- Automate with low-code tools and the worked example in Power Automate build a receiver without hosting a service.
Appendix: payload code tables¶
The API reference carries the full schema for every event payload. Kept here are only the pieces it does not: a minimal illustration of the shared envelope, and the export-only code lookups used inside the Learner Export Completed (700) payload. Fields on the export records (ClientReference, LearnerUniqueReference, and so on) and their casing are described in Core concepts; the end-to-end export flow is on Export evidence and data.
The shared envelope
Every JSON event shares the same shape. The Data object differs per event and its full schema is documented per event in the API reference; SystemEvent names the event; DataId is the idempotency key. Assessment Report Created (200) is the one exception: it is multipart/form-data, with the envelope in a data part and the PDF report in a file part.
Webhook payloads are PascalCase (Data, SystemEvent, DataId, LearnerId), unlike the camelCase REST bodies, so map on that boundary or your properties will bind to nulls. See field casing in Core concepts.
For Assessment Completed (100), the most common event to build against, Data carries the learner id you re-fetch on:
{
"Data": {
"LearnerId": "00000000-0000-0000-0000-000000000000"
},
"SystemEvent": {
"Id": 100,
"DisplayName": "Assessment Completed"
},
"DataId": "00000000-0000-0000-0000-000000000000"
}
The async report and export events carry the RequestId you match against the requestId returned by your original POST (see Retrieve and render results and Export evidence and data). For Learner Export Completed (700) it sits inside Data, alongside the exported evidence records:
{
"Data": {
"RequestId": "00000000-0000-0000-0000-000000000000"
},
"SystemEvent": { "Id": 700, "DisplayName": "Learner Export Completed" },
"DataId": "00000000-0000-0000-0000-000000000000"
}
For Assessment Report Created (200), this same envelope is the data part of the multipart body, alongside the file part carrying the PDF:
{
"Data": {
"RequestId": "00000000-0000-0000-0000-000000000000"
},
"SystemEvent": { "Id": 200, "DisplayName": "Assessment Report Created" },
"DataId": "00000000-0000-0000-0000-000000000000"
}
These snippets show only the field each receiver keys on. For the complete per-event Data schema, see the API reference.
Learner Export Completed code tables¶
The 700 Learner Export Completed payload is delivered to your webhook endpoint after you call POST /v1/learnerexports. Its records carry several numeric codes, sent as strings. The API reference holds the field list and schema; the code lookups the reference does not spell out are collected below.
Several of these codes are decoded once elsewhere, so read them at their home rather than here: the cognitive domain codes (1 to 9) and index codes (1 to 6) are the same nine domains and six indexes in Core concepts, and the supportType (10 Regular, 20 Above and beyond) and coverageStatus (blank, needs improvement, meets standard) codes are defined in Core concepts: evidence and support records. What remains below is export-only.
Export payload code lookups
Support session type codes
| Code | Session type |
|---|---|
| 10 | Progress review 1:1 |
| 20 | Pastoral support 1:1 |
| 30 | Learning support 1:1 |
| 40 | Careers guidance 1:1 |
| 50 | Functional skills coaching |
| 60 | Reasonable adjustment review |
| 70 | Return-to-learning meeting |
| 80 | Off-the-job training review |
| 90 | Workplace support visit |
| 100 | Skills gap coaching |
Duration of support codes
Codes run in half-hour steps: code 10 is 0.5 hours and each further step adds 0.5 hours, up to code 160 at 8 hours.
| Code | Duration |
|---|---|
| 10 | 0.5 hours |
| 20 | 1 hour |
| 30 | 1.5 hours |
| 40 | 2 hours |
| ... | ... in 0.5-hour steps ... |
| 150 | 7.5 hours |
| 160 | 8 hours |