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:
- How the webhook envelope looks
- Zapier setup walkthrough
- Make.com setup walkthrough
- Pre-built recipe ideas
- Event payload reference
- 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:
| Header | Example | Notes |
|---|---|---|
Content-Type | application/json | Always. |
User-Agent | Kindryn-Webhook/1.0 | Useful for filtering in logs. |
X-Kindryn-Event | onMemberJoin | Mirrors event in the body for quick routing. |
X-Kindryn-Delivery | whd_01HX... | Unique ID — use for idempotency on your side. |
X-Kindryn-Signature | sha256=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.
- 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. - 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. - 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.
- 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.
- 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. - (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
testsynthetic payload. - 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.authorNameshows 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.
- 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 likehttps://hook.us1.make.com/abcd.... Copy it. - In Kindryn: _Settings -> Webhooks -> New webhook_. Paste the Make URL, pick your events, save.
- 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.)
- 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.
- 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.
| Recipe | Trigger event | Destination |
|---|---|---|
| New member -> email list | onMemberJoin | Mailchimp / ConvertKit / Buttondown add-subscriber |
| New member -> CRM contact | onMemberJoin | HubSpot / Attio / Notion create row |
| New post -> Slack channel | onPostCreate | Slack _Send channel message_ — embed title + excerpt |
| New post -> Discord channel | onPostCreate | Discord webhook with an embed |
| New comment -> Slack thread | onCommentCreate | Slack _Reply to thread_ keyed by post ID |
| Event created -> Notion calendar | onEventCreate | Notion create-database-row |
| RSVP -> Google Sheet attendance log | onEventRsvp | Google Sheets append-row |
| Course enroll -> Mailchimp tag | onCourseEnroll | Mailchimp _Add tag to subscriber_ |
| Lesson complete -> ConvertKit sequence | onLessonComplete | ConvertKit _Subscribe to sequence_ |
| Coaching booked -> Calendly-style DM | onCoachingBook | Slack DM / Twilio SMS to coach |
| Member joined -> Stripe customer note | onMemberJoin | Stripe _Create customer note_ |
| Post created -> Buffer queue | onPostCreate | Buffer _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.dataon most webhook modules — not the parsed JSON)
Compare the result to X-Kindryn-Signature (stripping the sha256= prefix) in a filter step.
#Troubleshooting
| Symptom | Likely 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 401 | Zapier / Make URL is gated. Double-check the catch URL is public. |
| Signature never matches | You're verifying against the parsed JSON, not the raw body. Use the raw bytes. |
| Events fire sometimes but not always | Your webhook is only subscribed to a subset of events — check the edit form. |
| Retry storm filling Make queue | Your 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.
