STRIPE INTEGRATION - nself-org/cli GitHub Wiki
Comprehensive guide to integrating Stripe for billing, subscriptions, usage-based pricing, and payment processing with nself.
Time Estimate: 30-40 minutes Difficulty: Intermediate Prerequisites: Stripe account, nself project initialized
- Stripe Account Setup
- Plugin Installation
- Webhook Configuration
- Subscription Billing
- Usage-Based Billing
- One-Time Payments
- Stripe Connect (Marketplaces)
- Testing
- Production Deployment
- Troubleshooting
- Go to stripe.com
- Click "Sign up"
- Enter business information
- Complete email verification
Test Mode Keys (for development):
- Go to Dashboard > Developers > API keys
- Copy Publishable key (starts with
pk_test_) - Reveal and copy Secret key (starts with
sk_test_PLACEHOLDER)
Live Mode Keys (for production):
- Complete business verification first
- Toggle to Live mode in dashboard
- Go to API keys
- Copy Publishable key (
pk_live_) - Reveal and copy Secret key (
sk_live_)
Configure:
- Business name and address
- Support email and phone
- Statement descriptor (appears on customer statements)
- Tax settings (if applicable)
Go to: Dashboard > Settings > Business settings
cd your-project
nself plugin install stripeEdit .env:
# Stripe Configuration
STRIPE_API_KEY=sk_test_PLACEHOLDER_secret_key_here
STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key_here
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret_here # Get this from webhook setup
# Optional Settings
STRIPE_API_VERSION=2024-12-18 # Latest API version
STRIPE_MAX_NETWORK_RETRIES=2
STRIPE_TIMEOUT=30000 # 30 seconds
# Currency (default: usd)
STRIPE_CURRENCY=usd
# Sync Settings
STRIPE_SYNC_INTERVAL=3600 # Sync every hour (in seconds)
STRIPE_AUTO_SYNC=truenself build
nself restart# Check plugin status
nself plugin status stripe
# Test API connection
curl -u sk_test_PLACEHOLDER: https://api.stripe.com/v1/customers?limit=1Webhooks notify your application of Stripe events in real-time:
- Payment succeeded/failed
- Subscription created/updated/cancelled
- Invoice paid/payment failed
- Customer updated
For Development (local testing):
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
# Login
stripe login
# Forward webhooks to local
stripe listen --forward-to http://localhost/webhooks/stripeOutput:
> Ready! Your webhook signing secret is whsec_xxxxx
Copy the signing secret to .env:
STRIPE_WEBHOOK_SECRET=whsec_xxxxxWhen deploying to production:
- Go to Dashboard > Developers > Webhooks
- Click "Add endpoint"
- Enter endpoint URL:
https://yourdomain.com/webhooks/stripe - Select API version: Latest
- Select events to listen to:
Recommended events:
โ customer.created
โ customer.updated
โ customer.deleted
โ customer.subscription.created
โ customer.subscription.updated
โ customer.subscription.deleted
โ customer.subscription.trial_will_end
โ invoice.created
โ invoice.finalized
โ invoice.paid
โ invoice.payment_failed
โ payment_intent.created
โ payment_intent.succeeded
โ payment_intent.payment_failed
โ payment_method.attached
โ payment_method.detached
โ charge.succeeded
โ charge.failed
โ charge.refunded
- Click "Add endpoint"
- Copy Signing secret to
.env.prod
# Trigger test event
stripe trigger payment_intent.succeeded
# Check webhook events
nself plugin stripe webhook list
# View event details
nself plugin stripe webhook show evt_xxxxxOption A: Via Stripe Dashboard
- Go to Dashboard > Products
- Click "Add product"
- Enter product details:
- Name: "Pro Plan"
- Description: "Professional features"
- Pricing: $29.99/month
- Billing period: Monthly
- Currency: USD
- Click "Save product"
- Repeat for other tiers (Basic, Enterprise)
Option B: Via nself CLI
# Create Pro Plan
nself plugin stripe products create \
--name "Pro Plan" \
--description "Professional features" \
--price 29.99 \
--interval month \
--currency usd
# Create Basic Plan
nself plugin stripe products create \
--name "Basic Plan" \
--price 9.99 \
--interval month
# Create Enterprise Plan
nself plugin stripe products create \
--name "Enterprise Plan" \
--price 99.99 \
--interval monthnself plugin stripe sync --productsVerify:
SELECT * FROM stripe_products;
SELECT * FROM stripe_prices;Frontend: Collect payment method
// client/src/checkout.js
import { loadStripe } from '@stripe/stripe-js';
const stripe = await loadStripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY);
// Create payment method
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
billing_details: {
name: customerName,
email: customerEmail
}
});
if (error) {
console.error(error);
} else {
// Send to backend
await createSubscription(paymentMethod.id, priceId);
}Backend: Create subscription
// api/subscriptions/create.js
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_API_KEY);
export async function createSubscription(customerId, priceId, paymentMethodId) {
// Attach payment method to customer
await stripe.paymentMethods.attach(paymentMethodId, {
customer: customerId
});
// Set as default payment method
await stripe.customers.update(customerId, {
invoice_settings: {
default_payment_method: paymentMethodId
}
});
// Create subscription
const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
payment_behavior: 'default_incomplete',
payment_settings: {
save_default_payment_method: 'on_subscription'
},
expand: ['latest_invoice.payment_intent']
});
// Save to database
await saveSubscriptionToDB(subscription);
return subscription;
}GraphQL Mutation:
mutation CreateSubscription {
insert_subscriptions_one(object: {
user_id: "user-id"
stripe_subscription_id: "sub_xxxxx"
stripe_customer_id: "cus_xxxxx"
status: "active"
plan: "pro"
current_period_start: "2026-01-01"
current_period_end: "2026-02-01"
price_id: "price_xxxxx"
price_amount: 2999 # $29.99
currency: "usd"
}) {
id
status
}
}Upgrade/Downgrade:
export async function changeSubscription(subscriptionId, newPriceId) {
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
// Update subscription
const updatedSubscription = await stripe.subscriptions.update(subscriptionId, {
items: [{
id: subscription.items.data[0].id,
price: newPriceId
}],
proration_behavior: 'create_prorations' // Prorate the difference
});
// Update database
await updateSubscriptionInDB(updatedSubscription);
return updatedSubscription;
}Cancel Subscription:
export async function cancelSubscription(subscriptionId, immediate = false) {
const options = immediate ? {} : { at_period_end: true };
const subscription = await stripe.subscriptions.cancel(subscriptionId, options);
// Update database
await updateSubscriptionInDB(subscription);
return subscription;
}$0.01 per API call, billed monthly
Via Stripe Dashboard:
- Create product: "API Calls"
- Pricing model: Usage-based
- Unit price: $0.01
- Billing period: Monthly
- Usage aggregation: Sum
Via CLI:
nself plugin stripe products create \
--name "API Calls" \
--type usage \
--price 0.01 \
--interval month \
--usage-type metered \
--aggregate sumAfter each API call:
// api/middleware/track-usage.js
export async function trackAPIUsage(userId, subscriptionId) {
// Get subscription item ID
const subscription = await getSubscription(subscriptionId);
const usageItemId = subscription.items.data.find(
item => item.price.product.name === 'API Calls'
).id;
// Report usage to Stripe
await stripe.subscriptionItems.createUsageRecord(usageItemId, {
quantity: 1, // 1 API call
timestamp: Math.floor(Date.now() / 1000),
action: 'increment'
});
// Also log in database
await logUsageInDB(userId, subscriptionId, 'api_calls', 1);
}Batch reporting (more efficient):
// Cron job: Every hour
export async function reportBatchUsage() {
// Get usage counts from database
const usageCounts = await getHourlyUsageCounts();
for (const { subscriptionItemId, quantity } of usageCounts) {
await stripe.subscriptionItems.createUsageRecord(subscriptionItemId, {
quantity: quantity,
timestamp: Math.floor(Date.now() / 1000),
action: 'increment'
});
}
}export async function getUsageSummary(subscriptionItemId) {
const usageRecords = await stripe.subscriptionItems.listUsageRecordSummaries(
subscriptionItemId,
{
limit: 10
}
);
return usageRecords.data;
}// api/payments/create-intent.js
export async function createPaymentIntent(amount, currency, customerId) {
const paymentIntent = await stripe.paymentIntents.create({
amount: Math.round(amount * 100), // cents
currency: currency,
customer: customerId,
payment_method_types: ['card'],
metadata: {
product_id: 'product-id',
user_id: 'user-id'
}
});
return paymentIntent;
}// client/src/PaymentForm.jsx
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
export function PaymentForm({ amount, onSuccess }) {
const stripe = useStripe();
const elements = useElements();
const handleSubmit = async (event) => {
event.preventDefault();
if (!stripe || !elements) return;
// Create payment intent on backend
const { clientSecret } = await fetch('/api/payments/create-intent', {
method: 'POST',
body: JSON.stringify({ amount: amount })
}).then(r => r.json());
// Confirm payment
const { error, paymentIntent } = await stripe.confirmCardPayment(
clientSecret,
{
payment_method: {
card: elements.getElement(CardElement)
}
}
);
if (error) {
console.error(error);
} else if (paymentIntent.status === 'succeeded') {
onSuccess(paymentIntent);
}
};
return (
<form onSubmit={handleSubmit}>
<CardElement />
<button type="submit" disabled={!stripe}>
Pay ${amount}
</button>
</form>
);
}// api/webhooks/stripe.js
export async function handlePaymentIntentSucceeded(paymentIntent) {
// Save payment to database
await savePaymentToDB({
stripe_payment_intent_id: paymentIntent.id,
user_id: paymentIntent.metadata.user_id,
amount: paymentIntent.amount / 100,
currency: paymentIntent.currency,
status: 'succeeded'
});
// Fulfill order
await fulfillOrder(paymentIntent.metadata.product_id);
// Send receipt email
await sendReceiptEmail(paymentIntent.customer);
}- Go to Dashboard > Connect > Settings
- Click "Get started"
- Choose Account type: Express (recommended) or Standard
- Copy Client ID (starts with
ca_)
Edit .env:
STRIPE_CONNECT_ENABLED=true
STRIPE_CONNECT_CLIENT_ID=ca_your_client_id_here
PLATFORM_FEE_PERCENTAGE=15.0// api/vendors/create-connect-account.js
export async function createConnectAccount(vendorEmail) {
// Create Connect account
const account = await stripe.accounts.create({
type: 'express',
email: vendorEmail,
capabilities: {
card_payments: { requested: true },
transfers: { requested: true }
}
});
// Create onboarding link
const accountLink = await stripe.accountLinks.create({
account: account.id,
refresh_url: 'https://yoursite.com/vendor/connect/refresh',
return_url: 'https://yoursite.com/vendor/connect/complete',
type: 'account_onboarding'
});
return {
accountId: account.id,
onboardingUrl: accountLink.url
};
}// api/orders/create-with-split.js
export async function createOrderWithSplit(vendorAccountId, amount, platformFee) {
const vendorAmount = amount - platformFee;
// Create payment with destination
const paymentIntent = await stripe.paymentIntents.create({
amount: Math.round(amount * 100),
currency: 'usd',
application_fee_amount: Math.round(platformFee * 100), // Platform fee
transfer_data: {
destination: vendorAccountId // Vendor receives vendorAmount
},
metadata: {
vendor_account_id: vendorAccountId,
platform_fee: platformFee
}
});
return paymentIntent;
}Automatic (Stripe handles):
- Vendors receive payouts based on their payout schedule
- Default: 2-day rolling basis
Manual (you control):
export async function payoutToVendor(vendorAccountId, amount) {
const payout = await stripe.payouts.create(
{
amount: Math.round(amount * 100),
currency: 'usd'
},
{
stripeAccount: vendorAccountId // Vendor's Connect account
}
);
return payout;
}Test Mode:
- Use
sk_test_PLACEHOLDERandpk_test_keys - No real charges
- Test card numbers work
Live Mode:
- Use
sk_live_andpk_live_keys - Real charges
- Real credit cards only
Successful payments:
4242 4242 4242 4242 # Visa
5555 5555 5555 4444 # Mastercard
3782 822463 10005 # American Express
Failed payments:
4000 0000 0000 0002 # Card declined
4000 0000 0000 9995 # Insufficient funds
4000 0000 0000 0069 # Charge expired
3D Secure:
4000 0027 6000 3184 # 3DS required, succeeds
4000 0082 6000 3178 # 3DS required, fails
Expiry: Any future date (e.g., 12/34) CVC: Any 3 digits (e.g., 123)
# Start Stripe CLI listener
stripe listen --forward-to http://localhost/webhooks/stripe
# In another terminal, trigger events
stripe trigger payment_intent.succeeded
stripe trigger customer.subscription.created
stripe trigger invoice.payment_failed# Check Stripe data sync
nself plugin stripe sync
# View customers
nself plugin stripe customers list
# View subscriptions
nself plugin stripe subscriptions list
# View invoices
nself plugin stripe invoices list --status paid
# Check webhooks
nself plugin stripe webhook stats- Business verification complete in Stripe
- Live API keys obtained
- Webhook endpoint configured with live keys
- SSL certificate active on domain
- Test all payment flows with live keys in test environment
- Error handling implemented
- Email receipts configured
- Refund policy documented
- Terms of service updated
- Privacy policy includes payment processing
-
Complete business verification
-
Get live API keys
- Toggle to Live mode
- Copy keys
-
Update production environment
Edit .env.prod:
# REMOVE test keys
# STRIPE_API_KEY=sk_test_PLACEHOLDER
# STRIPE_PUBLISHABLE_KEY=pk_test_xxxxx
# ADD live keys
STRIPE_API_KEY=sk_live_your_live_key_here
STRIPE_PUBLISHABLE_KEY=pk_live_your_live_key_here-
Configure live webhook
- Create endpoint in Live mode
- Copy signing secret to
.env.prod
-
Deploy
nself deploy prod-
Test with real card
- Use your own card
- Make small test purchase
- Immediately refund
Stripe Dashboard:
- Monitor payments
- Track failed charges
- View customer activity
nself Monitoring:
# View webhook events
nself plugin stripe webhook list --live
# Check failed payments
nself plugin stripe payments list --status failed
# View subscription churn
nself plugin stripe subscriptions statsCause: Customer ID not found in Stripe
Fix:
# Verify customer exists
curl -u sk_test_PLACEHOLDER: https://api.stripe.com/v1/customers/cus_xxxxx
# Create customer if missing
nself plugin stripe customers create --email [email protected]Cause: No payment method saved
Fix:
// Attach payment method
await stripe.paymentMethods.attach(paymentMethodId, {
customer: customerId
});
// Set as default
await stripe.customers.update(customerId, {
invoice_settings: {
default_payment_method: paymentMethodId
}
});Cause: Incorrect webhook secret
Fix:
- Get correct secret from Stripe Dashboard
- Update
.env:STRIPE_WEBHOOK_SECRET=whsec_correct_secret_here
- Restart:
nself restart
Cause: Too many API requests
Fix:
// Implement retry logic
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_API_KEY, {
maxNetworkRetries: 2,
timeout: 30000
});
// Use batch operations
// Instead of creating 100 customers one by one,
// create them in batches with delaysCause: 3D Secure authentication pending
Fix: Implement 3DS2 handling:
const { error, paymentIntent } = await stripe.confirmCardPayment(
clientSecret,
{
payment_method: paymentMethodId,
return_url: 'https://yoursite.com/payment/complete' // Required for 3DS
}
);
if (paymentIntent.status === 'requires_action') {
// Handle 3DS authentication
const { error: confirmError } = await stripe.handleCardAction(clientSecret);
}Enable verbose logging:
# In .env
STRIPE_DEBUG=true
LOG_LEVEL=debug
nself restart
nself logs | grep stripe-
Never expose secret keys
- Keep in
.env,.env.secrets - Don't commit to git
- Use environment variables
- Keep in
-
Verify webhook signatures
const event = stripe.webhooks.constructEvent( req.body, signature, webhookSecret );
-
Use HTTPS only
- All Stripe communication must be over HTTPS
- Configure SSL:
nself ssl enable
-
Batch API calls
// Bad: 100 individual calls for (const customer of customers) { await stripe.customers.create(customer); } // Good: Batch with delays const batches = chunk(customers, 10); for (const batch of batches) { await Promise.all(batch.map(c => stripe.customers.create(c))); await sleep(1000); }
-
Cache product/price data
- Sync to database
- Query database instead of Stripe API
- Refresh periodically
-
Use webhooks, not polling
- Don't poll for payment status
- Listen to webhooks for updates
-
Handle all payment states
-
requires_payment_method- Payment failed, retry -
requires_action- 3DS authentication needed -
processing- Show pending state -
succeeded- Payment complete
-
-
Show clear error messages
const errorMessages = { 'card_declined': 'Your card was declined. Please try another card.', 'insufficient_funds': 'Insufficient funds. Please try another payment method.', 'expired_card': 'Your card has expired. Please try another card.' };
-
Send receipts
- Stripe can email receipts automatically
- Or send custom receipts via your email system
- Stripe API Documentation - Complete API reference
- Stripe Testing - Test card numbers
- Webhooks Guide - Webhook implementation
- Stripe Connect - Marketplace payments
- nself Plugin Docs - nself Stripe plugin
- Stripe Support: [email protected]
- nself Discord: https://discord.gg/nself
- GitHub Issues: https://github.com/nself-org/cli/issues
Your Stripe integration is complete! Start accepting payments.