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-Sha256 signature?
  • How do I handle order.created, enrollment.completed or user.signup events?
  • 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_body, hashlib.sha256).hexdigest(), then hmac.compare_digest on bytes (a non-ASCII str raises TypeError).
  • Return 401 on a missing, empty or malformed header, or a mismatch; 400 for a signed body that isn't a JSON object; 200 once 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.

TopicTriggered when
order.createdA student places an order
order_transaction.succeeded / .failed / .refundedA payment succeeds, fails or is refunded
enrollment.created / .completed / .progress / .trialA student enrols, finishes, progresses or starts a trial
user.signup / .signin / .updated / .deletedA user signs up, signs in, changes or is removed
course.created / .updated / .deletedA course changes
lesson.completed, quiz.attemptedA student completes a lesson or attempts a quiz
product.created / .updated / .deleted, plan.updatedCatalogue changes
subscription.past_due / .cancelled / .unpaidSubscription state changes
lead.created, app.uninstalledA lead is captured, an app is uninstalled
cart.abandonedA 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 13, 2026

View on GitHub →