webhooksTeachable Webhook Events: A Field Guide to Payloads, Gaps, and Workarounds
A practical reference for developers building on Teachable webhooks — what each event actually sends, where the payload data is inconsistent, and how to build reliable infrastructure around the gaps.
All 20 V1 Teachable Webhooks are covered in this article. To find out more about a specific webhook, use the links below or scroll down to the start of the article
Enrollment events
Sale & transaction events
- Sale.created
- Sale.subscription_canceled
- Transaction.created
- Transaction.refunded
- AbandonedOrder.created
User events
- User.created
- User.updated
- User.subscribe_to_marketing_emails
- User.unsubscribe_from_marketing_emails
- EmailLead.created
Tagging events
Content & engagement events
Intro
Open with the moment every developer hits: you've set up a webhook listener, Teachable fires an event, and you start building your handler based on what you received. Three events later you realise the payload structure isn't what you expected — a field that was there before is missing, something is always null, and there's no documentation explaining why. This is the article that should have existed before you started. A field-level reference for what Teachable's webhooks actually send, where the data is inconsistent across event types, and how to write handlers that don't break when the payload doesn't match your assumptions.
Section 1 — How Teachable webhooks work (school owner accessible, ~200 words)
Quick conceptual grounding before the technical depth. Explain the event-driven model in plain language:
- Something happens in your school — a student enrolls, a sale completes, a comment is posted
- Teachable sends an HTTP POST to a URL you've configured
- Your server receives the payload and does something with it — updates a CRM, triggers an email, logs to a database
One paragraph on why this matters for school owners: webhooks are how you connect Teachable to everything else. If your CRM isn't updating when students enroll, or your reporting is always a step behind, webhooks are the mechanism that fixes that — when they're implemented correctly. One honest note: unlike some platforms, Teachable doesn't sign its webhook payloads with a signature header. That means your endpoint can't cryptographically verify that a request genuinely came from Teachable. Flag this here, expand in section 4.
Section 2 — The full event list (reference section, scannable)
A clean reference table of every available webhook event. Something like:
| Event | Trigger |
|---|---|
| enrollment.created | Student is enrolled in a course |
| enrollment.completed | Student completes a course |
| sale.created | A purchase is made |
| comment.created | A student posts a comment |
| user.created | A new user account is created |
| lesson.completed | A student completes a lesson |
Add any others you know from experience. Flag any that are undocumented or behave unexpectedly — even a brief note in the table is more than anyone else has published.
Section 3 — The payload inconsistency problem (the core of the article)
This is the section that makes the article worth bookmarking. Structure it event-by-event or field-by-field depending on what's clearest — I'd suggest event-by-event since that's how a developer will be reading it (they're handling a specific event and want to know what they'll get).
Key points to cover from what you know:
enrollment.completed and enrollment.created — include last_ip and current_ip, and last_four which is always null. Worth noting explicitly:
{
"last_four": null,
"current_ip": "203.0.113.42",
"last_ip": "203.0.113.41"
}Add a note: last_four appears to be a legacy field retained from an earlier version of the API. Don't build any logic that depends on it containing a value — it won't. comment.created and sale.created — no IP data. For sale.created specifically this is a genuine gap — knowing the IP of a purchaser has legitimate fraud detection use cases and it simply isn't there. For each inconsistency, follow this pattern:
- What you'd expect to be there
- What's actually there
- What that means for your handler
- The workaround if one exists
Section 4 — What Teachable webhooks don't do (the honest list)
This is your Section 4 equivalent from the B2B article — the limitations stated plainly. Based on what you know, likely includes:
No signature verification. Teachable doesn't send a signature header with webhook payloads. Any system that knows your endpoint URL can send a fake payload and your handler will process it. The workaround is a shared secret token as a query parameter or header that you validate on your side — not cryptographically perfect but significantly better than nothing.
No guaranteed delivery. Teachable will retry failed webhook deliveries but the retry logic isn't publicly documented — how many times, at what intervals, whether there's a dead letter mechanism. Build your handlers to be idempotent from day one.
No event for everything you'd want. Flag the specific gaps — things you'd expect to have a webhook that don't. If organization events aren't firing yet given the beta status, that's worth noting. Any others from your experience.
Payload size and structure can change without notice. You've seen this firsthand — fields appear, fields disappear, fields are always null. Don't treat the payload as a stable contract. Always write defensive handlers that check for field existence before using a value.
Section 5 — Writing reliable webhook handlers (practical patterns)
This is where the article earns its keep for the developer who's read this far. Three or four patterns: Always validate the event type first
if (!payload.event) {
return res.status(400).json({ error: 'Missing event type' })
}Check for field existence before using it
const ip = payload.user?.current_ip ?? null
// Don't assume it's there even if you've seen it beforeMake handlers idempotent
Use a unique identifier from the payload (enrollment ID, sale ID) to check whether you've already processed this event before acting on it. Teachable can and will send duplicate events on retry. Log the raw payload before processing Always log what Teachable actually sent before you do anything with it. When something breaks three months from now you'll want to know exactly what came in. Include the full payload, timestamp, and event type at minimum.
Section 6 — The purple callout + CTA
Same pattern as the B2B article. The pivot paragraph:
None of this is insurmountable, but it does mean that a production-grade Teachable webhook integration requires more defensive coding than most platforms. The inconsistencies are manageable once you know about them — the danger is building on assumptions that seem reasonable until the edge case hits your live system at 2am.
Building this yourself vs. having it built
If you're a developer, this guide gives you what you need. If you're a school owner who's just realised this is more complex than expected — that's a completely reasonable conclusion. This is the kind of infrastructure Purple Hippo builds as a fixed-scope project.Closing summary table
| Webhook event | Has IP data | Consistent payload | Notes |
|---|---|---|---|
| enrollment.created | ✅ | Mostly | last_four always null |
| enrollment.completed | ✅ | Mostly | last_four always null |
| sale.created | ❌ | Yes | No IP — gap for fraud detection |
| comment.created | ❌ | Yes | No IP |
| user.created | TBC | TBC | Add from your experience |
| lesson.completed | TBC | TBC | Add from your experience |
One production note Same datestamp approach as the B2B article — this one especially since Teachable's webhook behaviour may change as they develop the v2 API. "Last verified: May 2026" at the top protects the content's credibility as the platform evolves.
Purple Hippo Web Studio
Need this built, not just documented?
We build custom Teachable integrations, webhook infrastructure, and B2B enrollment portals. Fixed scope, fixed price, no surprises.
Book a free discovery call