Gareth Wilson Gareth Wilson

Guide to Teachfloor Webhooks: Features and Best Practices

Published


Teachfloor is a cohort-based learning platform for running courses, programmes and academies. Its webhooks tell your systems when people join the organisation or a course, finish an element or a whole course, log in, or when course content changes, so you can provision access, issue certificates or keep a CRM in step with learning activity.

This guide covers Teachfloor's endpoint setup, the event envelope and update diffs, verifying Teachfloor-Signature, and planning around a retry window of under two minutes.

What are Teachfloor webhooks?

A Teachfloor webhook endpoint is an HTTPS URL added under Developers > Webhooks, with a list of events it should receive. Each endpoint has its own signing secret. When a selected event happens, Teachfloor sends a POST with a JSON body whose data block is the object involved, in the same shape the Public API returns.

Events fire for changes from any source: the dashboard, the Public API, automations and the platform itself. That includes writes your own integration makes through the API.

Teachfloor webhook features

FeatureDetails
Signaturehex HMAC-SHA256
Signature headerteachfloor-signature
BodyJSON
Event routingBody field type

From Teachfloor's delivery and retries page:

  • Any 2xx within 10 seconds counts as success. Teachfloor recommends 200.
  • A non-2xx status, a timeout or a connection failure triggers a retry. There are 3 attempts in total: the first, then retries after 10 seconds and 100 seconds.
  • Every delivery carries Teachfloor-Idempotency-Key, which equals the envelope id and stays the same across retries.
  • Delivery history for each endpoint is shown under Developers > Webhooks.

Events and payloads

Every delivery has the same envelope:

FieldMeaning
idDelivery ID, stable across retries and mirrored in Teachfloor-Idempotency-Key
typeEvent type, for example course.join. Route on this
created_atISO 8601 time the event was recorded
installation_idOnly on endpoints provisioned by an extension app: the installation the event belongs to
dataThe object the event is about. Its object field names the shape

data.object is one of course, module, element, member, course_member, organization_member or activity. Enumerated values such as status, availability, visibility and element type are upper-case, as in the Public API.

Event Types

| type | Event type, for example course.join. Dispatch on this | | created_at | ISO 8601 timestamp | | data | The object the event is about; its shape depends on type |

Teachfloor's event reference lists more events than the table above:

typeTriggered When
organization.joinA user becomes a member of the organisation (signup, invite, SSO or the Members API)
member.createdA member joins the organisation, at the same moment as organization.join
member.updatedA member's name, avatar, email or organisation-level custom fields change
member.revokedA member is removed from the organisation
xapi.statement.createdAn xAPI statement is recorded
app.installed / app.uninstalledAn organisation installs or uninstalls your extension app

member.login, course.completed and element.completed carry an activity object rather than the member or course itself, with the course or element in its context.

Update events carry a diff

Every *.updated event (course.updated, module.updated, element.updated, member.updated) sends the full current object plus data.previous_attributes, holding only the keys that changed and their old values. For custom_fields, the old value is the whole previous map.

Example payload

A course.join delivery, from the webhook samples library. Teachfloor's own reference shows id as a random string such as WqX3nKp7Lm2vRt9Z and upper-case enumerated values, so treat this sample's ID prefix and lower-case values as illustrative:

{
  "id": "evt_abc123",
  "type": "course.join",
  "created_at": "2025-10-07T15:30:00.000000Z",
  "data": {
    "id": "cm_xyz789",
    "object": "course_member",
    "joined_at": "2025-10-07T15:30:00.000000Z",
    "member": {
      "id": "usr_123",
      "object": "member",
      "first_name": "John",
      "last_name": "Doe",
      "full_name": "John Doe",
      "avatar": "https://example.com/avatar.jpg",
      "email": "john@example.com",
      "is_email_verified": true,
      "last_seen": "2025-10-07T14:00:00.000000Z"
    },
    "course": {
      "id": "crs_456",
      "object": "course",
      "created_at": "2025-01-01T00:00:00.000000Z",
      "name": "Introduction to Development",
      "status": "published",
      "cover": "https://example.com/cover.jpg",
      "availability": "public",
      "visibility": "listed",
      "start_date": null,
      "end_date": null,
      "currency": "USD",
      "price": 99.0,
      "free_label": null,
      "url": "https://app.teachfloor.com/org/intro-dev",
      "public_url": "https://app.teachfloor.com/org/intro-dev/join",
      "join_url": "https://app.teachfloor.com/org/intro-dev/join",
      "metadata": {},
      "custom_fields": {}
    },
    "custom_fields": {}
  }
}

The *.created events point to the object definitions in the reference rather than full examples, so check a real delivery before relying on optional fields.

Setting up Teachfloor webhooks

  1. In Teachfloor, open Developers > Webhooks and click Add Endpoint.
  2. Enter your HTTPS URL, for example https://example.com/webhooks/teachfloor.
  3. Select the events to receive, then save.
  4. Click Reveal Signing Secret on the endpoint and store it as TEACHFLOOR_SIGNING_SECRET. Use it as a plain string.

To route through Hookdeck, create a Teachfloor source with the endpoint's signing secret and register the source URL as the endpoint. For local development:

npx hookdeck-cli listen 3000 teachfloor --path /webhooks/teachfloor

Per endpoint: Developers > Webhooks > endpoint > Reveal Signing Secret.

Securing Teachfloor webhooks

Teachfloor-Signature is the hex HMAC-SHA256 of the raw request body, keyed with the endpoint's signing secret. The whole header value is the digest, with no prefix or version. Teachfloor's security page shows PHP and Node examples that compare it directly.

Teachfloor's getting-started sample parses the body with express.json(). If you verify, read the raw bytes with express.raw() instead, because re-serialised JSON won't match the signature.

Node.js verification

const crypto = require('node:crypto');

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['teachfloor-signature'];
  if (typeof signature !== 'string') return false;
  const key = Buffer.from(secret, 'utf8');
  const message = rawBody;
  const candidates = [signature];
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
    const received = Buffer.from(candidate, 'hex');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

The handler deduplicates on id, then queues the event for a worker. Invalid JSON or a missing id or type gets a 400, and a queue failure a 503 so Teachfloor retries.

const express = require('express');

const app = express();

app.post('/webhooks/teachfloor', express.raw({ type: '*/*' }), async (req, res) => {
  const secret = process.env.TEACHFLOOR_SIGNING_SECRET;
  if (!secret) return res.sendStatus(500);
  if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);

  let event;
  try {
    event = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  if (!event || typeof event !== 'object' || Array.isArray(event)) return res.sendStatus(400);
  if (typeof event.id !== 'string' || typeof event.type !== 'string') return res.sendStatus(400);

  const data = event.data && typeof event.data === 'object' ? event.data : {};

  try {
    if (await alreadyHandled(event.id)) return res.sendStatus(200);
    await queue.add('teachfloor-event', {
      id: event.id,
      type: event.type,
      object: data.object,
      data,
    });
    await markHandled(event.id);
  } catch {
    return res.sendStatus(503);
  }
  res.sendStatus(200);
});

queue, alreadyHandled and markHandled stand for your own job queue and deduplication store.

Hookdeck performs the same check when you use its Teachfloor source type. It returns 200 to Teachfloor and records any mismatch as failed verification. No timestamp is signed, and Hookdeck imposes no time window of its own.

Make Teachfloor webhooks production-ready. Hookdeck Event Gateway accepts each Teachfloor delivery and keeps retrying your service on its own schedule, so an outage longer than two minutes doesn't cost you enrolments or completions.

Teachfloor webhook limitations and pain points

Retries end after about two minutes

The Problem: If your endpoint is down for more than a couple of minutes, events such as course.completed are never delivered.

Why It Happens: Teachfloor makes 3 attempts in total, 10 seconds and then 100 seconds apart. Whether a failed delivery can be resent from the dashboard isn't documented.

Workarounds:

  • Check the endpoint's delivery history under Developers > Webhooks after any outage.
  • Reconcile enrolments and completions through the Public API.

How Hookdeck Can Help: Hookdeck accepts the first attempt, stores it, and retries your service for as long as your policy allows, with manual replay for anything left over.

Your own API writes come back as events

The Problem: A sync that writes to Teachfloor through the Public API receives webhooks for its own changes, which can loop.

Why It Happens: Teachfloor reports changes from every source, including the API.

Workarounds:

  • Compare the incoming object with the value you just wrote and skip no-op updates.
  • Use previous_attributes to act only on fields that matter to you.

How Hookdeck Can Help: Hookdeck filters can drop *.updated events whose previous_attributes hold only fields your service doesn't track.

A 10-second timeout

The Problem: Handlers that call a CRM or certificate service before responding get retried.

Why It Happens: Teachfloor treats anything slower than 10 seconds as a failure.

Workarounds:

  • Queue the event and respond straight away.
  • Deduplicate on Teachfloor-Idempotency-Key, because a slow success still triggers a retry.

How Hookdeck Can Help: Hookdeck responds to Teachfloor at once and lets you set a longer timeout for your own service.

Some events lack full examples

The Problem: Fields for events such as course.created or element.created have to be read from object definitions.

Why It Happens: The event reference gives complete examples for some events and points to the Object Types section for the rest.

Workarounds:

  • Capture one real delivery per event type before writing parsers.
  • Validate optional fields instead of assuming they're present.

How Hookdeck Can Help: Hookdeck keeps every delivered body, giving you real payloads to test against.

Best practices

Deduplicate before parsing

Teachfloor-Idempotency-Key matches id, so you can check it from the header before reading the body. The idempotency guide covers storage options.

Route by data.object as well as type

Completion events carry an activity, not the course. Read the course or element from the activity context and fetch more through the Public API when needed.

Handle upper-case enums

Compare status, availability and visibility as the upper-case values the Public API returns.

Keep the handler short

Verify, queue and respond, then do the slow work in a worker. See processing webhooks asynchronously.

Conclusion

Teachfloor sends signed JSON events for course, module, element and member changes, with update diffs in previous_attributes and a stable idempotency key. Verify the hex HMAC in Teachfloor-Signature against the raw body, respond within 10 seconds, and plan for retries that stop after about two minutes.

Hookdeck Event Gateway verifies Teachfloor requests and holds them until your service has processed them.

Get started with Hookdeck for free and handle Teachfloor webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.