| [ Web Proxy ] |
| Viewing: https://marrowstack.dev/docs/blocks/billing | [Back] [Original] |
Monetization Advanced
PayPal REST API integration for one-time orders, monthly and yearly subscriptions, refunds, webhook verification, usage tracking, and invoice generation wired to Supabase.
Read the Getting Access guideif you haven't yet.
Sign in at marrowstack.dev, open the Billing & Subscriptions block, and click Copy all files. Paste lib/billing.ts into your project.
PAYPAL_MODE=sandbox during development. Switch to live and update your webhook URL before going to production. Never use live credentials in development.developer.paypal.comlib/billing.ts from the block detail page into your project.npm install @supabase/supabase-js zod| Variable | Required | Default | Purpose |
|---|---|---|---|
| PAYPAL_CLIENT_ID | yes | PayPal REST API client ID | |
| PAYPAL_CLIENT_SECRET | yes | PayPal REST API client secret server-side only | |
| PAYPAL_WEBHOOK_ID | yes | Webhook ID from PayPal Developer Dashboard Webhooks | |
| PAYPAL_MODE | no | sandbox or live. Defaults to sandbox | |
| NEXT_PUBLIC_SUPABASE_URL | yes | Your Supabase project URL | |
| SUPABASE_SERVICE_ROLE_KEY | yes | Service role key server-side only |
Run the MIGRATION constant in Supabase SQL Editor. Creates:
orders one-time PayPal orders with status trackingsubscriptions PayPal subscription IDs, plan, billing cycle, statusinvoices generated invoice records linked to orders/subscriptionsimport { createOrder, captureOrder } from '@/lib/billing'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { NextRequest, NextResponse } from 'next/server'
// POST create a PayPal order
export async function POST(req: NextRequest) {
const session = await getServerSession(authOptions)
if (!session?.user?.id) return new NextResponse('Unauthorized', { status: 401 })
const { amountUsd, description } = await req.json()
const order = await createOrder({ userId: session.user.id, amountUsd, description })
// Return the PayPal order ID to the client use it to launch the PayPal button
return NextResponse.json({ orderId: order.id })
}
// PATCH capture after buyer approves
export async function PATCH(req: NextRequest) {
const session = await getServerSession(authOptions)
if (!session?.user?.id) return new NextResponse('Unauthorized', { status: 401 })
const { orderId } = await req.json()
const result = await captureOrder(orderId)
// Deliver value only after capture succeeds
return NextResponse.json(result)
}import { verifyWebhook, handleWebhookEvent } from '@/lib/billing'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(req: NextRequest) {
const body = await req.text()
const headers = Object.fromEntries(req.headers.entries())
const isValid = await verifyWebhook(body, headers)
if (!isValid) return new NextResponse('Invalid signature', { status: 400 })
const event = JSON.parse(body)
await handleWebhookEvent(event)
return NextResponse.json({ ok: true })
}orders table with status created.captured.PAYMENT.CAPTURE.COMPLETED event confirm verifyWebhook() passes.subscriptions.INVALID_CLIENT client authentication failed
Cause: PAYPAL_CLIENT_ID or PAYPAL_CLIENT_SECRET is wrong, or you're using live credentials in sandbox mode.
Fix: Check credentials in PayPal Developer Dashboard. Ensure PAYPAL_MODE matches the app type (sandbox/live).
Webhook signature verification failed
Cause: PAYPAL_WEBHOOK_ID is wrong, or the webhook body was modified in transit.
Fix: Copy the exact Webhook ID from PayPal Developer Dashboard Webhooks. Don't modify the raw body before passing to verifyWebhook().
ORDER_NOT_APPROVED
Cause: Attempting to capture an order before the buyer has approved it in the PayPal flow.
Fix: Only call captureOrder() after the PayPal JS SDK fires onApprove.
relation 'orders' does not exist
Cause: The migration hasn't been run.
Fix: Run the MIGRATION constant in Supabase SQL Editor.
captureOrder() succeeds server-side.PAYPAL_CLIENT_SECRET server-side only. It allows full API access to your PayPal account.orders table before delivering to prevent double-credit on webhook retries.| Web Proxy Viewer | New URL | Original Page |