To accept crypto payments in a V0 by Vercel app, create a Next.js API route that calls the Coinbase Commerce API to generate checkout charges. V0 generates the payment UI; you add your Coinbase Commerce API key to Vercel environment variables and create a webhook handler to confirm payments. The result is a working crypto checkout supporting Bitcoin, Ethereum, and other major cryptocurrencies.
Accept Cryptocurrency Payments in Your V0 App with Coinbase Commerce
Coinbase Commerce enables businesses to accept crypto payments — Bitcoin, Ethereum, Litecoin, Bitcoin Cash, Dogecoin, USDC, DAI, and more — without requiring customers to have a Coinbase account. The platform handles crypto wallet addresses, exchange rate calculations, and payment confirmation, abstracting away the complexity of blockchain transactions. For V0 developers, Coinbase Commerce is the most straightforward way to add a crypto payment option to a Next.js app.
The integration flow works through charges: your API route creates a charge object via the Coinbase Commerce API with a name, description, and amount (in fiat currency like USD). Coinbase Commerce returns a hosted_url that redirects the customer to a Coinbase-hosted payment page. The customer selects their preferred cryptocurrency, sends the exact amount to the provided wallet address, and Coinbase Commerce monitors the blockchain for confirmation. Once confirmed, Coinbase sends a webhook to your app.
The key difference from Stripe is that crypto payments are not instant — blockchain confirmations can take minutes to hours depending on network congestion and the cryptocurrency. Design your checkout flow to handle this asynchronous nature: redirect customers to a confirmation-pending page and update the order status when your webhook receives the charge:confirmed event. Coinbase Commerce does not charge transaction fees on payments — you keep 100% of crypto received.
Integration method
Coinbase Commerce connects to V0-generated Next.js apps through server-side API routes that create payment charges and handle webhooks. The Coinbase Commerce API key stays in Vercel environment variables. Your frontend redirects customers to a Coinbase-hosted checkout page or embeds a checkout widget, and your webhook handler confirms payment upon completion.
Prerequisites
- A V0 account at v0.dev with a Next.js project created
- A Coinbase Commerce account at commerce.coinbase.com (free to create)
- A Coinbase Commerce API key from Settings → API Keys in the Commerce dashboard
- A Vercel account connected to your V0 project for deployment and webhook endpoint
- Basic understanding that crypto payments are asynchronous — confirmation takes minutes to hours
Step-by-step guide
Generate the Payment UI with V0
Generate the Payment UI with V0
Open your V0 project and prompt V0 to generate the payment interface. For a Coinbase Commerce integration, your UI needs a product display section, a price display in USD (Coinbase Commerce converts to crypto automatically), and a payment button that triggers the charge creation flow. You can also include a list of accepted cryptocurrencies with their logos to signal to customers that crypto is accepted. Ask V0 to generate a component that calls /api/coinbase/create-charge on button click and handles the redirect to the returned hosted_url. The component should also handle loading states (while the API creates the charge) and error states (if charge creation fails). Consider adding a 'Payment Status' route that customers land on after completing the Coinbase checkout. V0 will generate functional React code using placeholder data. The buttons will not work until the API route is created, but you can verify the layout and styling match your requirements. Request shadcn/ui components for a polished look that integrates with V0's default design system.
Create a product checkout page with a product image placeholder, name, description, and price in USD. Add an 'Accept' / 'Pay with Crypto' button row with crypto currency icons (Bitcoin, Ethereum, USDC). On 'Pay with Crypto' click, call POST /api/coinbase/create-charge with the product details. Show a loading spinner during the API call. Redirect to the returned checkout_url. Include a '/payment-pending' page that shows 'Awaiting blockchain confirmation' with a transaction ID.
Paste this in V0 chat
Pro tip: Add a clear disclaimer on your checkout page that crypto payments may take 10-60 minutes to confirm depending on network conditions, so customers know what to expect before paying.
Expected result: V0 generates a checkout page with crypto payment button and a payment-pending confirmation page. The button calls a non-existent API route, so it will error until step 2 is complete.
Create the Coinbase Commerce API Route
Create the Coinbase Commerce API Route
Create a new file at app/api/coinbase/route.ts. This route handles POST requests from your frontend to create a new Coinbase Commerce charge. A charge is a payment request with a specific amount, currency, and metadata. Coinbase Commerce returns a hosted checkout URL that you redirect the customer to. The Coinbase Commerce REST API is straightforward. To create a charge, send a POST request to https://api.commerce.coinbase.com/charges with your API key in the X-CC-Api-Key header and the charge details in the JSON body. The required fields are name (product name), description, pricing_type (fixed_price or no_price), and local_price (amount and currency for fixed_price charges). You can also include metadata (order ID, user ID) that Coinbase will include in webhook events for matching payments to orders. The response includes a data.hosted_url (the Coinbase-hosted checkout page) and a data.id (the charge ID for webhook matching). Return both to the frontend so it can redirect and track the payment. Always store the charge ID in your database or session before redirecting, since you will need it to match incoming webhook events to orders.
1import { NextRequest, NextResponse } from 'next/server';23interface CreateChargeRequest {4 name: string;5 description: string;6 amount: string;7 currency: string;8 metadata?: Record<string, string>;9}1011export async function POST(request: NextRequest) {12 const body: CreateChargeRequest = await request.json();1314 const apiKey = process.env.COINBASE_COMMERCE_API_KEY;15 if (!apiKey) {16 return NextResponse.json(17 { error: 'Coinbase Commerce API key not configured' },18 { status: 500 }19 );20 }2122 try {23 const response = await fetch('https://api.commerce.coinbase.com/charges', {24 method: 'POST',25 headers: {26 'X-CC-Api-Key': apiKey,27 'X-CC-Version': '2018-03-22',28 'Content-Type': 'application/json',29 },30 body: JSON.stringify({31 name: body.name,32 description: body.description,33 pricing_type: 'fixed_price',34 local_price: {35 amount: body.amount,36 currency: body.currency || 'USD',37 },38 metadata: body.metadata || {},39 }),40 });4142 if (!response.ok) {43 const error = await response.json();44 return NextResponse.json(45 { error: error.error?.message || 'Failed to create charge' },46 { status: response.status }47 );48 }4950 const data = await response.json();51 return NextResponse.json({52 chargeId: data.data.id,53 checkoutUrl: data.data.hosted_url,54 expiresAt: data.data.expires_at,55 });56 } catch (error) {57 console.error('Coinbase Commerce error:', error);58 return NextResponse.json(59 { error: 'Failed to create payment charge' },60 { status: 500 }61 );62 }63}Pro tip: Include an orderId in the metadata field when creating charges. Coinbase Commerce includes metadata in webhook payloads, making it easy to link a payment confirmation back to a specific order in your database.
Expected result: POSTing to /api/coinbase/create-charge with a product name and price returns a chargeId and checkoutUrl pointing to a Coinbase-hosted payment page.
Add Coinbase Commerce API Key to Vercel
Add Coinbase Commerce API Key to Vercel
Your Coinbase Commerce API key must be stored as a Vercel environment variable. Go to your Vercel Dashboard, select your project, click Settings → Environment Variables, and add a new variable named COINBASE_COMMERCE_API_KEY. Paste your API key as the value. Select All Environments unless you want separate test and production keys. To find or create your API key: log in to commerce.coinbase.com, go to Settings → API Keys, and either copy your existing key or click Create an API Key. The API key is a long alphanumeric string. Keep this key private — anyone with it can create charges on your behalf and access your Commerce account data. For webhook signature verification, also add COINBASE_COMMERCE_WEBHOOK_SECRET. You will get this secret when you configure the webhook endpoint in the Coinbase Commerce dashboard (Settings → Webhook Subscriptions → Add an endpoint). The webhook secret is used to verify that incoming webhook requests actually originate from Coinbase Commerce. After adding both variables, trigger a new deployment in Vercel by clicking Redeploy on the latest deployment, or push a commit from V0.
Pro tip: Coinbase Commerce has separate API keys for sandbox testing. If you want to test without real crypto, check their developer documentation for the sandbox environment setup.
Expected result: Both COINBASE_COMMERCE_API_KEY and COINBASE_COMMERCE_WEBHOOK_SECRET are set in Vercel. The create-charge endpoint works end-to-end after redeployment.
Create the Webhook Handler for Payment Confirmation
Create the Webhook Handler for Payment Confirmation
Crypto payments need an asynchronous confirmation flow. When a customer completes a Coinbase Commerce checkout, Coinbase monitors the blockchain for the transaction. Once confirmed, Coinbase POSTs a webhook event to your registered endpoint. You must handle this webhook to update order status, grant access to purchased content, or trigger any post-payment business logic. Create a webhook handler at app/api/coinbase/webhook/route.ts. The critical requirement for Coinbase Commerce webhooks is that you use the raw request body (request.text() not request.json()) for signature verification. Coinbase signs the webhook payload using HMAC-SHA256 with your webhook secret, and the signature is sent in the X-CC-Webhook-Signature header. Verifying this signature is essential to prevent fraud — without it, anyone could POST fake payment confirmations to your endpoint. The key webhook events to handle are: charge:confirmed (payment confirmed on blockchain — fulfill the order), charge:failed (payment failed or expired after 60 minutes — notify customer), and charge:pending (payment detected on blockchain but not yet confirmed — can show a pending status). Always return a 200 response immediately after receiving and queuing the event for processing.
Create an order status page that takes an orderId prop and displays the current payment status. Show 'Pending' when waiting for confirmation, 'Confirmed' when payment succeeds (with a green checkmark), and 'Failed' when payment expires. Poll /api/coinbase/order-status every 15 seconds until confirmed or failed.
Paste this in V0 chat
1import { NextRequest, NextResponse } from 'next/server';2import crypto from 'crypto';34export async function POST(request: NextRequest) {5 const body = await request.text();6 const signature = request.headers.get('x-cc-webhook-signature');7 const webhookSecret = process.env.COINBASE_COMMERCE_WEBHOOK_SECRET;89 if (!signature || !webhookSecret) {10 return NextResponse.json({ error: 'Missing signature' }, { status: 400 });11 }1213 // Verify webhook signature14 const expectedSig = crypto15 .createHmac('sha256', webhookSecret)16 .update(body)17 .digest('hex');1819 if (expectedSig !== signature) {20 return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });21 }2223 const event = JSON.parse(body);24 const charge = event.event.data;25 const chargeId = charge.id;26 const orderId = charge.metadata?.orderId;2728 switch (event.event.type) {29 case 'charge:confirmed':30 console.log(`Payment confirmed for charge ${chargeId}, order ${orderId}`);31 // Update database: mark order as paid32 // Send confirmation email33 break;34 case 'charge:failed':35 console.log(`Payment failed for charge ${chargeId}`);36 // Update database: mark order as failed37 break;38 case 'charge:pending':39 console.log(`Payment pending for charge ${chargeId}`);40 // Update database: mark as pending confirmation41 break;42 default:43 console.log(`Unhandled event type: ${event.event.type}`);44 }4546 return NextResponse.json({ received: true });47}Pro tip: Register your webhook URL in the Coinbase Commerce Dashboard under Settings → Webhook Subscriptions. Use your production Vercel URL (e.g., https://your-app.vercel.app/api/coinbase/webhook). Preview URLs work for testing but change per deployment.
Expected result: The webhook handler receives Coinbase events, verifies signatures, and logs confirmed payments. charge:confirmed events trigger your order fulfillment logic.
Common use cases
Crypto-Native SaaS Checkout
Add a 'Pay with Crypto' option alongside traditional card payments on your SaaS pricing page. Customers select a plan, click the crypto payment button, and are redirected to a Coinbase Commerce hosted checkout page. After payment confirmation, webhook updates the user's subscription status.
Create a pricing page with three subscription tiers (Basic $9/mo, Pro $29/mo, Business $99/mo). Add a 'Pay with Crypto' button on each tier that calls /api/coinbase/create-charge with the plan details and redirects to the returned checkout URL. Show a 'Payment Pending' confirmation page after redirect. Use Tailwind CSS.
Copy this prompt to try it in V0
Digital Product Store with Crypto Payments
Sell digital downloads, templates, or online courses with a crypto payment option. The customer clicks buy, your API creates a charge for the exact product price, and the customer pays in their preferred cryptocurrency. A webhook unlocks the download after blockchain confirmation.
Build a digital product card component with product name, description, price, and a 'Buy with Crypto' button. On click, call /api/coinbase/create-charge and redirect to the checkout URL. After payment, show an order status page that polls /api/coinbase/order-status to check if payment is confirmed.
Copy this prompt to try it in V0
Donation Page for Web3 Projects
Create a crypto donation page for open-source projects, DAOs, or nonprofits that accept cryptocurrency contributions. Supporters choose a donation amount, confirm in crypto, and the transaction is recorded on-chain for full transparency.
Create a donation page with preset amounts ($10, $25, $50, $100) and a custom amount input. A 'Donate with Crypto' button calls /api/coinbase/create-charge and opens the Coinbase checkout in a new tab. Show a thank-you message with the transaction ID when the user returns. Support Bitcoin, Ethereum, and USDC.
Copy this prompt to try it in V0
Troubleshooting
401 Unauthorized — 'Invalid API key' when creating a charge
Cause: The COINBASE_COMMERCE_API_KEY environment variable is not set in Vercel, or the API key was deleted/regenerated in the Coinbase Commerce Dashboard after being added to Vercel.
Solution: In Vercel Dashboard → Settings → Environment Variables, confirm COINBASE_COMMERCE_API_KEY is set and not empty. In Coinbase Commerce Settings → API Keys, verify the key is still active. If you regenerated the key, update the Vercel variable and redeploy.
Webhook signature verification fails — returning 401 for every webhook
Cause: Either the webhook secret does not match (COINBASE_COMMERCE_WEBHOOK_SECRET is wrong or missing), or the request body is being parsed as JSON before signature verification, which changes the raw bytes and invalidates the HMAC signature.
Solution: Ensure you use request.text() (not request.json()) to get the raw body before verification. Confirm the COINBASE_COMMERCE_WEBHOOK_SECRET in Vercel exactly matches the secret shown in Coinbase Commerce Dashboard → Settings → Webhook Subscriptions for your endpoint.
1// Correct: read raw body for signature verification2const body = await request.text();3// Then parse after verification:4const event = JSON.parse(body);Payment shows as confirmed in Coinbase but order is not updated in the app
Cause: The webhook endpoint URL registered in Coinbase Commerce is incorrect, pointing to a deleted preview deployment URL instead of the production URL, or the endpoint is returning non-200 status codes causing Coinbase to stop retrying.
Solution: Go to Coinbase Commerce Dashboard → Settings → Webhook Subscriptions and verify the URL matches your current production Vercel URL. Check Vercel Function Logs for the webhook route to see if it is receiving requests. Coinbase retries failed webhooks for 24 hours — you can also manually replay events from the Coinbase Commerce dashboard.
Charge expires before customer pays — 'charge:expired' events in logs
Cause: Coinbase Commerce charges expire after 60 minutes if no payment is received. This is a blockchain safety mechanism — after 60 minutes, exchange rate drift could make the charge amount inaccurate.
Solution: Add clear messaging on your payment page showing the 60-minute expiry countdown and instructions to pay promptly. If a charge expires, create a new charge for the same order rather than reusing the old charge ID. Consider adding a 'Create new payment link' button on the payment-pending page.
Best practices
- Always verify Coinbase Commerce webhook signatures using HMAC-SHA256 before processing any payment event — without this check, fraudulent payment confirmations could grant unauthorized access.
- Store the Coinbase charge ID in your database when creating a charge so you can match incoming webhook events to specific orders using the charge ID in the webhook payload.
- Design your checkout flow for asynchronous confirmation — show a clear 'Payment Pending' state and update the order only when the charge:confirmed webhook arrives, not immediately after the customer returns from the Coinbase checkout.
- Use the metadata field when creating charges to embed order IDs and user IDs so webhook handlers can update the correct records without additional lookups.
- Handle the charge:failed and charge:expired events explicitly and notify customers so they can retry the payment rather than being left wondering what happened.
- Never expose your COINBASE_COMMERCE_API_KEY or COINBASE_COMMERCE_WEBHOOK_SECRET in client-side code — both must stay in server-only environment variables (no NEXT_PUBLIC_ prefix).
- Test the full payment flow end-to-end on a staging deployment before going live — create a real charge, complete the payment with a small crypto amount, and verify your webhook handler processes the confirmation correctly.
Alternatives
Stripe is the better choice for traditional card payments — it processes credit/debit cards instantly while Coinbase Commerce specializes in crypto payments with blockchain confirmation delays.
Binance Pay is an alternative crypto payment option if your customers are already Binance users, offering lower fees within the Binance ecosystem.
PayPal is better for instant fiat payouts and broad consumer trust, while Coinbase Commerce is purpose-built for cryptocurrency acceptance.
Frequently asked questions
What cryptocurrencies does Coinbase Commerce accept?
Coinbase Commerce accepts Bitcoin (BTC), Ethereum (ETH), Litecoin (LTC), Bitcoin Cash (BCH), Dogecoin (DOGE), USD Coin (USDC), DAI, and Tether (USDT). The customer selects their preferred currency from the hosted checkout page — you do not need to specify which crypto to accept.
Does Coinbase Commerce charge transaction fees?
Coinbase Commerce does not charge platform fees on transactions. You keep 100% of the cryptocurrency received. However, blockchain network fees (gas fees, miner fees) are paid by the customer when sending crypto. Coinbase also charges fees when you convert crypto to fiat and withdraw to your bank.
How long does crypto payment confirmation take?
Confirmation time varies by cryptocurrency and network congestion. Bitcoin typically takes 10-60 minutes for sufficient confirmations. Ethereum can take 1-5 minutes. USDC on Ethereum has similar timing to ETH. Design your checkout flow with a pending state that updates when the charge:confirmed webhook arrives.
Can I test Coinbase Commerce without spending real crypto?
Coinbase Commerce does not offer a traditional sandbox with test crypto like Stripe's test card numbers. For development testing, you can create real charges for very small amounts (e.g., $0.01 USD) using testnet cryptocurrencies if your account supports it. Check the Coinbase Commerce developer documentation for the current testing approach.
What happens if the crypto price changes between charge creation and payment?
Coinbase Commerce uses a 60-minute price lock. When a charge is created for $100 USD, Coinbase calculates the equivalent amount in the customer's chosen cryptocurrency at that moment and locks that amount for 60 minutes. If the customer pays during this window, they pay the locked amount regardless of price movement. If the charge expires, a new charge must be created with updated prices.
How do I convert received crypto to USD?
Coinbase Commerce deposits received crypto directly to your Coinbase Commerce account. You can then transfer it to a regular Coinbase account and sell it for USD, which can be withdrawn to your bank account. Conversions are subject to Coinbase's standard trading fees. You can also hold the crypto in your account if you prefer not to convert immediately.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation