Guide to Teachfloor Webhooks: Features and Best Practices
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
| Feature | Details |
|---|---|
| Signature | hex HMAC-SHA256 |
| Signature header | teachfloor-signature |
| Body | JSON |
| Event routing | Body field type |
From Teachfloor's delivery and retries page:
- Any
2xxwithin 10 seconds counts as success. Teachfloor recommends200. - A non-
2xxstatus, 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 envelopeidand stays the same across retries. - Delivery history for each endpoint is shown under Developers > Webhooks.
Events and payloads
Every delivery has the same envelope:
| Field | Meaning |
|---|---|
id | Delivery ID, stable across retries and mirrored in Teachfloor-Idempotency-Key |
type | Event type, for example course.join. Route on this |
created_at | ISO 8601 time the event was recorded |
installation_id | Only on endpoints provisioned by an extension app: the installation the event belongs to |
data | The 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:
type | Triggered When |
|---|---|
organization.join | A user becomes a member of the organisation (signup, invite, SSO or the Members API) |
member.created | A member joins the organisation, at the same moment as organization.join |
member.updated | A member's name, avatar, email or organisation-level custom fields change |
member.revoked | A member is removed from the organisation |
xapi.statement.created | An xAPI statement is recorded |
app.installed / app.uninstalled | An 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
- In Teachfloor, open Developers > Webhooks and click Add Endpoint.
- Enter your HTTPS URL, for example
https://example.com/webhooks/teachfloor. - Select the events to receive, then save.
- Click Reveal Signing Secret on the endpoint and store it as
TEACHFLOOR_. Use it as a plain string.SIGNING_ SECRET
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_attributesto 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.