checkout.session.completed webhook and finishes everything in 38 seconds. We have run it for 41 new clients across 2025. Failure rate so far: 0%. This is the exact build, the node JSON, and the one deduplication mistake that cost us a duplicate Slack channel before we caught it.
client-acme-corp and invites the assigned team), and Notion (clones a project-page template, fills in the client name, and links the Slack channel). A final node updates a "clients" airtable with the project ID. Anything that fails routes to an error sub-workflow that pings ops on a separate Slack channel.
## Why this matters now — November 2025
Stripe shipped [improved checkout-session metadata in March 2025](https://docs.stripe.com/changelog/basil/2025-03-31/checkout-legacy-subscription-upgrade) — you can now pass arbitrary key-value pairs that survive the entire payment flow. This made it possible to handle "what plan did they buy" inside the webhook handler without an extra Stripe API call. Combined with Slack's revamped conversations.create endpoint (December 2024) which dropped the deprecated channels.create, the modern stack for client onboarding is simpler than it was 12 months ago. The whole flow runs on n8n v1.65 (released October 2025).
A founder in [r/n8n posted in September 2025](https://www.reddit.com/r/n8n/) about a similar onboarding flow that broke when Stripe added new event versions — the lesson: pin your Stripe API version in the webhook endpoint, or read the version field defensively in your handler.
## The 5-block workflow
{
"parameters": {
"httpMethod": "POST",
"path": "stripe-onboarding",
"responseMode": "responseNode",
"options": {
"rawBody": true
}
},
"name": "Stripe webhook in",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [240, 300],
"webhookId": "REDACTED"
}rawBody: true flag is non-negotiable. Stripe signs the raw request body bytes — if n8n parses the JSON before signature verification, the bytes change and the signature mismatches. We learned this the painful way on a Sunday at 11 pm.
### Node 2 — Signature verification (Code node)
{
"parameters": {
"jsCode": "const crypto = require('crypto');\nconst secret = $env.STRIPE_WEBHOOK_SECRET;\nconst sig = $input.item.json.headers['stripe-signature'];\nconst body = $input.item.json.body;\nconst parts = sig.split(',').reduce((m, p) => { const [k,v] = p.split('='); m[k]=v; return m; }, {});\nconst signed = parts.t + '.' + body;\nconst expected = crypto.createHmac('sha256', secret).update(signed).digest('hex');\nif (expected !== parts.v1) {\n throw new Error('Stripe signature mismatch');\n}\nconst now = Math.floor(Date.now() / 1000);\nif (Math.abs(now - parseInt(parts.t)) > 300) {\n throw new Error('Stripe webhook too old');\n}\nreturn [{ json: JSON.parse(body) }];"
},
"name": "Verify Stripe sig",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [460, 300]
}{
"parameters": {
"method": "POST",
"url": "https://slack.com/api/conversations.create",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "slackApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{ "name": "Content-Type", "value": "application/json" }
]
},
"sendBody": true,
"bodyContentType": "json",
"jsonBody": "={\n \"name\": \"client-{{ $json.client_slug }}\",\n \"is_private\": true,\n \"team_id\": \"{{ $env.SLACK_WORKSPACE_ID }}\"\n}",
"options": {}
},
"name": "Create Slack channel",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [900, 240]
}conversations.create endpoint by a few months in 2024. The pattern client-{{slug}} is enforced by a Code node upstream that converts the company name to a kebab-case slug — Slack channel names cannot exceed 80 chars and must be lowercase.
## The credentials checklist (45 minutes, do this once)
- Stripe webhook subscribed to checkout.session.completed only (no extra events)
- Webhook signing secret stored in n8n env vars as STRIPE_WEBHOOK_SECRET, not in workflow JSON
- Signature verification node throws on mismatch (test with a tampered body)
- Deduplication Airtable lookup wired before any side-effect node
- Slack channel name is lowercase, kebab-case, max 80 chars (test with a 60-char company name)
- Notion template page shared with the integration (verify in Notion Share menu)
- ConvertKit form ID parameter set per project — never hardcoded
- Error workflow set to ping a private "ops-alerts" Slack channel
- Manual test run completed end-to-end with a $1 Stripe test charge in last 24 hours
- Workflow JSON exported to git the day of go-live
options.rawBody: true on the Webhook node. Re-test.
Symptom: "Duplicate Slack channels for the same client." Cause: Stripe retried the webhook because we returned 504 once (n8n was restarting). No dedup check. Fix: add the Airtable lookup before the Slack create. Make the lookup the first side-effect-free node in the chain.
Symptom: "Slack channel created but no team members invited." Cause: conversations.invite requires user IDs, not emails. We were passing emails. Fix: use users.lookupByEmail first to resolve emails to IDs, then pass the IDs to invite. Add the lookup as a separate HTTP Request node.
Symptom: "Notion page created but template body is empty." Cause: Notion's API does not deep-clone children of a page in a single call. Fix: use the Notion "Duplicate page" UI workflow once to create a real template, then in n8n use the page-create endpoint with a children array — or call the unofficial duplicate endpoint (/v1/pages/{id}/duplicate) which returns a job ID you must poll.
Symptom: "ConvertKit subscriber added but no welcome email." Cause: subscribed to a tag, not a form. Tags do not trigger automation. Fix: pass the form ID, not the tag ID, in the API call. The endpoint is /v3/forms/{form_id}/subscribe.
Symptom: "Workflow timeout on parallel branches." Cause: ConvertKit's API is slow during peak hours (we have seen 8-12 second responses). The aggregator was timing out at 10 seconds. Fix: bump the workflow's "Save & Execute Timeout" setting to 60 seconds. Track the slowest branch separately so you can switch providers later if needed.
## The deduplication trick — full SQL-style logic
Before any side-effect, we run a Code node that does a two-phase commit:
// 1. Look up by Stripe session ID
const sessionId = $input.item.json.data.object.id;
const existing = await $http.get(
'https://api.airtable.com/v0/' + $env.AIRTABLE_BASE +
'/processed_sessions?filterByFormula={session_id}=\\\'' + sessionId + '\\\''
);
if (existing.records.length > 0) {
return [{ json: { skip: true, reason: 'already_processed', sessionId } }];
}
// 2. Insert reservation row immediately (idempotency token)
await $http.post(
'https://api.airtable.com/v0/' + $env.AIRTABLE_BASE + '/processed_sessions',
{ fields: { session_id: sessionId, status: 'processing', started_at: new Date().toISOString() } }
);
return [{ json: $input.item.json }];client-fabflora was live at 14:23:21. Notion project page populated by 14:23:32. Welcome email landed in the founder's inbox at 14:23:41. The assigned account manager joined the Slack channel and posted "hi" at 14:31 — eight minutes from payment to first human contact, mostly waiting for him to read the auto-DM. Total elapsed: 38 seconds for the automation, 8 minutes for the human handoff. The CFO emailed us in week 2 saying "I have never seen onboarding this clean from any vendor" — usable as a testimonial.
For another build in the same family, see our [n8n receptionist workflow](/blog/n8n-receptionist-twilio-whisper-claude-call-routing). Both share the signature-verification + dedup pattern.
## When not to build this
Skip this workflow if (a) you onboard fewer than 4 clients per month — manual onboarding is honestly faster than maintaining a 22-node automation, (b) your contracts vary so wildly that there is no shared "kickoff" template — automation needs a stable shape, (c) your team's Slack workspace is the free tier — channel limits will bite you within 6 months. We turned down a client in May 2025 specifically because their custom proposal flow had no shared structure to automate against.
For the full [AI automation services](/services/ai-automation) we offer including this onboarding flow, see the [Softechinfra team's case work on Radiant Finance](/projects/radiant-finance) — same n8n stack, different industry.
## FAQ
### Why n8n and not Zapier for this onboarding flow?
Three reasons. Zapier charges per "task" — our 22-node flow would consume ~16 tasks per onboarding (Zapier counts each step). At 41 clients × 16 tasks = 656 tasks/month, you would need a $200/month Zapier plan. n8n self-hosted runs unlimited executions on a ₹740/month box. Second, Zapier has no first-class signature verification helpers — you would write a custom Python step. Third, Zapier hides errors behind their UI; n8n's execution log gives you raw payloads for debugging.
### How do I handle test-mode vs live-mode webhooks?
Stripe sends a livemode boolean in every event. Branch on it early — route test events to a "test" Notion database and a "test" Slack workspace if you have one. We use one webhook URL but two routing tables based on livemode.
### What if Slack returns "name_taken" on channel create?
Add a numeric suffix and retry up to 3 times. Our retry pattern: client-acme-2, client-acme-3, client-acme-4. After 3 attempts, route to the error workflow which DMs ops to handle manually. We have not hit attempt 3 in 41 onboardings.
### Can this work with Razorpay instead of Stripe?
Yes, with one swap. Razorpay's webhook signature is HMAC-SHA256 of the raw body keyed with your webhook secret — slightly different formula from Stripe's. Replace the verification Code node with a Razorpay-specific one. Everything downstream stays the same.
### How do I version the Notion template?
Keep the template page in a "Templates" Notion database. Add a "version" property. The workflow reads the latest active template by query — when you publish a new version, mark the old one inactive and the workflow picks up the new one on the next run with no code change.
### What about GDPR / India DPDP for storing client data in Airtable?
Airtable bases are stored in US data centres. For Indian clients under DPDP, document this in your privacy policy and DPA. We are migrating sensitive fields to a self-hosted Postgres for clients who need data residency in India — the n8n workflow swaps the Airtable nodes for Postgres nodes with a one-line change.
### How long does this take to build for someone new to n8n?
A first-time n8n builder needs about 4-5 working days. Day 1: credentials setup. Day 2: Stripe webhook + verification. Day 3: parallel branches. Day 4: error handling and dedup. Day 5: testing with real Stripe test charges. We ship it for clients in 7 days because we have a reusable template; from scratch it is closer to 5 days of focused work.
Want this onboarding automation built into your CRM?
We ship the Stripe + ConvertKit + Slack + Notion onboarding flow — wired into your existing tools, with template approvals and credentials setup — in 7 working days. Typical cost: ₹62,000 fixed scope. Suitable for any SMB onboarding 3+ clients per week.
Book a 20-min Call
