All docs

Documentation

Integrations — Zapier, Make, and any webhook consumer

Source: docs/INTEGRATIONS.md

#Integrations — Zapier, Make, and any webhook consumer

Kindryn's webhook system is vendor-agnostic: any service that can accept a signed HTTP POST can receive Kindryn events. Zapier and Make (formerly Integromat) are the two no-code automation platforms we explicitly test and document here, but the same setup works for n8n, Pipedream, Pabbly, or a Node/Deno script of your own.

This guide covers:

  1. How the webhook envelope looks
  2. Zapier setup walkthrough
  3. Make.com setup walkthrough
  4. Pre-built recipe ideas
  5. Event payload reference
  6. Verifying the signature on the receiving end

#1. The webhook envelope

Every delivery Kindryn sends is a JSON POST with this exact shape:

{
  "event": "onMemberJoin",
  "payload": { "communityId": "...", "memberId": "...", ... },
  "deliveryId": "whd_...",
  "timestamp": "2026-04-11T14:02:18.472Z"
}

And these headers:

HeaderExampleNotes
Content-Typeapplication/jsonAlways.
User-AgentKindryn-Webhook/1.0Useful for filtering in logs.
X-Kindryn-EventonMemberJoinMirrors event in the body for quick routing.
X-Kindryn-Deliverywhd_01HX...Unique ID — use for idempotency on your side.
X-Kindryn-Signaturesha256=4f2e...HMAC-SHA256 of the raw body, hex-encoded.

Retries: if your endpoint returns a non-2xx or the request times out (10s), Kindryn retries up to 5 times with exponential backoff (1s, 2s, 4s, 8s, 16s). Each attempt reuses the same deliveryId, so your consumer should dedupe on that ID.


#2. Zapier setup walkthrough

Zapier's free Webhooks by Zapier trigger speaks HTTP POST out of the box, so no Kindryn-specific Zapier app is required.

  1. In Zapier: click _Create Zap_, pick Webhooks by Zapier as the trigger app, and choose the Catch Hook event. Zapier gives you a URL that looks like https://hooks.zapier.com/hooks/catch/1234567/abcde/. Copy it.
  2. In Kindryn: open your community, go to Settings -> Webhooks (admin only), click New webhook, and paste the Zapier URL into _Endpoint URL_. Name it Zapier — <what it does> so future-you knows what it powers.
  3. Pick events: check only the events you want this Zap to receive. Each event type should usually get its own webhook (and its own Zap) — mixing them makes the Zapier filter step more complex.
  4. Save the webhook. Kindryn auto-generates a signing secret — you can copy it now if you plan to verify signatures in a _Code by Zapier_ step (see §6). Most simple Zaps skip verification because Zapier hook URLs are already secret.
  5. Generate a test event: back in Kindryn, click the Test button on the webhook row. Kindryn fires a synthetic event: "test" delivery through the same retry pipeline. In Zapier, click _Test Trigger_ — you should see the sample payload appear within a few seconds.
  6. (Alternative) Copy a realistic test payload: click Copy sample payload on the webhook card, pick the event you care about, and paste the JSON into Zapier's _Raw Body_ field if you want a more realistic example than the test synthetic payload.
  7. Build your action step in Zapier — Mailchimp, Google Sheets, Slack, Notion, Airtable, etc. Map fields from payload.*.

#Zapier gotchas

  • Zapier parses nested JSON automatically: payload.authorName shows up as its own field in the UI. No need for a JSON parser step.
  • If you hit Zapier's 1-task-per-minute polling limit on a free plan, that's a plan limit, not a Kindryn limit — Kindryn delivers instantly.
  • Zapier's _Test_ payloads are cached. After editing your Kindryn webhook's event list, click _Test Trigger_ again to refresh the sample.

#3. Make.com setup walkthrough

Make calls webhooks Custom Webhooks. The flow mirrors Zapier's.

  1. In Make: create a new scenario, add the Webhooks module, pick Custom webhook, and click _Add_. Name it something like Kindryn — new member. Make assigns a URL like https://hook.us1.make.com/abcd.... Copy it.
  2. In Kindryn: _Settings -> Webhooks -> New webhook_. Paste the Make URL, pick your events, save.
  3. Determine data structure: Make needs to see at least one payload before it can map fields. Click Redetermine data structure in Make, then either:
  • Click Test on the Kindryn webhook row, or
  • Trigger the real event (create a post, add a member, etc.)
  1. Make captures the sample and the fields become available for downstream modules. Connect a router, a Slack module, a Google Sheets row — whatever your automation needs.
  2. Turn the scenario ON and set a schedule. Make's webhook modules run instantly on receipt (not on a schedule), so "On demand" is fine.

#Make gotchas

  • Make rejects a payload if the scenario is off. Turn the scenario on before clicking Kindryn's Test button, or the test delivery will retry 5 times and then mark itself FAILED.
  • Make's webhook queue holds deliveries briefly if the scenario is paused. If you see delayed events, check the scenario's queue.

#4. Pre-built recipe ideas

These are the flows most Kindryn admins wire up first. Each one is listed with the Kindryn event it subscribes to and the downstream app you'd plug into. We don't ship actual zap templates (Zapier's template sharing is opinionated about branding) but these recipes are the mental model to copy.

RecipeTrigger eventDestination
New member -> email listonMemberJoinMailchimp / ConvertKit / Buttondown add-subscriber
New member -> CRM contactonMemberJoinHubSpot / Attio / Notion create row
New post -> Slack channelonPostCreateSlack _Send channel message_ — embed title + excerpt
New post -> Discord channelonPostCreateDiscord webhook with an embed
New comment -> Slack threadonCommentCreateSlack _Reply to thread_ keyed by post ID
Event created -> Notion calendaronEventCreateNotion create-database-row
RSVP -> Google Sheet attendance logonEventRsvpGoogle Sheets append-row
Course enroll -> Mailchimp tagonCourseEnrollMailchimp _Add tag to subscriber_
Lesson complete -> ConvertKit sequenceonLessonCompleteConvertKit _Subscribe to sequence_
Coaching booked -> Calendly-style DMonCoachingBookSlack DM / Twilio SMS to coach
Member joined -> Stripe customer noteonMemberJoinStripe _Create customer note_
Post created -> Buffer queueonPostCreateBuffer _Add to queue_ for cross-posting

Every one of these is a single-trigger, single-action Zap or Make scenario. You shouldn't need filter or code steps for the common case.


#5. Event payload reference

All 14 lifecycle events are documented below with a sample payload you can paste into a Zapier / Make test step. You can also call GET /api/communities/:slug/webhooks/sample-payload?event=<name> on your own Kindryn instance (admin-only) to get a live sample, or omit event to get every sample in one JSON blob keyed by event name.

Every sample is wrapped in the standard delivery envelope shown in §1.

#onMemberJoin

Fired when a new member joins the community (invite accepted, open signup, or admin add).

{
  "event": "onMemberJoin",
  "payload": {
    "communityId": "cmt_abc123",
    "communitySlug": "acme",
    "memberId": "mem_abc123",
    "userId": "usr_abc123",
    "email": "[email protected]",
    "name": "Jordan Rivera",
    "role": "MEMBER",
    "joinedAt": "2026-04-11T14:02:18.472Z"
  },
  "deliveryId": "whd_abc123",
  "timestamp": "2026-04-11T14:02:18.472Z"
}

#onMemberLeave

Fired when a member leaves or is removed from the community.

{
  "event": "onMemberLeave",
  "payload": {
    "communityId": "cmt_abc123",
    "communitySlug": "acme",
    "memberId": "mem_abc123",
    "userId": "usr_abc123",
    "email": "[email protected]",
    "name": "Jordan Rivera",
    "leftAt": "2026-04-11T14:02:18.472Z"
  }
}

#onPostCreate

Fired when a post is published in any space.

{
  "event": "onPostCreate",
  "payload": {
    "communityId": "cmt_abc123",
    "communitySlug": "acme",
    "postId": "pst_abc123",
    "spaceId": "spc_abc123",
    "spaceSlug": "general",
    "authorId": "mem_abc123",
    "authorName": "Jordan Rivera",
    "title": "Welcome to the community!",
    "excerpt": "Excited to be here and start learning from everyone...",
    "createdAt": "2026-04-11T14:02:18.472Z"
  }
}

#onPostDelete

Fired when a post is soft-deleted.

{
  "event": "onPostDelete",
  "payload": {
    "communityId": "cmt_abc123",
    "communitySlug": "acme",
    "postId": "pst_abc123",
    "spaceId": "spc_abc123",
    "deletedBy": "mem_abc123",
    "deletedAt": "2026-04-11T14:02:18.472Z"
  }
}

#onCommentCreate

Fired when a comment is added to a post.

{
  "event": "onCommentCreate",
  "payload": {
    "communityId": "cmt_abc123",
    "communitySlug": "acme",
    "commentId": "cmt_abc123",
    "postId": "pst_abc123",
    "authorId": "mem_abc123",
    "authorName": "Jordan Rivera",
    "excerpt": "Great post — thanks for sharing!",
    "createdAt": "2026-04-11T14:02:18.472Z"
  }
}

#onEventCreate

Fired when an event is created in any space.

{
  "event": "onEventCreate",
  "payload": {
    "communityId": "cmt_abc123",
    "communitySlug": "acme",
    "eventId": "evt_abc123",
    "spaceId": "spc_abc123",
    "spaceSlug": "events",
    "title": "Monthly office hours",
    "description": "Bring your questions to the group Q&A.",
    "startsAt": "2026-04-18T14:02:18.472Z",
    "endsAt": "2026-04-18T15:02:18.472Z",
    "createdAt": "2026-04-11T14:02:18.472Z"
  }
}

#onEventRsvp

Fired when a member RSVPs to an event (GOING, MAYBE, NOT_GOING, or removed).

{
  "event": "onEventRsvp",
  "payload": {
    "communityId": "cmt_abc123",
    "communitySlug": "acme",
    "eventId": "evt_abc123",
    "memberId": "mem_abc123",
    "status": "GOING",
    "rsvpedAt": "2026-04-11T14:02:18.472Z"
  }
}

#onCourseEnroll

Fired when a member enrolls in a course.

{
  "event": "onCourseEnroll",
  "payload": {
    "communityId": "cmt_abc123",
    "communitySlug": "acme",
    "courseId": "crs_abc123",
    "memberId": "mem_abc123",
    "enrolledAt": "2026-04-11T14:02:18.472Z"
  }
}

#onLessonComplete

Fired when a member marks a lesson complete.

{
  "event": "onLessonComplete",
  "payload": {
    "communityId": "cmt_abc123",
    "communitySlug": "acme",
    "lessonId": "lsn_abc123",
    "courseId": "crs_abc123",
    "memberId": "mem_abc123",
    "completedAt": "2026-04-11T14:02:18.472Z"
  }
}

#onCoachingBook

Fired when a member books a coaching session.

{
  "event": "onCoachingBook",
  "payload": {
    "communityId": "cmt_abc123",
    "communitySlug": "acme",
    "sessionId": "ses_abc123",
    "memberId": "mem_abc123",
    "coachId": "mem_coach1",
    "startsAt": "2026-04-13T14:02:18.472Z",
    "endsAt": "2026-04-13T14:32:18.472Z",
    "bookedAt": "2026-04-11T14:02:18.472Z"
  }
}

#onInstall / onUninstall / onEnable / onDisable

Plugin lifecycle events. Useful for ops dashboards and audit trails.

{
  "event": "onInstall",
  "payload": {
    "communityId": "cmt_abc123",
    "communitySlug": "acme",
    "installationId": "pin_abc123",
    "pluginSlug": "sample-plugin",
    "pluginName": "Sample Plugin",
    "installedAt": "2026-04-11T14:02:18.472Z"
  }
}

#6. Verifying the signature

Every Kindryn webhook carries an X-Kindryn-Signature header. The value is sha256=<hex> where <hex> is the HMAC-SHA256 of the raw request body using the webhook's signing secret.

Verify on the receiving end before trusting the payload — especially if your endpoint is publicly reachable.

#Node.js / Deno

import { createHmac, timingSafeEqual } from 'node:crypto'

function verifyKindrynSignature(secret, header, rawBody) {
  if (!header || !header.startsWith('sha256=')) return false
  const expected = createHmac('sha256', secret).update(rawBody).digest('hex')
  const provided = header.slice('sha256='.length)
  if (expected.length !== provided.length) return false
  return timingSafeEqual(Buffer.from(expected), Buffer.from(provided))
}

Important: rawBody must be the exact bytes you received, before any JSON parsing. If you're using Express, use express.raw({ type: '*/*' }) on the webhook route (not express.json()) so the body middleware doesn't re-serialize it.

#Python

import hmac, hashlib

def verify_kindryn_signature(secret: str, header: str, raw_body: bytes) -> bool:
    if not header or not header.startswith("sha256="):
        return False
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    provided = header[len("sha256="):]
    return hmac.compare_digest(expected, provided)

#Zapier _Code by Zapier_ step (JavaScript)

const crypto = require('crypto')
const secret = 'paste-your-kindryn-secret-here'
const header = inputData.signature // map to X-Kindryn-Signature
const rawBody = inputData.rawBody // map to Raw Body (not parsed JSON)

const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
const provided = header.replace('sha256=', '')

if (expected !== provided) {
  throw new Error('Invalid Kindryn signature — dropping event.')
}
output = { verified: true }

#Make.com _Tools -> HMAC_ module

Make has a built-in HMAC generator. Feed it:

  • Algorithm: SHA256
  • Key: your webhook's signing secret
  • Input: the raw request body from the Webhook trigger (available as 1.data on most webhook modules — not the parsed JSON)

Compare the result to X-Kindryn-Signature (stripping the sha256= prefix) in a filter step.


#Troubleshooting

SymptomLikely cause / fix
Zapier says "Hook not received"Kindryn hasn't fired yet — click _Test_ on the Kindryn webhook row.
All deliveries marked FAILED with HTTP 401Zapier / Make URL is gated. Double-check the catch URL is public.
Signature never matchesYou're verifying against the parsed JSON, not the raw body. Use the raw bytes.
Events fire sometimes but not alwaysYour webhook is only subscribed to a subset of events — check the edit form.
Retry storm filling Make queueYour scenario is off. Turn it on, then redeliver via the Kindryn "Test" button.
"Delivery history is preserved" — where is it?_Settings -> Webhooks -> Logs_ button on each webhook row. Full request/response.

Feature matrix / status: see the Integrations & Comms section of FEATURES.md at the repository root.