Sell access to members-only pages: when someone pays with Stripe, they are added to your Notion members database with the right tier and get a welcome email that tells them how to log in. This is the same flow Super runs for its own paid Pro Trial, packaged as an AI skill - your AI assistant asks for your details and builds the automation for you in n8n, Make or Zapier.
Stripe payment → n8n / Make / Zapier → row in your Notion members database → Super lets them in
└→ welcome email: "log in with this email at <members page>"Before you start
- A Super site on the Personal plan or higher with Super Memberships connected to a Notion members database (Email + Tier multi-select) and at least one page protected by a tier.
- A Stripe account with a payment link per membership product.
- An n8n, Make or Zapier account, and a way to send email (SendGrid, Gmail, Resend…).
Download
The zip holds the skill for Claude: instructions, the n8n workflow, the Make and Zapier steps and the welcome email. Everything is also below - open a section and copy it.
You are helping me sell access to members-only pages of my Super website (super.so) with Stripe.
How Super Memberships works - keep to these facts:
- Super takes no payments and has no member API. It reads ONE Notion database: one row per member, an Email property (Email, Text or Title type) and a Tier property (Multi-select). A visitor gets into a gated page when their row has at least one of the tiers that page requires.
- Members log in with a one-time email link (valid 1 hour). Super sends that link itself; it sends nothing when a member is added.
- Tier options must already exist in Notion and be chosen on the page in Super (Pages → page → Protection → Membership). An automation must never invent a new tier name.
- Removed access ends within 24 hours for members who are already logged in.
Build me an automation in {{n8n | Make | Zapier}} that does exactly this:
1. Trigger on the Stripe event checkout.session.completed only (no second payment event, or I get duplicates).
2. Verify the event: use the platform's native Stripe trigger (it checks Stripe's signature); only with a generic webhook, re-fetch the session from the Stripe API by id with my key.
3. Continue only if payment_status is paid or no_payment_required and status is complete.
4. Skip session ids that were already processed.
5. Map the payment link to a tier and a members page URL with a lookup table:
{{plink_… → tier "…" → https://mysite.com/members}}
6. Find the member row by email (lower-cased, trimmed) in my members database {{database URL}}, properties Email = "{{…}}", Tier = "{{…}}".
7. Found → ADD the tier to the existing multi-select (never replace). Not found → create the row with name, email and tier.
8. Send a welcome email from {{sender address}} via {{SendGrid | Gmail | …}} with click tracking off. It says: open the members page (button), enter THIS email (show it in bold), click the login link that arrives. Mention that another email address will not be recognised.
9. {{If subscriptions:}} a second flow on customer.subscription.deleted that removes only that tier.
Output:
- n8n: a complete workflow JSON I can import, using credentials (Stripe API, Notion API, SendGrid) - no keys in the JSON.
- Make / Zapier: numbered module-by-module steps with the exact field values and formulas.
- Then a test plan with Stripe test mode (card 4242 4242 4242 4242): workflow run → Notion row with tier → welcome email → login link → gated page opens; and a second purchase with the same email creates no duplicate.
If you have the Super MCP server (https://mcp.super.so/mcp) connected: look up my site with list_sites / list_pages first - each page shows membershipTierIds, the Notion option ids of the tiers that gate it - and match them to the Tier options in my Notion database before building. If you have Notion access, read the members database schema yourself instead of asking me.{
"name": "Stripe → Super Membership (welcome email)",
"nodes": [
{
"name": "Stripe Trigger",
"type": "n8n-nodes-base.stripeTrigger",
"typeVersion": 1,
"position": [
0,
0
],
"webhookId": "550ef8ae-05ed-4a0a-916c-4af854685449",
"notes": "Registers the webhook in Stripe on activation and verifies every event's signature.",
"parameters": {
"events": [
"checkout.session.completed"
]
},
"id": "5a418e60-9541-41f0-9ff3-8eea6e0adadf"
},
{
"name": "Is Paid Checkout",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
208,
0
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "99f8694d-3698-4638-ad06-4d5b39c9887d",
"leftValue": "={{ $json.type }}",
"rightValue": "checkout.session.completed",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
},
{
"id": "565f2d24-6431-4206-bbc4-63e3cf0aed96",
"leftValue": "={{ ['paid', 'no_payment_required'].includes($json.data.object.payment_status) ? 'paid' : $json.data.object.payment_status }}",
"rightValue": "paid",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
},
{
"id": "a19ec253-4a35-44b7-931e-61cde5d76f8e",
"leftValue": "={{ $json.data.object.status }}",
"rightValue": "complete",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "8678e43b-2f33-43e4-9b66-74456dc05b5f"
},
{
"name": "Skip Already Processed",
"type": "n8n-nodes-base.removeDuplicates",
"typeVersion": 2,
"position": [
416,
0
],
"parameters": {
"operation": "removeItemsSeenInPreviousExecutions",
"logic": "removeItemsWithAlreadySeenKeyValues",
"dedupeValue": "={{ $json.data.object.id }}",
"options": {}
},
"id": "fbb84377-470b-49f7-8421-b50434ad855b"
},
{
"name": "Plan Lookup",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
624,
0
],
"parameters": {
"jsCode": "// ---- EDIT THIS NODE ONLY -------------------------------------------------\n// One row per Stripe payment link (plink_...) that grants membership.\n// tier must be an EXISTING option of the Tier multi-select in your Notion members database.\nconst PLANS = {\n \"plink_REPLACE_ME\": {\n productName: \"Pro Membership\",\n tier: \"pro\",\n accessUrl: \"https://yoursite.com/members\",\n },\n};\n\nconst CONFIG = {\n notionDatabaseId: \"REPLACE_WITH_MEMBERS_DATABASE_ID\",\n emailProperty: \"Email\", // the property Super uses as Email\n emailPropertyType: \"email\", // \"email\", \"rich_text\" or \"title\" - the Notion type of that property\n tierProperty: \"Tier\", // the multi-select property Super uses as Tier\n nameProperty: \"Name\", // the title property (ignored when emailPropertyType is \"title\")\n brandName: \"Your Brand\",\n logoUrl: \"https://yoursite.com/logo.png\",\n accent: \"#047857\",\n fromEmail: \"hello@yoursite.com\",\n fromName: \"Your Brand\",\n};\n// ---------------------------------------------------------------------------\n\nconst session = $input.first().json.data.object;\nconst plan = PLANS[session.payment_link];\n// Purchases that do not grant membership end the run quietly.\nif (!plan) return [];\n\nconst customerEmail = String(\n (session.customer_details && session.customer_details.email) || session.customer_email || \"\"\n).trim().toLowerCase();\nif (!customerEmail) throw new Error(`Checkout session ${session.id} has no customer email`);\n\nreturn [{ json: {\n ...CONFIG,\n ...plan,\n customerEmail,\n customerName: String((session.customer_details && session.customer_details.name) || \"\").trim(),\n sessionId: session.id,\n emailSubject: `Your access to ${plan.productName} is ready`,\n} }];"
},
"id": "471a498d-f1bf-42f8-9473-bcf71a2e52db"
},
{
"name": "Notion: Find Member",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
832,
0
],
"parameters": {
"method": "POST",
"url": "={{ 'https://api.notion.com/v1/databases/' + $json.notionDatabaseId + '/query' }}",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "notionApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Notion-Version",
"value": "2022-06-28"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ filter: { property: $json.emailProperty, [$json.emailPropertyType]: { equals: $json.customerEmail } }, page_size: 1 }) }}",
"options": {}
},
"id": "2afc77f6-a8d0-46ce-ad35-2111b26ceab0"
},
{
"name": "Build Member Update",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1040,
0
],
"parameters": {
"jsCode": "// Existing row: merge the tier into the multi-select (never replace - members keep earlier tiers).\n// No row: create one with name, email and tier.\nconst plan = $('Plan Lookup').first().json;\nconst existing = ($input.first().json.results || [])[0];\nconst NOTION = 'https://api.notion.com/v1/pages';\n\nif (existing) {\n const current = ((existing.properties[plan.tierProperty] || {}).multi_select || []).map(o => o.name);\n const tiers = [...new Set([...current, plan.tier])];\n return [{ json: { method: 'PATCH', url: `${NOTION}/${existing.id}`,\n body: { properties: { [plan.tierProperty]: { multi_select: tiers.map(name => ({ name })) } } } } }];\n}\n\nconst text = [{ text: { content: plan.customerEmail } }];\nconst name = [{ text: { content: plan.customerName || plan.customerEmail } }];\nconst properties = { [plan.tierProperty]: { multi_select: [{ name: plan.tier }] } };\nif (plan.emailPropertyType === 'title') {\n properties[plan.emailProperty] = { title: text };\n} else {\n properties[plan.nameProperty] = { title: name };\n properties[plan.emailProperty] = plan.emailPropertyType === 'email' ? { email: plan.customerEmail } : { rich_text: text };\n}\nreturn [{ json: { method: 'POST', url: NOTION,\n body: { parent: { database_id: plan.notionDatabaseId }, properties } } }];"
},
"id": "e29a866c-b761-4570-bee3-3264b3c8f54e"
},
{
"name": "Notion: Upsert Member",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
1248,
0
],
"parameters": {
"method": "={{ $json.method }}",
"url": "={{ $json.url }}",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "notionApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Notion-Version",
"value": "2022-06-28"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.body) }}",
"options": {}
},
"id": "e2291832-bf0d-4b2f-8481-199d5f15fd2e"
},
{
"name": "Welcome Email",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1456,
0
],
"parameters": {
"assignments": {
"assignments": [
{
"id": "6dabfe73-a2e8-439a-91c5-5d8b838fff63",
"name": "emailHtml",
"type": "string",
"value": "=<div style=\"margin:0;padding:0;background:#f7f7f5;font-family:Arial,Helvetica,sans-serif;color:#191918;\">\n <div style=\"max-width:640px;margin:0 auto;padding:32px 20px;\">\n <div style=\"background:#ffffff;border:1px solid #e6e3dc;border-radius:16px;overflow:hidden;\">\n\n <div style=\"background:#ffffff;padding:26px 32px;text-align:center;\">\n <img src=\"{{ $('Plan Lookup').item.json.logoUrl }}\" alt=\"{{ $('Plan Lookup').item.json.brandName }}\" width=\"150\"\n style=\"display:block;width:150px;max-width:100%;height:auto;margin:0 auto;border:0;\" />\n </div>\n\n <div style=\"background:{{ $('Plan Lookup').item.json.accent }};padding:36px 32px;text-align:center;\">\n <p style=\"margin:0 0 10px;font-size:12px;line-height:18px;letter-spacing:1.4px;font-weight:700;color:#ffffff;opacity:.85;text-transform:uppercase;\">\n {{ $('Plan Lookup').item.json.productName }}\n </p>\n <h1 style=\"margin:0;font-size:32px;line-height:40px;color:#ffffff;\">\n Your membership is ready\n </h1>\n </div>\n\n <div style=\"padding:36px 32px;\">\n <p style=\"margin:0 0 18px;font-size:16px;line-height:26px;\">Hello,</p>\n\n <p style=\"margin:0 0 18px;font-size:16px;line-height:26px;color:#55534e;\">\n Thank you for your purchase. You now have access to <strong style=\"color:#191918;\">{{ $('Plan Lookup').item.json.productName }}</strong>.\n There is no password to remember - you sign in with your email:\n </p>\n\n <ol style=\"margin:0 0 22px;padding-left:22px;font-size:15px;line-height:25px;color:#55534e;\">\n <li>Open the members page with the button below.</li>\n <li>Enter this email address: <strong style=\"color:#191918;\">{{ $('Plan Lookup').item.json.customerEmail }}</strong></li>\n <li>Click the login link we send you. It works once and is valid for one hour.</li>\n </ol>\n\n <a href=\"{{ $('Plan Lookup').item.json.accessUrl }}\"\n style=\"display:inline-block;background:{{ $('Plan Lookup').item.json.accent }};color:#ffffff;text-decoration:none;font-size:15px;font-weight:700;padding:14px 22px;border-radius:8px;\">\n Open the members area →\n </a>\n\n <p style=\"margin:30px 0 0;font-size:15px;line-height:25px;color:#55534e;\">\n Use the same email you paid with - another address will not be recognised.\n If the page does not let you in, just reply to this email.\n </p>\n\n <div style=\"border-top:1px solid #e6e3dc;margin-top:30px;padding-top:24px;font-size:14px;color:#55534e;\">\n {{ $('Plan Lookup').item.json.brandName }}\n </div>\n </div>\n </div>\n </div>\n</div>\n"
}
]
},
"includeOtherFields": true,
"options": {}
},
"id": "e72bc24e-3e5e-4b14-9308-1e2996b42be0"
},
{
"name": "SendGrid: Send Email",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
1664,
0
],
"parameters": {
"method": "POST",
"url": "https://api.sendgrid.com/v3/mail/send",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "sendGridApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ personalizations: [{ to: [{ email: $('Plan Lookup').item.json.customerEmail }] }], from: { email: $('Plan Lookup').item.json.fromEmail, name: $('Plan Lookup').item.json.fromName }, subject: $('Plan Lookup').item.json.emailSubject, content: [{ type: 'text/html', value: $json.emailHtml }], tracking_settings: { click_tracking: { enable: false, enable_text: false } } }) }}",
"options": {
"response": {
"response": {
"responseFormat": "text"
}
}
}
},
"id": "0ce67071-af6d-4bd0-9475-fff19c9868fa"
}
],
"connections": {
"Stripe Trigger": {
"main": [
[
{
"node": "Is Paid Checkout",
"type": "main",
"index": 0
}
]
]
},
"Is Paid Checkout": {
"main": [
[
{
"node": "Skip Already Processed",
"type": "main",
"index": 0
}
]
]
},
"Skip Already Processed": {
"main": [
[
{
"node": "Plan Lookup",
"type": "main",
"index": 0
}
]
]
},
"Plan Lookup": {
"main": [
[
{
"node": "Notion: Find Member",
"type": "main",
"index": 0
}
]
]
},
"Notion: Find Member": {
"main": [
[
{
"node": "Build Member Update",
"type": "main",
"index": 0
}
]
]
},
"Build Member Update": {
"main": [
[
{
"node": "Notion: Upsert Member",
"type": "main",
"index": 0
}
]
]
},
"Notion: Upsert Member": {
"main": [
[
{
"node": "Welcome Email",
"type": "main",
"index": 0
}
]
]
},
"Welcome Email": {
"main": [
[
{
"node": "SendGrid: Send Email",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}---
name: stripe-to-super-memberships
description: Builds a paid-membership automation for a Super site - a Stripe payment adds the buyer to the Notion members database that Super Memberships reads, with the right tier, and sends a branded welcome email that tells them how to log in. Produces an importable n8n workflow, a Make scenario plan or a Zapier Zap plan, checks the Super site and the Notion database through the Super and Notion MCP servers when they are connected, and walks the user through testing with Stripe test mode. Use when someone wants Stripe (payment links, checkout, subscriptions) to grant access to members-only pages on a Super site, wants to automate adding members to the Notion access list, or asks how to send a welcome or login email for Super Memberships. Do not use for Super's own billing or plan upgrades, for MemberSpace or Outseta setups, for gating pages with a password, or for payment providers other than Stripe.
---
# Stripe → Super Memberships (n8n, Make or Zapier)
Super Memberships does not take payments and has no member API. Super reads one Notion database
(one row per member, an **Email** property and a **Tier** multi-select) and lets an email in when its row
holds one of the tiers a page requires. So "connect Stripe to memberships" always means:
~~~
Stripe payment ──► automation (n8n / Make / Zapier) ──► row in the Notion members database ──► Super lets them in
└──► welcome email: "log in with this email at <members page>"
~~~
This is the flow Super runs in production for its own paid Pro Trial, built in n8n: a Stripe payment adds the
buyer to Super's members database with the tier `trial`, and the welcome email sends them to a members-only
booking page where they log in with the email they paid with.
## Step 1 - Gather the facts before building anything
Ask only for what you cannot look up. You need:
| Fact | How to get it |
|---|---|
| Automation platform: n8n, Make or Zapier | Ask |
| The Super site and its members-only pages | Super MCP: `list_sites` / `find_site`, then `list_pages`; every page returns `membershipTierIds` (Notion option ids of the tiers that gate it, empty = public) |
| The members database, its Email and Tier properties, the tier names | Notion MCP: fetch the database; map the multi-select option ids to names, and match them to `membershipTierIds` from Super |
| Which Stripe product grants which tier | Ask for the payment link (`plink_…`) or price id (`price_…`) per tier. Stripe MCP, when connected, can list payment links and prices |
| Who sends the email and from which address | Ask: SendGrid, Gmail, Resend, Postmark or the platform's own mail module |
| One-time purchase or subscription | Ask; subscriptions need the cancellation path in Step 4 |
Check before you build, and stop with a clear explanation if any fails:
1. The site is on the **Personal plan or higher** and Membership is connected (the dashboard Membership card shows the database). If no page has `membershipTierIds`, tell the user to protect pages in **Pages → page → Protection → Membership** first.
2. The Tier property is a **Multi-select** and every tier the automation writes already exists as an option. Do not invent tier names: Super tracks tiers by option id, so a new option created by the automation gates nothing until the user picks it on a page.
3. The member's email is unique per row, or the automation updates the existing row instead of adding a second one (Super merges duplicate rows, but duplicates make cancellation unreliable).
Without MCP access, ask the user to paste: the database URL and its property names, the tier names, the
members-only page URL and the payment link ids.
## Step 2 - The workflow (same logic on every platform)
1. **Trigger** - Stripe event `checkout.session.completed`. Subscribe to this one event only; adding `payment_intent.succeeded` or `invoice.paid` for the same purchase creates duplicates.
2. **Verify** - the event must be genuine. Use the platform's native Stripe trigger: it registers the webhook in Stripe and verifies the signature of every event (Super's production workflow does this). If you must use a generic webhook instead, **re-fetch the session from the Stripe API by its id** with your own key - a forged request cannot produce a real paid session.
3. **Paid?** - continue only when `payment_status` is `paid` or `no_payment_required` and `status` is `complete`.
4. **Deduplicate** - skip session ids already processed (Stripe retries webhooks).
5. **Map** - `payment_link` (or the line item's `price.id`) → tier name, members page URL, product name. Keep it in one lookup table so adding a product is a one-line change. Email = `customer_details.email`, trimmed and lower-cased.
6. **Find member** - query the members database for a row whose Email equals the address.
7. **Upsert** - found: add the tier to the existing multi-select (merge, never replace, or the member loses tiers they bought earlier). Not found: create a row with Name, Email and Tier.
8. **Welcome email** - see Step 3.
9. Optional: notify the team (Slack, Discord) with product and email.
Write the Notion row with the database id as `parent.database_id` (API version `2022-06-28`) or the data
source id as `parent.data_source_id` (API version `2025-09-03` and newer). Mixing the two is the most common
cause of a 404 from Notion.
### n8n
Start from `references/n8n-workflow.json` - it is Super's live Pro Trial flow with the Super-specific values
replaced by placeholders: **Stripe Trigger → Is Paid Checkout → Skip Already Processed → Plan Lookup →
Notion: Find Member → Build Member Update → Notion: Upsert Member → Welcome Email → SendGrid**. All settings
live in the **Plan Lookup** node: the `PLANS` table (payment link → tier, members page, product name) and
`CONFIG` (database id, property names and Notion types, brand, sender). Hand the user the JSON to import
(**Workflows → ⋯ → Import from file / paste**). Credentials: *Stripe API* (secret key, or a restricted key
with write access to Webhook Endpoints so the trigger can register itself), *Notion API* (an internal
integration shared with the members database), *SendGrid API* (or swap the email node). Activating the
workflow registers the webhook in Stripe automatically - there is no URL to copy.
The email goes out only after the Notion row is written, so a buyer is never told to log in before they are a member.
To post each purchase to Slack, add a Slack node after **Plan Lookup**.
**Second entry path (optional).** Super's flow also listens to `customer.subscription.created`, for
subscriptions that are started in its own app rather than through a payment link, filtered by a subscription
metadata key; the email comes from `GET /v1/customers/{customer}`. Add this only for subscriptions created
outside Checkout and always filter it (by metadata or price), otherwise a payment-link subscription fires
both events and the buyer gets two emails.
If the n8n instance MCP server is connected and allowed to create workflows, create it there directly, inactive,
and give the user the editor link.
### Make and Zapier
Follow `references/make.md` or `references/zapier.md`. They map each step above to that platform's modules.
Neither platform imports a generated file reliably, so give numbered click-by-click steps with the exact
field values, not a blueprint.
## Step 3 - The welcome email
Super sends no email when a member is added; it only sends a login link when someone asks for one on a
gated page. The welcome email is what tells a buyer how to get in. Use `references/welcome-email.html`
(the layout of Super's own trial email) and fill in: product name, members page URL, the buyer's email.
It must say, in this order:
1. Open the members page (button with the URL).
2. Enter **this exact email** - the one used at checkout. Show it in bold. A different address is "not a member".
3. Click the login link that arrives (valid 1 hour, works once).
Send with click tracking **off** (SendGrid `tracking_settings.click_tracking.enable: false`) so the link is
not rewritten. Sender should be a domain with SPF/DKIM set up.
### Optional: branded login emails too
Super's login email comes from `no-reply@links.super.site`. To send it from the user's own address, add a
second, separate workflow: **Webhook trigger → email**, and paste its URL into Site → Settings → Membership
→ Login settings → **Login webhook URL**, press **Test**, then enable **Skip sending the magic-link email**
and publish the site. The webhook receives `{ email, magicLinkUrl, expiresAt }` (plus `test: true` on the
test call). `magicLinkUrl` is a live credential: HTTPS only, never log it, never send it anywhere except the
member's inbox. Prefix the subject with `[TEST] ` when `test` is true.
## Step 4 - Cancellations and refunds (subscriptions)
Add a second trigger path for `customer.subscription.deleted` (and `charge.refunded` if refunds should revoke):
look up the customer's email (`GET /v1/customers/{customer}`), find the member row, **remove only the tier
that subscription granted**. Delete the row only when no tier is left. Tell the user an already logged-in
member keeps access for up to 24 hours; disconnecting and reconnecting the database in Super forces an
immediate re-check.
## Step 5 - Test before going live
1. Stripe **test mode**: create a test payment link for the product, point a test-mode webhook at the workflow.
2. Pay with card `4242 4242 4242 4242`, any future date, any CVC, using an email you can read.
3. Check, in order: the workflow run succeeded → the Notion row exists with the tier → the welcome email arrived with the right link → on the members page, entering that email sends a login link → the gated page opens. (Super MCP: `get_link` for the page URL; the dashboard **View login log** shows *Login requested / Link sent / Login succeeded*.)
4. Pay a second time with the same email: no duplicate row, tier unchanged.
5. Switch Stripe to live mode, create the live webhook endpoint and live payment links, update the `PLANS` table to the live `plink_` ids.
## What not to do
- Do not put secret keys, Notion tokens or webhook URLs into the workflow as plain text; use the platform's credential store.
- Do not trigger on more than one payment event for the same purchase.
- Do not replace the Tier multi-select on update; merge.
- Do not create tier options from the automation; they must exist in Notion and be chosen on a page in Super.
- Do not promise instant revocation; sessions are re-checked at most every 24 hours.
- Super MCP cannot connect the members database, set page tiers or set the login webhook; those are dashboard steps for the user. Say so instead of attempting them.# Make scenario - Stripe → Super Membership
Same logic as the n8n workflow, module by module. Module names can differ slightly between Make versions;
when a named module is missing, use the app's **Make an API call** module with the request shown.
| # | Module | Settings |
|---|---|---|
| 1 | **Stripe → Watch Events** (instant) | Connection with a restricted key. Event type: `checkout.session.completed` only. Make registers the webhook in Stripe for you and receives verified events. |
| 2 | **Stripe → Make an API call** | `GET /v1/checkout/sessions/{{1.data.object.id}}`. Re-reads the session with your key. |
| 3 | Filter on the link 2 → 4 | `payment_status` *In* `paid, no_payment_required` **and** `status` = `complete`. |
| 4 | **Data store → Check existence of a record** | Key `{{2.body.id}}`. Filter after it: continue only when the record does **not** exist (Stripe retries webhooks). Create the data store first with one field `processedAt`. |
| 5 | **Tools → Switch** (or a lookup in a data store) | Input `{{2.body.payment_link}}`; one case per `plink_…` → tier name. Add a second Switch for the members page URL if tiers open different pages. No match → a filter stops the run. |
| 6 | **Tools → Set multiple variables** | `email` = `{{lower(trim(2.body.customer_details.email))}}`, `tier` = output of 5, `accessUrl`, `productName`. |
| 7 | **Notion → Make an API call** | `POST /v1/databases/<DATABASE_ID>/query`, header `Notion-Version: 2022-06-28`, body `{"filter":{"property":"Email","email":{"equals":"{{6.email}}"}},"page_size":1}`. |
| 8 | **Router** | Route A filter: `{{length(7.body.results)}}` = 0. Route B filter: greater than 0. |
| 8A | **Notion → Make an API call** | `POST /v1/pages`, body `{"parent":{"database_id":"<DATABASE_ID>"},"properties":{"Name":{"title":[{"text":{"content":"{{6.email}}"}}]},"Email":{"email":"{{6.email}}"},"Tier":{"multi_select":[{"name":"{{6.tier}}"}]}}}` |
| 8B | **Notion → Make an API call** | `PATCH /v1/pages/{{7.body.results[1].id}}`, body `{"properties":{"Tier":{"multi_select":[{"name":"{{join(distinct(add(map(7.body.results[1].properties.Tier.multi_select; "name"); 6.tier)); "\"},{\"name\":\"")}}"}]}}}` - keeps the member's earlier tiers and adds the new one. |
| 9 | **SendGrid → Send an email** (or Gmail → Send an email) on both routes | To `{{6.email}}`, subject `Your access to {{6.productName}} is ready`, HTML body = `welcome-email.html` with the placeholders mapped. With SendGrid, turn click tracking off. |
| 10 | **Data store → Add/replace a record** | Key `{{2.body.id}}`, `processedAt` = `{{now}}`. |
Turn on the scenario (scheduling: **Immediately**). Test with Stripe test mode first (a separate Stripe
connection with the test key), then switch the connection in module 1 and 2 to the live key.
**Cancellations** (subscriptions): a second scenario, **Stripe → Watch Events** `customer.subscription.deleted`
→ `GET /v1/customers/{{1.data.object.customer}}` → module 7 query by email → `PATCH` the Tier with
`remove(map(...; "name"); "<tier>")`.
**Branded login email** (optional): a third scenario, **Webhooks → Custom webhook** → Gmail/SendGrid email with
`magicLinkUrl` as the button. Paste the webhook URL into Super → Settings → Membership → Login settings, press
**Test**, then enable **Skip sending the magic-link email** and publish the site.# Zapier Zap - Stripe → Super Membership
Same logic as the n8n workflow, step by step. Zapier step names change now and then; pick the closest match.
Zapier cannot import a generated Zap, so give the user these steps with their own values filled in.
| # | Step | Settings |
|---|---|---|
| 1 | **Trigger: Stripe → Checkout Session Completed** (instant) | Connect Stripe (test mode account first). Zapier registers and verifies the webhook. |
| 2 | **Filter by Zapier** | `Payment Status` *(Text) Exactly matches* `paid` - OR - `no_payment_required`; and `Payment Link` *is in* your membership links. |
| 3 | **Formatter → Utilities → Lookup Table** | Lookup key `Payment Link`; rows `plink_…` → tier name. Add a second lookup table for the members page URL if tiers open different pages. |
| 4 | **Formatter → Text → Lowercase** (then **Trim Whitespace**) | Input `Customer Details Email`. |
| 5 | **Notion → Find Database Item** with **"Create Notion Database Item if it doesn't exist yet?"** ticked | Database: your members database. Search property Email = step 4. Create fields: Name = email, Email = email, Tier = step 3. |
| 6 | **Code by Zapier → Run JavaScript** | Input `existing` = step 5 Tier (comma separated), `tier` = step 3. Code: `const t = new Set((inputData.existing || '').split(',').map(s => s.trim()).filter(Boolean)); t.add(inputData.tier); return { tiers: [...t].join(',') };` |
| 7 | **Notion → Update Database Item** | Item = step 5 ID; Tier = step 6 `tiers`. Keeps earlier tiers and adds the new one; safe to run twice. |
| 8 | **Gmail → Send Email** or **SendGrid → Send Email** | To step 4, subject `Your access to <product> is ready`, body type HTML = `welcome-email.html` with fields mapped. Avoid *Email by Zapier* - it sends from a Zapier address that often lands in spam. |
Zapier deduplicates trigger events itself, so no extra dedupe step is needed.
**Cancellations** (subscriptions): a second Zap, **Stripe → Canceled Subscription** → **Stripe → Find Customer**
for the email → **Notion → Find Database Item** → Code step that removes the tier from the list → **Update Database Item**.
**Branded login email** (optional): a third Zap, **Webhooks by Zapier → Catch Hook** → Gmail/SendGrid with
`magicLinkUrl` as the button. Paste the hook URL into Super → Settings → Membership → Login settings, press
**Test**, then enable **Skip sending the magic-link email** and publish the site. Webhooks by Zapier needs a paid
Zapier plan.<!--
Welcome email for a new paid member. Inline styles only (email clients ignore <style>).
Placeholders: {{PRODUCT_NAME}}, {{MEMBER_EMAIL}}, {{ACCESS_URL}}, {{BRAND_NAME}}, {{LOGO_URL}}, {{ACCENT}} (hex, e.g. #047857).
This is the layout Super's own n8n flow sends; swap the logo, colour and copy for your brand.
-->
<div style="margin:0;padding:0;background:#f7f7f5;font-family:Arial,Helvetica,sans-serif;color:#191918;">
<div style="max-width:640px;margin:0 auto;padding:32px 20px;">
<div style="background:#ffffff;border:1px solid #e6e3dc;border-radius:16px;overflow:hidden;">
<div style="background:#ffffff;padding:26px 32px;text-align:center;">
<img src="{{LOGO_URL}}" alt="{{BRAND_NAME}}" width="150"
style="display:block;width:150px;max-width:100%;height:auto;margin:0 auto;border:0;" />
</div>
<div style="background:{{ACCENT}};padding:36px 32px;text-align:center;">
<p style="margin:0 0 10px;font-size:12px;line-height:18px;letter-spacing:1.4px;font-weight:700;color:#ffffff;opacity:.85;text-transform:uppercase;">
{{PRODUCT_NAME}}
</p>
<h1 style="margin:0;font-size:32px;line-height:40px;color:#ffffff;">
Your membership is ready
</h1>
</div>
<div style="padding:36px 32px;">
<p style="margin:0 0 18px;font-size:16px;line-height:26px;">Hello,</p>
<p style="margin:0 0 18px;font-size:16px;line-height:26px;color:#55534e;">
Thank you for your purchase. You now have access to <strong style="color:#191918;">{{PRODUCT_NAME}}</strong>.
There is no password to remember - you sign in with your email:
</p>
<ol style="margin:0 0 22px;padding-left:22px;font-size:15px;line-height:25px;color:#55534e;">
<li>Open the members page with the button below.</li>
<li>Enter this email address: <strong style="color:#191918;">{{MEMBER_EMAIL}}</strong></li>
<li>Click the login link we send you. It works once and is valid for one hour.</li>
</ol>
<a href="{{ACCESS_URL}}"
style="display:inline-block;background:{{ACCENT}};color:#ffffff;text-decoration:none;font-size:15px;font-weight:700;padding:14px 22px;border-radius:8px;">
Open the members area →
</a>
<p style="margin:30px 0 0;font-size:15px;line-height:25px;color:#55534e;">
Use the same email you paid with - another address will not be recognised.
If the page does not let you in, just reply to this email.
</p>
<div style="border-top:1px solid #e6e3dc;margin-top:30px;padding-top:24px;font-size:14px;color:#55534e;">
{{BRAND_NAME}}
</div>
</div>
</div>
</div>
</div>Install in your AI assistant
Claude
- Settings → Capabilities → Skills → Upload skill, choose
stripe-to-super-memberships.zip. - Settings → Connectors → Add custom connector, URL
https://mcp.super.so/mcp, then sign in to Super. Add the Notion connector too, so Claude can read your members database. - Ask: "Connect my Stripe payment link to Super Memberships with n8n."
Claude checks your site and its protected pages through the Super MCP server, reads the tiers from Notion, asks only for what it cannot look up (payment links, sender), and hands you the finished workflow. With the n8n or Zapier MCP server connected as well, it can create the workflow in your account directly. In Claude Code, unzip the skill into ~/.claude/skills/.
ChatGPT
ChatGPT has no skill upload, so the skill becomes a project:
- Create a Project, paste the Prompt into the project instructions and upload the files from the zip.
- Connect Super: Settings → Apps & Connectors, add a custom connector with
https://mcp.super.so/mcp(needs developer mode on your plan), and connect Notion. - Ask in the project: "Build it for Make."
Notion AI
- Create a Custom Agent, paste the Prompt into its instructions and add the n8n, Make and Zapier sections as pages.
- Notion AI already sees your workspace, so it can create the members database for you or fix one that does not match (Email property, Tier as multi-select, tier options).
- Super MCP: in the agent's Tools & Access → Custom MCP server, add
https://mcp.super.so/mcp(Business or Enterprise plan; a workspace admin enables custom MCP servers first). - Notion AI cannot open n8n, Make or Zapier - it writes the workflow and you import it.
What each assistant can do
Claude | ChatGPT | Notion AI | |
Install the skill | Upload zip | Project instructions + files | Custom Agent instructions |
Read your Super site (MCP) | Yes | Yes, custom connector | Yes, Business/Enterprise |
Read your members database | Notion connector | Notion connector | Native, can also create it |
Create the workflow for you | With n8n / Zapier MCP | With n8n / Zapier MCP | No - import / follow steps |
n8n, Make or Zapier?
n8n | Make | Zapier | |
What you get | Workflow JSON - import and fill in | Module-by-module steps | Step-by-step Zap |
Stripe trigger | Stripe Trigger node (verifies signatures) | Stripe Watch Events | Stripe Checkout Session Completed |
Duplicate protection | Remove Duplicates node | Data store check | Built in |
Add tier, keep old ones | Code node merges | distinct(add(map(…))) formula | Code by Zapier step |
Best for | Self-hosting, full control | Visual builder, low cost | Fastest setup |
The welcome email
Super only sends the login link when someone asks for it on a gated page - adding a member sends nothing. The welcome email closes that gap: open the members page → enter this exact email → click the login link. The template in the zip follows the email Super sends to its own trial buyers; change the logo, colour and copy.
Want Super's login email to come from your own address too? The skill also sets up the optional login webhook flow - see Super Memberships → Login settings.
Test it
- Stripe test mode, test payment link, card
4242 4242 4242 4242, an email you can read. - Check: workflow ran → Notion row has the tier → welcome email arrived → the members page sends a login link → the page opens.
- Pay again with the same email: no duplicate row.
- Switch to live keys and live payment links.
Removing a member (or a cancelled subscription) ends access within 24 hours for members who are already logged in. Disconnect and reconnect the database in Super to force it immediately.
Still evolving. The Super MCP server, Notion and the AI assistants are all changing fast, so you may run into limits. Tell us in MCP feedback: we want to know, and if there is a workaround, we will help you with it.