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 TypeDescriptionPayload
learner.updatedTriggered when a learner's information is updatedLearner object
learner.startedTriggered when a learner begins a new activityLearner object
learner.completedTriggered when a learner completes an activityLearner 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 - ISO timestamp when the webhook was sent
  • webhook-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

  1. Always verify signatures - Never process webhooks without signature verification
  2. Check timestamps - Reject webhooks with timestamps too far in the past/future
  3. Return quickly - Respond with a 2xx status code within 10 seconds
  4. Handle idempotency - The same event might be delivered multiple times

Retry Policy

Our webhook system implements an exponential backoff retry strategy:

AttemptDelay
1stImmediate
2nd5 seconds
3rd1 minute
4th5 minutes
5th30 minutes
6th2 hours
7th5 hours
8th10 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:

  1. Accept POST requests with Content-Type: application/json
  2. Respond with 2xx status codes (200, 201, 204) for successful processing
  3. Respond within 10 seconds to avoid timeouts
  4. Use HTTPS for production environments
  5. 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:

  1. Your webhook endpoint URL
  2. The specific event types you're trying to receive
  3. Any error messages or logs from your endpoint
  4. 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.