Agent skill
Thinkific Webhooks Skill
Receive and verify Thinkific webhooks. Use when setting up a Thinkific webhook endpoint, verifying the X-Thinkific-Hmac-Sha256 signature, debugging 401s, or handling order.created, enrollment.completed, user.signup and course events.
Install this skill
npx skills add hookdeck/webhook-skills --skill thinkific-webhooks
Thinkific is an online course platform. Webhooks report leads, orders, transactions, subscriptions, users, enrollments, courses, lessons, quizzes, products and app uninstalls for one Thinkific site, registered per topic through the Webhooks API or the admin UI. Each request is signed: the X-Thinkific-Hmac-Sha256 header carries the hex HMAC-SHA256 of the raw request body.
When to Use This Skill
- How do I receive Thinkific webhooks?
- How do I verify the
X-Thinkific-Hmac-Sha256signature? - How do I handle
order.created,enrollment.completedoruser.signupevents? - Which secret signs a Thinkific webhook: the signing secret, the API key or the client secret?
- Why does my Thinkific webhook endpoint return 401?
Verification (core)
const crypto = require('crypto');
const HEX_SHA256 = /^[0-9a-fA-F]{64}$/;
// rawBody: the exact bytes received (Buffer), never re-serialised JSON
function verifyThinkificSignature(rawBody, signature, secret = process.env.THINKIFIC_WEBHOOK_SECRET) {
if (!secret || typeof signature !== 'string' || !HEX_SHA256.test(signature)) return false;
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
const received = Buffer.from(signature.toLowerCase(), 'hex'); // safe: format validated above
return crypto.timingSafeEqual(received, expected);
}
- Header:
X-Thinkific-Hmac-Sha256: <64 hex characters>, the bare digest with no prefix. Compare the whole value as one digest. Hex is case-insensitive. - Signed content: the raw request body bytes only. No timestamp or id is signed, so there is no replay window to check.
- Key: the secret's UTF-8 bytes, used as-is (not hex- or base64-decoded).
- Python:
hmac.new(secret.encode(), raw_, thenbody, hashlib.sha256).hexdigest() hmac.compare_digeston bytes (a non-ASCIIstrraisesTypeError). - Return
401on a missing, empty or malformed header, or a mismatch;400for a signed body that isn't a JSON object;200once handled. - Read the body raw (
express.raw({ type: '*/*' }),await request.arrayBuffer(),await request.body()) and parse it only after the check.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Types
Dispatch on the body's resource and action fields (topic = resource.action). Thinkific documents 27 topics; the ones below are the ones the examples handle or that have documented sample payloads.
| Topic | Triggered when |
|---|---|
order.created | A student places an order |
order_ / .failed / .refunded | A payment succeeds, fails or is refunded |
enrollment.created / .completed / .progress / .trial | A student enrols, finishes, progresses or starts a trial |
user.signup / .signin / .updated / .deleted | A user signs up, signs in, changes or is removed |
course.created / .updated / .deleted | A course changes |
lesson.completed, quiz.attempted | A student completes a lesson or attempts a quiz |
product.created / .updated / .deleted, plan.updated | Catalogue changes |
subscription.past_due / .cancelled / .unpaid | Subscription state changes |
lead.created, app.uninstalled | A lead is captured, an app is uninstalled |
cart.abandoned | A cart is abandoned (different envelope: name and data, no resource) |
Handle the few you need and log the rest. The X-Thinkific-Topic header also names the topic, but its only documented example is orders.created, so dispatch on the body.
Environment Variables
THINKIFIC_WEBHOOK_SECRET=your-thinkific-signing-secret
Which secret signs a webhook depends on how it was created: its own signing secret (admin UI, at least 16 characters, shown once; still being rolled out), the site's API key, or the OAuth app's client secret.
Using Hookdeck
Paste the webhook's signing secret. Older webhooks are signed with the site API key, and OAuth app webhooks with the app client secret.
Delivery
A 2xx within 5 seconds acknowledges. Otherwise Thinkific retries up to 14 times over about 16 hours (10 s, 30 s, 1 min, 2 min, ... 1 h at the 11th, 8 h at the 14th), and webhooks that keep failing may be deactivated. Delivery logs are kept 30 days. Make handlers idempotent (key on the event id).
Local Development
Thinkific can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 thinkific --path /webhooks/thinkific
Use the printed URL as the webhook URL in Thinkific.
Resources
- references/overview.md - What Thinkific webhooks are, topics, payload shape and delivery
- references/setup.md - Register a webhook, pick the signing secret, Hookdeck setup
- references/verification.md - Signature check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests