Webhooks
Webhooks deliver platform events to your app's own backend — HTTP POSTs signed with your app's secret, fired whenever something the app cares about happens on the platform. Complements Realtime Channels: realtime pushes events into your app's SDK view while a learner is active; webhooks reach your server even when no learner is online.
What you can build
- Data sync — mirror course completions, quiz submissions, or member joins into an HR system, LMS, analytics warehouse, or CRM.
- Backend integrations — post activity notifications to Slack, Discord, Microsoft Teams, or a shared inbox.
- Provisioning + teardown — bootstrap per-tenant state when your app is installed on a new organization; clean up when it's uninstalled.
- Third-party mirroring — keep a system-of-record (e.g. a marketing tool or student-info system) in step with events happening on the platform.
How it works
Every extension app can optionally declare a single webhook endpoint URL in its manifest. When an organization installs the app, Teachfloor auto-provisions the delivery pipeline against that URL — no per-tenant configuration required. The app's backend receives POSTs for every event it subscribed to, plus two lifecycle events (app.installed, app.uninstalled) that fire regardless of subscription.
One URL per app is delivered to for every installing organization. The payload carries the organization identifier so your backend can attribute the event to the right tenant.
Manifest schema
Declare the block at the top level of your manifest alongside views and permissions:
Code
Fields:
| Field | Required | Rules |
|---|---|---|
webhook.url | yes (when block present) | https:// only. HTTP is rejected — signed payloads over plaintext give no meaningful integrity guarantee. Max 2048 chars. |
webhook.events | no | Array of event names drawn from the event catalog. Empty array is valid — you still receive the lifecycle events. Unknown event names are rejected at manifest push time. |
Omit the block entirely if your app doesn't need backend deliveries.
The signing secret
Every app has a single master signing secret used to sign every webhook delivery to your endpoint, across every installing organization. You manage one secret per app, not one per install.
Where to find it: Developers → Apps → your app → the header metadata strip includes a "Signing Secret" cell with a reveal button. Click to show the raw value.
The secret is generated automatically when your app is created.
Signature verification
Every delivery includes a Teachfloor-Signature header. The signature is HMAC-SHA256(secret, request_body). Verify it before trusting the payload.
Node.js / Express:
Code
Python / Flask:
Code
PHP:
Code
Always use a constant-time comparison (timingSafeEqual / hmac.compare_digest / hash_equals). A plain == opens a timing side-channel on the signature check.
Payload shape
Every delivery has the same envelope, with an event-specific data block inside:
Code
id is stable and unique — safe to use as an idempotency key (and mirrored in the Teachfloor-Idempotency-Key header — see Delivery guarantees). type matches one of the subscribable event names. installation_id is the hashid of the InstalledApp this delivery targets — use it to look up which install fired the event without inspecting the data block (whose shape varies per event type).
Lifecycle events
Two events are always delivered to your endpoint regardless of your events subscription:
app.installed — fired when an organization installs your app.
Code
The installer block identifies the user who clicked Install — always present on app.installed, regardless of whether the app declares any additional integrations. Use it to auto-provision or match an account on your landing page before the user arrives, so the post-install redirect can drop them straight into a signed-in experience rather than a sign-up form.
installer fields:
id— the installer's user hashid; same format the SDK surfaces elsewhere.email— key your account lookup on this.full_name— for greetings / display.
The credentials block is opt-in and only appears when both of these are true in the app manifest:
- An explicit
oauth: { type: "install" }block declares that the app wants install-integrated OAuth. - At least one declared permission maps to an OAuth scope (currently:
courses:read,modules:read,elements:read,members:read,activities:read).
See the OAuth chapter for the full end-to-end recipe — opt-in, storage shape, calling the API, refresh flow, and code samples in Node / Python / PHP.
Code
Without the oauth block, no tokens are minted even if the manifest declares OAuth-mappable permissions — an SDK-only app that adds courses:read for in-app use won't unexpectedly start receiving credentials in its webhook receiver logs. Adding oauth: { type: "install" } is the explicit opt-in.
SDK-only apps and apps with only SDK-scoped storage permissions receive no credentials — they can still deliver a signed webhook, they just don't get an access token for the public API. When credentials is present, use the tokens to call the public API on behalf of the installing organization — bearer-authenticated with access_token, refreshable via the standard OAuth 2.0 /oauth/token refresh flow using client_id + client_secret + refresh_token. Store access_token and refresh_token per-install (keyed by organization.id).
The client_secret is not in the webhook payload — you fetch it once from the Teachfloor developer dashboard (Developers → Apps → your app → OAuth Client Secret) or from the teachfloor apps create CLI response, and store it in your app config. It's the same across every install of your app.
credentials fields:
client_id— the app's OAuth client identifier. Same across every install; not sensitive. Delivered here for correlation.access_token— bearer token for public API calls. Expires perexpires_at.refresh_token— POST to/oauth/tokenwithgrant_type=refresh_tokento mint a new access token; refresh tokens rotate (the old one is invalidated).expires_at— ISO 8601 timestamp for the access token; refresh before this.token_type— alwaysBearer.scope— space-separated scopes granted to this token.
app.uninstalled — fired just before an organization uninstalls your app.
Same payload shape as app.installed minus the installer block. app.version reflects the version being uninstalled (relevant during upgrades — see below). Use this event to purge tenant state.
Version upgrades
When an organization upgrades your app to a new version, the platform tears down the old install and creates a new one. Your backend receives, in order:
app.uninstalledfor the old version (fired against the old endpoint URL if that changed between versions).app.installedfor the new version (fired against the new endpoint URL).
Design your handlers to be idempotent. The envelope-level installation_id changes between the old and new install; correlate by organization.id if you need to detect an upgrade rather than a fresh install.
If the new manifest drops the webhook block, only app.uninstalled fires — the app.installed event has nowhere to go. Treat the app.uninstalled as your last signal from that org.
Event catalog
You can subscribe to any of the following:
| Event | Fires when |
|---|---|
course.created | A course is created in the org |
course.updated | Course metadata changes |
course.completed | A learner marks a course as completed |
course.join | A learner joins a course |
module.created | A module is added to a course |
module.updated | Module metadata changes |
element.created | An element is added to a module |
element.updated | Element metadata changes |
element.deleted | An element is removed |
element.completed | A learner completes an element |
member.login | A member logs into the org |
app.installed | (always delivered) |
app.uninstalled | (always delivered) |
Subscribing to an unknown event name is rejected at manifest push time.
Delivery guarantees
- Retry — up to 3 attempts on any non-2xx response, with exponential backoff between attempts.
- Timeout — 10 seconds per attempt. Endpoints that take longer are considered failed and retried. If you need to do more work than 10 seconds allows, respond 2xx immediately and enqueue the work.
- Order — deliveries are queued and processed asynchronously. Events for the same organization may be delivered out of order if one takes multiple attempts to succeed. Use the envelope's
created_atif strict ordering matters. - At-least-once — a single event may be delivered more than once if your endpoint responds 200 after our timeout window. Dedupe on the
Teachfloor-Idempotency-Keyheader (equivalent to the envelopeid) — the header is stable across retry attempts, so it lets you short-circuit duplicate work before parsing the body. - HTTPS required — HTTP endpoints are rejected at manifest validation. TLS 1.2+ is expected on your endpoint.
Respond with any 2xx status code to acknowledge receipt. Any other response (4xx, 5xx, timeout) triggers a retry.
Security model
- Signature verification is your responsibility. A delivery that doesn't verify against your master secret is forged — reject it.
- The delivery URL and secret cross the wire only over HTTPS, both from admin to browser (when revealing the secret) and from Teachfloor to your endpoint.
- No inbound path is opened by declaring webhooks. Webhooks are strictly outbound — Teachfloor never calls into your app's SDK view or your platform-side runtime as a result of a webhook declaration.
- Uninstall removes the endpoint. When an org uninstalls your app, the webhook endpoint row is deleted (after the final
app.uninstalleddelivery). Further platform events do not attempt to reach your endpoint for that org.
Next Steps
→ Continue to OAuth
Additional Resources
- Realtime Channels - Sub-second event delivery into your app's SDK view while the learner is active. Complementary to webhooks (which reach your backend regardless).
- Permissions - The permission scopes your app can declare in its manifest.
- App Manifest - The full manifest schema, including the
webhookblock.