Collect real card payments inside your chatbot using Stripe Elements — PCI-compliant, zero card data on your server, supports Stripe Connect for split payments to connected accounts.
PCI-DSS Level 1 Stripe Elements Stripe Connect 4 Edge Functions
→
How It Works
User in Chat
→
Payment Node
→
Edge Function (create_intent)
→
Stripe API
Stripe.js encrypts card
→
Payment succeeded
→
Next node in flow
Card data never touches your server. Stripe.js encrypts it in the browser and sends it directly to Stripe's servers. Your Edge Function only ever sees the client_secret and PaymentIntent ID.
In the Stripe Dashboard → Developers → API keys. Copy both the Publishable key (pk_live_…) and Secret key (sk_live_…). Use pk_test_ / sk_test_ keys for testing.
Enable required payment methods
Dashboard → Settings → Payment methods. Enable Cards (Visa, Mastercard, Amex). Optionally enable Apple Pay, Google Pay.
2
Run the Database Migration
Create the stripe_payments table in your Supabase project.
Run this SQL in your Supabase project → SQL Editor. This creates the table that stores webhook payment events.
-- Run in Supabase SQL EditorCREATE TABLE IF NOT EXISTS stripe_payments (
id TEXT PRIMARY KEY, -- Stripe event ID (evt_xxx)
intent_id TEXT NOT NULL, -- pi_xxx PaymentIntent ID
amount INTEGER, -- amount in cents
currency TEXT,
status TEXT, -- succeeded | failed | canceled
description TEXT,
metadata JSONB,
account_id TEXT, -- connected Stripe account (acct_xxx)
created_at TIMESTAMPTZDEFAULT NOW(),
updated_at TIMESTAMPTZDEFAULT NOW()
);
-- Also add Stripe key columns to platform_settings (if not already present)ALTER TABLE platform_settings
ADD COLUMN IF NOT EXISTS stripe_publishable_key TEXT,
ADD COLUMN IF NOT EXISTS stripe_secret_key TEXT,
ADD COLUMN IF NOT EXISTS stripe_webhook_secret TEXT,
ADD COLUMN IF NOT EXISTS stripe_platform_fee_pct NUMERIC DEFAULT 0;
-- RLS: allow service role to write, anon to read their own paymentsALTER TABLE stripe_payments ENABLE ROW LEVEL SECURITY;
CREATE POLICY "service_all" ON stripe_payments
FOR ALL TO service_role USING (true);
-- ══════════════════════════════════════════════════════════════
-- NEW: stripe_connect_accounts — company-level Stripe Connect
-- One row per company (id = your internal sb_accounts.id).
-- Populated by stripe-connect-onboard, kept in sync by stripe-webhook
-- on account.updated, read by stripe-create-payment to auto-route
-- payments to the company's own connected Stripe account.
-- ══════════════════════════════════════════════════════════════CREATE TABLE IF NOT EXISTS stripe_connect_accounts (
id TEXT PRIMARY KEY, -- = sb_accounts.id
stripe_account_id TEXT NOT NULL, -- acct_xxx
email TEXT,
status TEXT DEFAULT'pending', -- pending | pending_review | active
charges_enabled BOOLEAN DEFAULTfalse,
payouts_enabled BOOLEAN DEFAULTfalse,
details_submitted BOOLEAN DEFAULTfalse,
updated_at TIMESTAMPTZDEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS stripe_connect_accounts_stripe_acct_idx
ON stripe_connect_accounts (stripe_account_id);
ALTER TABLE stripe_connect_accounts ENABLE ROW LEVEL SECURITY;
CREATE POLICY "service_all_connect" ON stripe_connect_accounts
FOR ALL TO service_role USING (true);
CREATE POLICY "auth_read_connect" ON stripe_connect_accounts
FOR SELECT TO authenticated USING (true);
After running the SQL, go to Table Editor in Supabase to confirm stripe_payments, stripe_connect_accounts both appear and platform_settings has the new columns.
3
Deploy the 4 Edge Functions
All 4 functions are already in supabase/functions/ — deploy them using the Supabase CLI.
POST
stripe-create-payment
Gets the publishable key and creates a PaymentIntent server-side. Called by sb-stripe.js from the browser. Auto-resolves a company's connected account via company_account_id.
POST
stripe-connect-onboard
Creates a Stripe Express connected account, returns the onboarding URL, and saves the account to stripe_connect_accounts when company_account_id is supplied.
GET
stripe-payment-status
Checks the status of a PaymentIntent by ID. Used for polling after payment confirmation.
POST
stripe-webhook
Receives Stripe webhook events, verifies signature, upserts payment records into stripe_payments, and syncs account.updated status into stripe_connect_accounts.
Deploy command
# Deploy all 4 Stripe Edge Functions at once
supabase functions deploy stripe-create-payment
supabase functions deploy stripe-connect-onboard
supabase functions deploy stripe-payment-status
supabase functions deploy stripe-webhook
The functions automatically read SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY from the Supabase environment — no manual secrets needed in the Supabase Dashboard → Secrets.
No CLI? Use stripe-deploy.html instead — copy-paste every SQL statement and every function's full source directly into the Supabase Dashboard (SQL Editor + Edge Functions Editor), no terminal required.
4
Save Your Stripe Keys in Platform Settings
The keys are stored server-side and read by Edge Functions — never exposed to the browser.
Open Superadmin → Platform Settings → Stripe Payments
Go to superadmin.html → click Platform Settings in the sidebar → scroll to the Stripe Payments card.
After creating the endpoint, click it → Signing secret → Reveal → copy whsec_…. Paste into Platform Settings → Stripe → Webhook Signing Secret.
The webhook verifies the Stripe-Signature header using HMAC-SHA256. If stripe_webhook_secret is empty, the signature check is skipped (useful for testing — add it before going live).
6
Build a Flow with a Payment Node
Drag a Payment node from the node picker and configure it in the inspector.
Payment Node Inspector Fields
Field
Example
Notes
Amount
29.99
In dollars (stored as cents internally)
Currency
usd
3-letter ISO code
Description
Support Plan — Monthly
Shown on Stripe dashboard + receipt
Pay Button Label
Pay $29.99
Leave blank to auto-generate
Status variable
paymentStatus
Set to succeeded or failed
Intent ID variable
paymentIntentId
Stripe pi_xxx ID
Connected Account
acct_1ABC…
Manual override. Leave blank to auto-use the company's own connected account (if onboarded) or the platform account
On Success → go to
Confirmation node
Next node after payment succeeds
On Failure → go to
Retry / Help node
Next node if card is declined
What the user sees in the chat
Support Plan — MonthlyUSD 29.99
4242 4242 4242 4242 12/27 123
Secured by Stripe
Stripe Connect (Split Payments)
Charge customers and automatically split the payment with a connected business account.
How Stripe Connect works in your flows
Your platform collects the card payment
Stripe automatically deducts your platform fee % (set in Platform Settings)
The remainder is transferred to the connected account (acct_xxx)
To onboard a business, call the stripe-connect-onboard Edge Function from your app — pass company_account_id so the resulting account is automatically saved and later auto-used by Payment nodes:
// Example: onboard a company's own Stripe accountconst res = await fetch('https://YOUR.supabase.co/functions/v1/stripe-connect-onboard', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ANON_KEY' },
body: JSON.stringify({
email: 'partner@company.com',
return_url: 'https://yourapp.com/onboard-complete',
refresh_url: 'https://yourapp.com/onboard-refresh',
company_account_id: '<their sb_accounts.id>', // auto-saves to stripe_connect_accounts
metadata: { company_id: 'acme-corp' }
})
});
const { url, account_id } = await res.json();
// account_id is already saved to stripe_connect_accounts — just redirect:
window.location.href = url;
Auto-routing payments to the company's account
Once onboarding completes (Stripe fires account.updated → webhook flips charges_enabled to true), just pass company_account_id instead of a manual account_id when creating a PaymentIntent — stripe-create-payment looks it up automatically:
Still to build: a company-facing stripe-settings.html page (in App Settings, alongside SMS/Email Settings) with a "Connect with Stripe" button that calls the flow above. The Edge Functions + stripe_connect_accounts table are ready — this UI page is the remaining piece. See stripe-deploy.html → Company-Level Stripe Connect for the full architecture.
Test Cards
Use these card numbers in test mode — any future date for expiry, any 3 digits for CVC.
Card Number
Result
Use case
4242 4242 4242 4242
✅ Succeeded
Happy path — payment succeeds
4000 0000 0000 0002
❌ Declined
Test your "On Failure" routing
4000 0025 0000 3155
3D Secure
Authentication required
4000 0000 0000 9995
❌ Insufficient funds
Test decline reasons
Payments Dashboard
Live view of all payments recorded by the webhook. Loads from the stripe_payments table.
Date
Description
Amount
Status
Intent ID
Loading payments…
Troubleshooting
❌ "Could not fetch Stripe publishable key"
Check that stripe_publishable_key is not empty in platform_settings (row id=global).
Make sure the stripe-create-payment Edge Function is deployed and responding.
Test with: curl -X POST https://YOUR.supabase.co/functions/v1/stripe-create-payment -H "Authorization: Bearer ANON_KEY" -d '{"action":"get_publishable_key"}'
❌ Card declined in test mode
Make sure you're using sk_test_ / pk_test_ keys (not live keys) during testing.
Use the test card 4242 4242 4242 4242 with any future date and any CVC.
❌ Webhook events not appearing in stripe_payments
Check the Stripe Dashboard → Webhooks → your endpoint → Event deliveries for failed attempts.
Confirm the stripe_webhook_secret in platform_settings matches the Signing secret shown in Stripe Dashboard.
The stripe_payments table must exist with RLS allowing service_role to write (see Step 2 SQL).
❌ CORS error calling Edge Function
All 4 Edge Functions already include Access-Control-Allow-Origin: * headers.
Make sure you're sending the Authorization: Bearer ANON_KEY header — missing auth causes Supabase to return a 401 which browsers treat as a CORS failure.