Webhooks
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/jsonwebhook-timestamp: 2024-01-15T10:30:00.000Z- ISO timestamp when the webhook was sentwebhook-signature: <signature>- HMAC-SHA256 signature for verification (see Security section)- Any custom headers you've configured for your webhook endpoint
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));
}
// Usage in your webhook handler
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");
}
// Process the webhook
console.log("Received event:", req.body.event);
res.status(200).send("OK");
});
Best Practices
- Always verify signatures - Never process webhooks without signature verification
- Check timestamps - Reject webhooks with timestamps too far in the past/future
- Return quickly - Respond with a 2xx status code within 10 seconds
- Handle idempotency - The same event might be delivered multiple times
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 considered successful when your endpoint returns a 2xx HTTP status code
- After the final retry attempt, the webhook delivery is marked as failed and will not be retried
- Failed deliveries are automatically cleaned up after the expiry period
Endpoint Requirements
Your webhook endpoint must:
- Accept POST requests with
Content-Type: application/json - Respond with 2xx status codes (200, 201, 204) 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 {
// 1. Verify the signature
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");
}
// 2. Process the event
const { event, data } = req.body;
switch (event) {
case "learner.updated":
console.log(`Learner ${data.id} was updated`);
// Your business logic here
break;
case "learner.started":
console.log(`Learner ${data.id} started an activity`);
// Your business logic here
break;
case "learner.completed":
console.log(`Learner ${data.id} completed an activity`);
// Your business logic here
break;
default:
console.log(`Unknown event type: ${event}`);
}
// 3. Respond quickly
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
- However, 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.