Every new Softechinfra client used to take 90 minutes of human time on day one — an account manager copying form fields into ConvertKit, creating the Slack channel by hand, opening a Notion page from a template, scheduling a Calendly link. We replaced that with a 22-node n8n workflow that fires on a Stripe 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.
TL;DR — what this onboarding workflow does
Stripe sends a webhook on payment. n8n verifies the signature, extracts client metadata (name, email, project type, plan), then in parallel fans out to four destinations: ConvertKit (subscribes the client to a 6-email welcome sequence), Calendly (sends a personalised booking link via Postmark), Slack (creates a private channel like 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 — 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 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
The actual n8n node JSON (3 critical nodes)
Node 1 — Stripe Webhook receiver
{
"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"
} The 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]
}Two checks. First, the HMAC matches Stripe's documented v1 signature. Second, the timestamp is within 5 minutes — protects against replay attacks where someone captures an old webhook and replays it later.
Node 3 — Slack create channel (HTTP Request)
{
"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]
} We call Slack via HTTP Request rather than the built-in Slack node because the built-in node lagged behind the new 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)
The cost — what this actually adds to your bill
The marginal cost of running this workflow is the Postmark line, for 10,000 transactional emails a month. Everything else rides on plans you almost certainly already pay for.
The pre-flight checklist
- 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 Stripe test charge in last 24 hours
- Workflow JSON exported to git the day of go-live
Common mistakes — symptoms first, fixes second
Symptom: "Stripe shows the webhook delivered, but n8n shows nothing." Cause: rawBody flag missing on the Webhook node, n8n is silently parsing the body, signature fails, webhook returns 200 OK to Stripe but the workflow never starts. Fix: set 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 }];The two-phase pattern means a second concurrent webhook for the same session sees the "processing" row and exits. We have only seen this race condition fire twice in 41 onboardings, but both times the dedup row prevented a duplicate Slack channel.
Mini case study — Coimbatore D2C client, October 2025
A 32-person D2C apparel brand in Coimbatore signed up via our Stripe checkout on October 9, 2025 at 14:23. Webhook fired at 14:23:08. Slack channel 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. 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 we offer including this onboarding flow, see the Softechinfra team's case work on 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 higher-tier Zapier plan. n8n self-hosted runs unlimited executions on a small self-hosted 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. Fixed scope. Suitable for any SMB onboarding 3+ clients per week.
Book a 20-min CallFor more on how Vivek Kumar thinks about onboarding automation versus white-glove account management, see the founder portfolio.
