Payments
Dual payment provider integration with Stripe and PayPal — one-time payments, subscriptions, webhook auto-sync, guest checkout, and automated email notifications.
Stripe
Setup
- Create an account at stripe.com
- Copy your API keys from the Stripe Dashboard
- Create a webhook endpoint pointing to
https://yourdomain.com/api/stripe/webhook - Enable the events listed in the webhook section below
Configuration
# .env
STRIPE_SECRET_KEY="sk_test_xxx"
STRIPE_PUBLISHABLE_KEY="pk_test_xxx"
STRIPE_WEBHOOK_SECRET="whsec_xxx"Use test keys during development. Switch to live keys before going to production.
One-time Payment
import { createStripeCheckoutSession } from '@/lib/payment/stripe'
const session = await createStripeCheckoutSession({
productId: product.id,
userId: user?.id, // optional — guest checkout supported
successUrl: 'https://yourdomain.com/success?session_id={CHECKOUT_SESSION_ID}',
cancelUrl: 'https://yourdomain.com/cancel',
})
// Redirect the browser
window.location.href = session.urlSubscription
import { createStripeSubscription } from '@/lib/payment/stripe'
const subscription = await createStripeSubscription({
customerId: stripeCustomerId,
priceId: 'price_xxx',
metadata: { userId: user.id },
})Webhook Events
TikShip handles these Stripe events automatically:
| Event | Action |
|---|---|
checkout.session.completed | Mark order completed, trigger confirmation email |
payment_intent.payment_failed | Mark order failed, trigger failure email |
invoice.payment_succeeded | Renew subscription order |
customer.subscription.deleted | Cancel subscription |
// src/app/api/stripe/webhook/route.ts (auto-handled)
export async function POST(req: NextRequest) {
const event = await verifyStripeWebhook(req)
switch (event.type) {
case 'checkout.session.completed':
await handleStripeCheckoutComplete(event.data.object)
break
case 'payment_intent.payment_failed':
await handleStripePaymentFailed(event.data.object)
break
}
return new Response('OK')
}Test Cards
| Scenario | Card number |
|---|---|
| Success | 4242 4242 4242 4242 |
| Decline | 4000 0000 0000 0002 |
| Insufficient funds | 4000 0000 0000 9995 |
| 3D Secure | 4000 0025 0000 3155 |
Use any future expiry date, any 3-digit CVC, and any 5-digit ZIP.
PayPal
Setup
- Create an account at developer.paypal.com
- Create a new app to get Client ID and Secret
- Create a webhook and point it to
https://yourdomain.com/api/paypal/webhook
Configuration
# .env
PAYPAL_MODE="sandbox" # sandbox | live
PAYPAL_CLIENT_ID="your-client-id"
PAYPAL_CLIENT_SECRET="your-client-secret"
PAYPAL_WEBHOOK_ID="your-webhook-id"One-time Payment
import { createPayPalOrder } from '@/lib/payment/paypal'
const order = await createPayPalOrder({
productId: product.id,
userId: user?.id,
returnUrl: 'https://yourdomain.com/success',
cancelUrl: 'https://yourdomain.com/cancel',
})
window.location.href = order.approvalUrlSubscription Plans
import { createPayPalSubscription } from '@/lib/payment/paypal'
const subscription = await createPayPalSubscription({
planId: 'P-xxx', // created in PayPal dashboard
returnUrl: 'https://yourdomain.com/success',
cancelUrl: 'https://yourdomain.com/cancel',
})Webhook Events
| Event | Action |
|---|---|
PAYMENT.CAPTURE.COMPLETED | Mark order completed, trigger confirmation email |
PAYMENT.CAPTURE.DENIED | Mark order failed, trigger failure email |
BILLING.SUBSCRIPTION.CREATED | Activate subscription order |
BILLING.SUBSCRIPTION.CANCELLED | Cancel subscription |
Sandbox Accounts
Use auto-generated sandbox accounts from the PayPal Developer Dashboard:
- Buyer: personal sandbox account
- Seller: business sandbox account
Flexible Product Pricing
Each product can enable any combination of payment methods:
Admin → Products → Edit → Payment Methods
✅ Stripe (one-time)
✅ Stripe (subscription — monthly)
✅ PayPal (one-time)
✅ PayPal (subscription — monthly)At checkout the customer sees only the payment options configured for that product.
Order Lifecycle
All orders share a single status model:
pending → completed
→ failed
→ refunded
→ expired| Status | Meaning |
|---|---|
pending | Payment initiated, awaiting webhook confirmation |
completed | Payment confirmed by provider webhook |
failed | Payment declined or errored |
refunded | Refund issued via provider dashboard or API |
expired | Pending order not completed within the expiry window |
Query Orders
import { prisma } from '@/lib/db/prisma'
// User's order history
const orders = await prisma.order.findMany({
where: { userId: user.id },
select: {
id: true, status: true, amount: true, currency: true,
paymentProvider: true, createdAt: true,
product: { select: { name: true } },
},
orderBy: { createdAt: 'desc' },
})Guest Checkout
Orders can be placed without a registered account. When userId is omitted the order is stored with the guest's email address only.
const session = await createStripeCheckoutSession({
productId: product.id,
guestEmail: 'guest@example.com',
successUrl: '...',
cancelUrl: '...',
})Email Notifications
Payment events automatically trigger transactional emails (no configuration required):
| Event | Email sent |
|---|---|
Order completed | Payment confirmation with order details |
Order failed | Payment failure notice with retry link |
Order expired | Expiry notice with checkout link |
See Email to customize the templates.
Best Practices
- Never expose secret keys — keep them in
.env, never commit them - Always verify webhook signatures —
verifyStripeWebhookandverifyPayPalWebhookdo this for you - Validate amounts server-side — never trust the amount coming from the client
- Use idempotency keys — for retry-safe Stripe API calls
- Test all scenarios — success, decline, 3DS, subscription renewal, cancellation
Next Steps
- Email — customize payment notification templates
- Admin Panel — manage orders and providers from the UI
- Database — Order and Product schema reference
Detailed Integration Guides
- Stripe & PayPal Setup Guide — step-by-step walkthrough: API keys, webhook configuration, Stripe CLI local testing, PayPal sandbox, subscription plans, and production checklist
Authentication & Authorization
Complete authentication system with multiple login methods, enterprise-grade RBAC, brute-force protection, and audit logs.
Content Management
Full CMS for articles and standalone pages with TipTap rich-text editor, draft/publish/schedule workflow, categories, tags, SEO fields, and in-article product embeds.