Integration · Reference
Webhooks
Receive signed learner and notification events in your systems.
Learning webhook setup
- Open Workspace > Integrations > Webhooks and create a webhook.
- Enter a public endpoint URL.
- Select events, or leave the list empty to receive all events, including event types added later.
- Add any custom headers, enable the webhook, and save.
- Copy the signing secret into your secret manager.
Events are scoped to the organization where the action occurs. Learner event dates are ISO 8601 strings. For notification events, data.to contains recipient user IDs, not email addresses.
For polling and other integrations, see the REST API guide.
Use Kokobi Learning webhooks to receive events at an HTTPS endpoint you control.
Configure a subscription
Ask your administrator how to manage webhook subscriptions for this application.
- Create a public HTTPS endpoint that accepts the documented requests.
- Register its URL and select only the events you need.
- If a signing secret is provided, store it in a secrets manager, never in public code or a URL.
- Use a test event, if available, and verify receipt before processing live events.
Events and reference
The body contains event (the event name below) and data (the corresponding payload).
| Event | Description | Payload |
|---|---|---|
learner.completed | Learner completed | Payload 1 |
learner.started | Learner started | Payload 1 |
learner.updated | Learner progress updated | Payload 1 |
course.invite | Course Invitation | Payload 2 |
course.inactive | Course Reminder | Payload 2 |
course.completion | Course Completion | Payload 3 |
course.request.accepted | Course Request Accepted | Payload 3 |
course.request.rejected | Course Request Rejected | Payload 3 |
collection.invite | Collection Invitation | Payload 2 |
collection.completion | Collection Completion | Payload 3 |
collection.request.accepted | Collection Request Accepted | Payload 3 |
collection.request.rejected | Collection Request Rejected | Payload 3 |
course.request | Course Request | Payload 4 |
collection.request | Collection Request | Payload 4 |
Events with the same payload share one reference. Required applies within the containing object; nested fields apply only when their parent is present and non-null. Conditional fields belong to a particular variant. [] denotes an array item and * an arbitrary key.
Payload 1
learner.completed, learner.started, learner.updated
| Field | Type | Required | Description |
|---|---|---|---|
data.attempt | object | null | Yes | — |
data.attempt.id | string | Yes | — |
data.attempt.userId | string | Yes | — |
data.attempt.organizationId | string | Yes | — |
data.attempt.moduleId | string | Yes | — |
data.attempt.courseId | string | Yes | — |
data.attempt.completedAt | string | null | Yes | — |
data.attempt.data | Record<string, string> | Yes | — |
data.attempt.data.* | string | No | — |
data.attempt.createdAt | string | Yes | — |
data.attempt.updatedAt | string | Yes | — |
data.attempt.status | "completed" | "passed" | "failed" | "in-progress" | "not-started" | Yes | — |
data.attempt.score | object | null | No | — |
data.attempt.score.raw | number | null | No | — |
data.attempt.score.max | number | null | No | — |
data.attempt.score.min | number | null | No | — |
data.attempt.module | object | Yes | Module metadata, including its name, for a learner attempt. |
data.attempt.module.id | string | Yes | — |
data.attempt.module.name | string | null | Yes | — |
data.attempt.module.courseId | string | Yes | — |
data.attempt.module.locale | "en" | "fr" | Yes | — |
data.attempt.module.multilingual | boolean | Yes | — |
data.attempt.module.type | "1.2" | "2004" | Yes | — |
data.attempt.module.versionNumber | number | Yes | — |
data.attempt.module.sizeBytes | number | null | Yes | — |
data.attempt.module.deletedAt | string | null | Yes | — |
data.attempt.module.createdAt | string | Yes | — |
data.attempt.module.updatedAt | string | Yes | — |
data.connection | object | Yes | — |
data.connection.respondedAt | string | null | No | — |
data.connection.userId | string | Yes | — |
data.connection.organizationId | string | Yes | — |
data.connection.courseId | string | Conditional | — |
data.connection.externalId | string | null | Conditional | — |
data.connection.createdAt | string | Yes | — |
data.connection.updatedAt | string | Yes | — |
data.connection.connectType | "invite" | "request" | Yes | — |
data.connection.connectStatus | "pending" | "accepted" | "rejected" | "disconnected" | "removed" | Yes | — |
data.connection.collectionId | string | Conditional | — |
data.user | object | Yes | — |
data.user.id | string | Yes | — |
data.user.email | string | Yes | — |
data.user.name | string | Yes | — |
Payload 2
course.invite, course.inactive, collection.invite
| Field | Type | Required |
|---|---|---|
data.to | Array<string> | Yes |
data.data | object | Yes |
data.data.id | string | Yes |
data.data.reminderDays | unknown | No |
Payload 3
course.completion, course.request.accepted, course.request.rejected, collection.completion, collection.request.accepted, collection.request.rejected
| Field | Type | Required |
|---|---|---|
data.to | Array<string> | Yes |
data.data | object | Yes |
data.data.id | string | Yes |
Payload 4
course.request, collection.request
| Field | Type | Required |
|---|---|---|
data.to | Array<string> | Yes |
data.data | object | Yes |
data.data.id | string | Yes |
data.data.userId | string | Yes |
data.data.userEmail | string | Yes |
data.data.userName | unknown | No |
Do not assume every application uses the same event names, fields, versions, or delivery identifiers. Validate incoming data against the event schema before using it.
Verify requests
The KrakStack Webhooks sender includes Content-Type: application/json, webhook-id, webhook-timestamp (ISO 8601), and webhook-signature (hex-encoded HMAC-SHA256). These headers are reserved and cannot be overridden by custom headers.
Using the endpoint's signing secret, compute the HMAC-SHA256 digest of the exact UTF-8 bytes of:
webhook-id + "." + webhook-timestamp + "." + raw_request_bodyCompare it with the decoded signature using a constant-time comparison. Reject invalid or stale timestamps to limit replay attacks. The webhook-id is the delivery identifier and remains stable across retries; scope your idempotency key to the endpoint. Do not use the timestamp or signature as an idempotency key, since they change for each attempt.
The delivered JSON body is { "event": string, "data": object }. Ignore additional fields your handler does not use.
Verify authenticity before processing an event. Use the application's documented verification method; HTTPS alone does not prove the sender's identity.
For signatures computed over the raw body, preserve the received bytes before parsing JSON. Use the documented headers, algorithm, and secret, a constant-time comparison, and any protocol-defined timestamp validity checks. Reject missing, invalid, or expired signatures when signing is required. Do not invent header names or an algorithm.
Do not log secrets or sensitive payloads. Follow the application's secret rotation procedure.
Retries and duplicates
The KrakStack Webhooks sender treats a 2xx response as success. Each HTTP attempt has a 30-second timeout. Other responses and network errors are retried, up to eight total attempts, with increasing delays. Use webhook-id to deduplicate retries. Delivery order is not guaranteed. Check the delivery log for errors and pending retries.
Disabling an endpoint prevents delivery; pending deliveries processed while it is disabled are marked failed. Completed delivery records become eligible for cleanup after seven days.
Design processing to tolerate duplicates. Use the documented event or delivery identifier as an idempotency key, scoped to the relevant subscription or organization. Persist processing state durably and prevent concurrent handlers from repeating side effects.
Where the protocol allows, validate the request and persist or enqueue it durably before returning the expected success status. Run long tasks in the background. Do not acknowledge receipt before you can resume processing after an interruption.
Success statuses, timeouts, retry schedules, retention periods, and redelivery options depend on the application. Do not assume exactly-once delivery or guaranteed ordering unless the reference explicitly promises them.
Troubleshooting
- No events received: check the subscription, selected events, and endpoint's public HTTPS accessibility.
- Invalid signature: check the active secret, documented headers, raw body, and your server clock if the protocol uses a timestamp.
- Failed delivery: inspect available delivery logs and check your server's HTTP status and response time. Follow the application's redelivery procedure, if available.
- Repeated effects: check your idempotency key's persistence and scope, including concurrent processing.