Search

Search for an item to open.

Webhooks

Kokobi helps you deliver training with courses, collections, SCORM 1.2 and 2004 support, learner management, custom domains, white labeling, and simple course sharing.

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
  • 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

  1. Always verify signatures.
  2. Check timestamps.
  3. Return quickly with a 2xx status code.
  4. Handle idempotency.

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 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:

  1. Accept POST requests with Content-Type: application/json.
  2. Respond with 2xx status codes 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 {
    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:

  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.
  • 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.