This documentation explains how to integrate with our webhook system to receive real-time notifications about learner events in your application.
Overview
Webhooks allow your application to receive HTTP POST requests whenever specific events occur in our system. Instead of polling our API for changes, webhooks provide a reliable way to get notified immediately when events happen.
Event Types
We support the following webhook event types:
| Event Type | Description | Payload |
|---|---|---|
| learner.updated | Triggered when a learner's information is updated | Learner object |
| learner.started | Triggered when a learner begins a new activity | Learner object |
| learner.completed | Triggered when a learner completes an activity | Learner object |
Webhook Payload Structure
All webhook requests will be sent as HTTP POST requests with the following JSON structure:
Body
More detailed types follow
interface FullLearner {
attempt: {
id: string;
userId: string;
organizationId: string;
moduleId: string;
courseId: string;
completedAt: Date | null;
data: Record<string, string>;
createdAt: Date;
updatedAt: Date;
status: "completed" | "passed" | "failed" | "in-progress" | "not-started";
score?: { raw?: number; max?: number; min?: number };
module: Module;
} | null;
user: {
id: string;
email: string;
name: string;
};
connection: UserToCourseType | UserToCollectionType;
}Attempt
interface FullLearnerAttempt {
id: string;
userId: string;
organizationId: string;
moduleId: string;
courseId: string;
completedAt: Date | null;
data: Record<string, string>;
createdAt: Date;
updatedAt: Date;
status: "completed" | "passed" | "failed" | "in-progress" | "not-started";
score?: { raw?: number; max?: number; min?: number };
module: Module;
}User
interface FullLearnerUser {
id: string;
email: string;
name: string;
}Connection
Either a course or a collection connection:
Course Connection
interface UserToCourseType {
userId: string;
organizationId: string;
courseId: string;
externalId: string | null;
connectType: "invite" | "request";
connectStatus: "pending" | "accepted" | "rejected";
createdAt: Date;
updatedAt: Date;
}Collection Connection
interface UserToCollectionType {
userId: string;
organizationId: string;
collectionId: string;
connectType: "invite" | "request";
connectStatus: "pending" | "accepted" | "rejected";
createdAt: Date;
updatedAt: Date;
}Request Headers
Each webhook request includes the following headers:
- Content-Type: application/json
- webhook-timestamp: 2024-01-15T10:30:00.000Z
- webhook-signature: <signature>
Security & Verification
Signature Verification
Every webhook request includes a signature in the webhook-signature header. This signature is generated using HMAC-SHA256 with your webhook secret.
Signature Generation:
HMAC-SHA256(secret, timestamp + '.' + request_body)Example verification (Node.js):
const crypto = require("crypto");
function verifyWebhookSignature(secret, timestamp, body, signature) {
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(timestamp + "." + body)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature),
);
}
app.post("/webhook", (req, res) => {
const timestamp = req.headers["webhook-timestamp"];
const signature = req.headers["webhook-signature"];
const body = JSON.stringify(req.body);
if (!verifyWebhookSignature(webhookSecret, timestamp, body, signature)) {
return res.status(401).send("Unauthorized");
}
console.log("Received event:", req.body.event);
res.status(200).send("OK");
});Best Practices
- Always verify signatures.
- Check timestamps.
- Return quickly with a 2xx status code.
- Handle idempotency.
Retry Policy
Our webhook system implements an exponential backoff retry strategy:
| Attempt | Delay |
|---|---|
| 1st | Immediate |
| 2nd | 5 seconds |
| 3rd | 1 minute |
| 4th | 5 minutes |
| 5th | 30 minutes |
| 6th | 2 hours |
| 7th | 5 hours |
| 8th | 10 hours |
Important Notes:
- Webhooks are retried for up to 7 days.
- A delivery is successful when your endpoint returns a 2xx HTTP status code.
- After the final retry attempt, the delivery is marked as failed.
Endpoint Requirements
Your webhook endpoint must:
- Accept POST requests with Content-Type: application/json.
- Respond with 2xx status codes for successful processing.
- Respond within 10 seconds to avoid timeouts.
- Use HTTPS for production environments.
- Be publicly accessible from our servers.
Testing Your Integration
Example Webhook Handler
Here's a basic webhook handler example:
const express = require("express");
const crypto = require("crypto");
const app = express();
app.use(express.json());
app.post("/webhook", (req, res) => {
try {
const timestamp = req.headers["webhook-timestamp"];
const signature = req.headers["webhook-signature"];
const body = JSON.stringify(req.body);
if (!verifySignature(timestamp, body, signature)) {
return res.status(401).send("Unauthorized");
}
const { event, data } = req.body;
switch (event) {
case "learner.updated":
console.log("Learner " + data.id + " was updated");
break;
case "learner.started":
console.log("Learner " + data.id + " started an activity");
break;
case "learner.completed":
console.log("Learner " + data.id + " completed an activity");
break;
default:
console.log("Unknown event type: " + event);
}
res.status(200).send("OK");
} catch (error) {
console.error("Webhook processing error:", error);
res.status(500).send("Internal Server Error");
}
});
function verifySignature(timestamp, body, signature) {
const expectedSignature = crypto
.createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(timestamp + "." + body)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature),
);
}
app.listen(3000, () => {
console.log("Webhook server running on port 3000");
});Troubleshooting
Common Issues
Webhook deliveries failing:
- Verify your endpoint returns 2xx status codes.
- Check that your endpoint is publicly accessible.
- Ensure your server responds within 10 seconds.
- Verify signature verification is working correctly.
Not receiving webhooks:
- Confirm your webhook is enabled in the dashboard.
- Check that the correct events are selected.
- Verify the webhook URL is correct and accessible.
Duplicate events:
- Implement idempotency in your webhook handler.
- Use the event timestamp or a unique identifier to detect duplicates.
Support
If you encounter issues with webhook delivery or need assistance with implementation, please contact our support team with:
- Your webhook endpoint URL.
- The specific event types you're trying to receive.
- Any error messages or logs from your endpoint.
- The approximate time when the issue occurred.
Rate Limits
- Webhook deliveries are processed every 5 seconds.
- No specific rate limits are imposed on individual endpoints.
- Consistently slow or failing endpoints may be temporarily disabled.
Remember to implement proper error handling and logging in your webhook handlers to ensure reliable event processing.