Integrating Coinbase Commerce with Lovable uses Edge Functions to create crypto payment charges, verify webhook signatures, and track payment status. Store your Coinbase Commerce API key and webhook secret in Cloud Secrets, create an Edge Function to generate payment charges and verify incoming webhooks, and accept Bitcoin, Ethereum, and other crypto payments in your app. Setup takes 35 minutes.
Why integrate Coinbase Commerce with Lovable?
Cryptocurrency payments represent a growing segment of digital commerce, particularly for digital products, international transactions where traditional payment rails are expensive or unavailable, and communities that prefer the censorship-resistance of blockchain payments. Coinbase Commerce is the developer-friendly crypto payment processor: it creates a payment request (charge), the buyer sends crypto to the generated address, and Coinbase detects the payment automatically. No exchange accounts or crypto wallets are required from your customers beyond a basic crypto wallet.
For Lovable developers, Coinbase Commerce is particularly interesting for: digital product stores serving global markets where credit cards are less accessible, creator economy tools where fans can support creators with crypto tips, Web3 applications where users already hold crypto, and markets where payment privacy is valued. Coinbase Commerce accepts Bitcoin, Ethereum, Litecoin, Bitcoin Cash, Dai, and USD Coin — covering the major cryptocurrencies most users already hold.
The technical integration is straightforward: create a charge object (specifying the price in your currency and what the buyer is purchasing), redirect the buyer to Coinbase's hosted checkout page, and handle webhook events when payment is confirmed. Coinbase handles all blockchain complexity — address generation, transaction monitoring, and confirmations. Your Edge Function only needs to create charges and verify webhook signatures.
Integration method
Coinbase Commerce has no native Lovable connector. Integration requires Supabase Edge Functions to create payment charges via the Commerce API, redirect users to Coinbase's hosted checkout page, and verify incoming webhook signatures when payment status changes. The Commerce API key and webhook secret are stored in Cloud Secrets. All API calls run server-side to protect credentials.
Prerequisites
- A Lovable project with Cloud enabled
- A Coinbase Commerce account — create free at commerce.coinbase.com
- Coinbase Commerce API key from Settings → Security in your Commerce dashboard
- Coinbase Commerce webhook secret configured for your endpoint URL
- Basic understanding of how crypto payment addresses work (users send to a generated address)
Step-by-step guide
Set up Coinbase Commerce and get credentials
Set up Coinbase Commerce and get credentials
Go to commerce.coinbase.com and create a Coinbase Commerce account (separate from Coinbase Exchange — you don't need to hold crypto or have an exchange account). After email verification, you can immediately accept payments. In your Commerce dashboard, navigate to Settings → Security. Click 'Create an API key'. Give it a name like 'Lovable App' and copy the API key — it's shown only once. This key is used in the Authorization header as 'Bearer {apikey}' for all Commerce API requests. Still in Settings → Security, scroll down to 'Webhook subscriptions'. Click 'Add an endpoint' and enter your webhook URL: https://your-project.supabase.co/functions/v1/coinbase-webhooks (your Edge Function URL). Select the events you want to receive: charge:created, charge:confirmed, charge:failed, charge:delayed, charge:pending, charge:resolved. Coinbase provides a webhook signing secret for each endpoint — copy this secret for webhook verification. Coinbase Commerce has no sandbox mode — all charges are real and must be paid with real crypto. For testing, create small charges (e.g., $0.01 USD) and pay them with a small crypto amount. Alternatively, use Coinbase Commerce's 'dummy' payment feature in the dashboard that simulates payment without actual crypto transaction for development testing.
Pro tip: Coinbase Commerce's hosted checkout handles all currency conversion automatically — you price in USD (or another fiat currency) and Coinbase shows the equivalent in BTC, ETH, etc. at the current exchange rate. Users pay in their preferred cryptocurrency and you receive the value in your local currency converted at settlement.
Expected result: You have a Coinbase Commerce API key and webhook secret. The webhook endpoint URL is registered in the Commerce dashboard with the relevant events selected.
Store credentials in Cloud Secrets
Store credentials in Cloud Secrets
Open your Lovable project, click '+', select 'Cloud', and expand Secrets. Add COINBASE_COMMERCE_API_KEY with your Commerce API key. Add COINBASE_COMMERCE_WEBHOOK_SECRET with the signing secret for your webhook endpoint. Add COINBASE_COMMERCE_API_BASE with 'https://api.commerce.coinbase.com'. The API key grants full access to your Commerce account including creating charges, listing them, and canceling them. If compromised, someone could create fraudulent charges or access your payment history. Store it exclusively in Cloud Secrets. The webhook secret is used to verify that incoming webhook requests are genuinely from Coinbase Commerce and not spoofed by a malicious actor sending fake payment confirmations. Without signature verification, someone could send a fake 'payment confirmed' webhook and trigger your fulfillment logic without actually paying. Always verify signatures before processing webhook events — Lovable's security model blocks approximately 1,200 API key exposures daily, but application-level signature verification is your responsibility.
Pro tip: Create separate Coinbase Commerce API keys for development and production if you're testing with real small transactions. This keeps your development payment history separate from production. The webhook secret is endpoint-specific — each registered webhook endpoint gets its own secret.
Expected result: COINBASE_COMMERCE_API_KEY, COINBASE_COMMERCE_WEBHOOK_SECRET, and COINBASE_COMMERCE_API_BASE appear in Cloud Secrets.
Create the charge creation Edge Function
Create the charge creation Edge Function
Create the Edge Function that generates Coinbase Commerce payment charges. A charge is the central concept in Commerce: it represents a payment request for a specific amount. Each charge gets a unique ID, a hosted checkout URL, and payment addresses for each supported cryptocurrency. The charge creation request requires: name (what the buyer is purchasing), description (more detail), pricing_type ('fixed_price' for a specific amount or 'no_price' for open-ended), price object (amount and currency for fixed_price), and optional metadata (your order ID, user ID, etc. — sent back in webhooks). Coinbase Commerce supports pricing in major fiat currencies (USD, EUR, GBP, etc.) and stablecoins (USDC). For a fixed price item, set pricing_type='fixed_price' and price={amount: '29.99', currency: 'USD'}. Coinbase converts this to the equivalent crypto amount at the current exchange rate. After creating the charge, your Edge Function returns the charge ID and hosted_url. Store the charge ID in your database immediately so you can match incoming webhook events to the correct order. Redirect the user to hosted_url for the payment experience.
Create a Supabase Edge Function at supabase/functions/coinbase-commerce/index.ts. Support: action='create-charge' with name, description, amount_usd, user_id, order_id — POST to Coinbase Commerce /charges with API key, create a fixed_price charge; store charge_id, order_id, user_id, status='NEW', created_at in a crypto_orders Supabase table; return {charge_id, hosted_url, expires_at}. Add CORS headers.
Paste this in Lovable chat
1// supabase/functions/coinbase-commerce/index.ts2import { serve } from "https://deno.land/std@0.168.0/http/server.ts";3import { createClient } from "https://esm.sh/@supabase/supabase-js@2";45const API_KEY = Deno.env.get("COINBASE_COMMERCE_API_KEY") ?? "";6const API_BASE = Deno.env.get("COINBASE_COMMERCE_API_BASE") ?? "https://api.commerce.coinbase.com";7const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization" };89serve(async (req) => {10 if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });11 const supabase = createClient(Deno.env.get("SUPABASE_URL") ?? "", Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "");1213 try {14 const body = await req.json();15 const { action } = body;1617 if (action === "create-charge") {18 const { name, description, amount_usd, user_id, order_id } = body;19 const res = await fetch(`${API_BASE}/charges`, {20 method: "POST",21 headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json", "X-CC-Version": "2018-03-22" },22 body: JSON.stringify({23 name,24 description,25 pricing_type: "fixed_price",26 local_price: { amount: amount_usd.toString(), currency: "USD" },27 metadata: { user_id, order_id },28 }),29 });30 const data = await res.json();31 if (!data.data?.code) throw new Error(data.error?.message ?? "Charge creation failed");32 const charge = data.data;3334 await supabase.from("crypto_orders").insert({35 charge_id: charge.code,36 order_id,37 user_id,38 amount_usd,39 status: "NEW",40 hosted_url: charge.hosted_url,41 created_at: new Date().toISOString(),42 });4344 return new Response(JSON.stringify({ charge_id: charge.code, hosted_url: charge.hosted_url, expires_at: charge.expires_at }), {45 headers: { ...corsHeaders, "Content-Type": "application/json" },46 });47 }4849 return new Response(JSON.stringify({ error: "Unknown action" }), { status: 400, headers: corsHeaders });50 } catch (err) {51 return new Response(JSON.stringify({ error: err.message }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } });52 }53});Pro tip: Coinbase Commerce charges expire after 1 hour if unpaid. After the expiry, the hosted_url still works but new payment addresses are generated. Store the expiry time in your database and show a 'Payment link expired' message in your UI if the user returns after an hour without completing payment.
Expected result: The coinbase-commerce Edge Function creates payment charges. Calling action='create-charge' returns a hosted_url that opens Coinbase's checkout showing the price in multiple cryptocurrencies.
Create the webhook receiver Edge Function
Create the webhook receiver Edge Function
Create a separate Edge Function that receives and processes Coinbase Commerce webhooks. The webhook receiver must be at the URL you registered with Coinbase, and it must verify the signature on every incoming request before processing payment events. Coinbase Commerce signs webhooks using HMAC-SHA256 with your webhook secret as the key and the raw request body as the message. The signature is included in the X-CC-Webhook-Signature header. Compute the expected signature and compare it to the header value before trusting the event data. Key Coinbase Commerce webhook events: charge:created (charge was created — usually from your app), charge:pending (partial payment received — user sent some but not full amount), charge:confirmed (full payment received and confirmed on blockchain — trigger fulfillment), charge:failed (charge expired without payment), charge:delayed (payment received but delayed blockchain confirmation), charge:resolved (charge resolved — could be paid or cancelled). For most use cases, you need to handle charge:confirmed (update order status to paid, trigger fulfillment) and charge:failed (update order status to expired). Store all webhook events in a Supabase webhook_events table for audit purposes before processing — this lets you replay events if your processing logic has a bug.
Create a Supabase Edge Function at supabase/functions/coinbase-webhooks/index.ts. Verify the X-CC-Webhook-Signature header using HMAC-SHA256 with COINBASE_COMMERCE_WEBHOOK_SECRET. Parse the event body and handle: event type 'charge:confirmed' — update the crypto_orders table status to 'CONFIRMED' where charge_id matches, trigger order fulfillment logic (e.g., set order_fulfilled=true); event type 'charge:failed' — update status to 'FAILED'. Log all events to a webhook_events table regardless of type. Return 200 OK.
Paste this in Lovable chat
1// supabase/functions/coinbase-webhooks/index.ts2import { serve } from "https://deno.land/std@0.168.0/http/server.ts";3import { createClient } from "https://esm.sh/@supabase/supabase-js@2";45const WEBHOOK_SECRET = Deno.env.get("COINBASE_COMMERCE_WEBHOOK_SECRET") ?? "";67serve(async (req) => {8 const supabase = createClient(Deno.env.get("SUPABASE_URL") ?? "", Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "");9 const rawBody = await req.text();10 const signature = req.headers.get("X-CC-Webhook-Signature") ?? "";1112 // Verify signature13 const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(WEBHOOK_SECRET), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);14 const expectedSig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(rawBody));15 const expectedHex = Array.from(new Uint8Array(expectedSig)).map(b => b.toString(16).padStart(2, "0")).join("");1617 if (expectedHex !== signature) {18 console.error("Webhook signature mismatch");19 return new Response("Unauthorized", { status: 401 });20 }2122 const event = JSON.parse(rawBody);23 const chargeCode = event.data?.code;24 const eventType = event.type;2526 // Log the event27 await supabase.from("webhook_events").insert({ event_type: eventType, charge_id: chargeCode, payload: event, received_at: new Date().toISOString() });2829 if (eventType === "charge:confirmed" && chargeCode) {30 await supabase.from("crypto_orders").update({ status: "CONFIRMED", confirmed_at: new Date().toISOString() }).eq("charge_id", chargeCode);31 } else if (eventType === "charge:failed" && chargeCode) {32 await supabase.from("crypto_orders").update({ status: "FAILED" }).eq("charge_id", chargeCode);33 }3435 return new Response("OK", { status: 200 });36});Pro tip: Crypto transactions typically need 1-3 blockchain confirmations before Coinbase Commerce fires a charge:confirmed event. For Bitcoin, this takes 10-30 minutes. For Ethereum and ERC-20 tokens (USDC, DAI), it takes 1-5 minutes. Don't fulfill orders based on charge:pending — wait for charge:confirmed which indicates sufficient blockchain confirmations.
Expected result: The coinbase-webhooks Edge Function verifies signatures and processes payment events. When a test payment is confirmed, the crypto_orders table status updates to CONFIRMED.
Common use cases
Digital product purchase with crypto checkout
An online store for digital downloads (ebooks, templates, software licenses) offers crypto payment as an alternative to Stripe. When a buyer selects a product and clicks 'Pay with Crypto', an Edge Function creates a Coinbase Commerce charge and redirects the buyer to the hosted checkout where they can pay with their preferred cryptocurrency. After payment confirmation via webhook, the buyer receives their download link.
Add a 'Pay with Crypto' button to the product purchase page. When clicked, call an Edge Function that creates a Coinbase Commerce charge with the product name, price in USD, and a metadata field containing the user's order ID. Return the hosted_url from the charge and redirect the user to it. When payment is confirmed via Coinbase webhook, update the order status to 'paid' in Supabase and send the buyer their download link via email.
Copy this prompt to try it in Lovable
Crypto tip jar for creators
A creator portfolio site lets fans send crypto tips in any amount they choose. The Edge Function creates a charge without a fixed price (letting the buyer set the amount on Coinbase's page), and Coinbase detects whatever amount is sent. Tips are recorded in Supabase with the sender's crypto currency and amount for display on a public contribution list.
Add a crypto tip feature to the creator page. A 'Send a Tip' button opens a form where visitors enter their name and optional message. Clicking Send calls an Edge Function to create a Coinbase Commerce charge with no fixed price and a description showing the creator's name and the visitor's message. Redirect to the Coinbase hosted checkout. When payment confirmed via webhook, save the tip to a Supabase tips table with amount, currency, and optional message. Display recent tips publicly on the creator page.
Copy this prompt to try it in Lovable
Subscription payment in USDC
A membership platform accepts USDC (a stablecoin pegged to USD) for monthly subscriptions, avoiding credit card fees. Each month, the system creates a new charge for active subscribers and sends them a payment link. The webhook confirms payment and extends their membership. USDC eliminates price volatility compared to paying in Bitcoin or Ethereum.
Build a subscription billing system using Coinbase Commerce USDC payments. Create an Edge Function that generates monthly payment charges in USDC for each active subscriber (stored in Supabase). Send each subscriber a payment email with their Coinbase charge hosted_url. When the payment webhook fires with status 'CONFIRMED' for USDC, update the subscriber's renewal_date in Supabase by 30 days. Add a membership status indicator on the user dashboard showing days remaining and a 'Renew' link.
Copy this prompt to try it in Lovable
Troubleshooting
Webhook signature verification fails for all incoming events
Cause: The webhook secret in Cloud Secrets doesn't match the secret associated with the registered endpoint in Coinbase Commerce, or the raw body is being parsed before signature verification (destroying the exact byte representation needed for HMAC).
Solution: Verify that COINBASE_COMMERCE_WEBHOOK_SECRET exactly matches the webhook signing secret shown for your endpoint in Commerce → Settings → Security → Webhook subscriptions. Ensure you call await req.text() to get the raw body BEFORE any JSON parsing — the signature is computed over the raw bytes, and parsing JSON then re-stringifying changes the byte representation.
1// CORRECT: read raw body first, parse after signature verification2const rawBody = await req.text();3// ... verify signature ...4const event = JSON.parse(rawBody); // parse after verificationCharges expire before users complete payment
Cause: Coinbase Commerce charges expire after 1 hour by default. If users click 'Pay with Crypto' and then delay paying (common with crypto where users need time to find their wallet), the charge expires and the hosted_url stops accepting new payments.
Solution: Implement charge status checking in your app: when a user returns to complete payment, check the charge status via the Commerce API. If expired, create a new charge automatically. You can also increase the charge expiry time in Commerce settings. Show a countdown timer on your payment page so users know they need to complete payment within the hour.
charge:confirmed event received but order not found in database
Cause: The charge_id in the webhook doesn't match what's stored in your crypto_orders table. Coinbase Commerce charges have a 'code' field (a short alphanumeric string like 'ABC123XY') as the identifier — if you stored the full charge object ID instead of the code field, lookups will fail.
Solution: Use event.data.code (the short charge code, e.g., 'ABC123XY') as the identifier in your database, not the full UUID-format id. The X-CC-Webhook-Signature event payload uses the code consistently. Verify which field you stored at charge creation time and ensure the webhook handler matches.
1// Store and look up by charge CODE, not charge ID2const chargeCode = event.data?.code; // 'ABC123XY' format3await supabase.from('crypto_orders').update({...}).eq('charge_id', chargeCode);Best practices
- Always verify the X-CC-Webhook-Signature header before processing any payment events — without verification, fraudulent webhook calls could trigger order fulfillment without actual payment.
- Store all incoming webhook events in a raw log table before processing — this gives you an audit trail and allows replaying events if your processing logic has a bug.
- Don't fulfill orders on charge:pending events — only fulfill on charge:confirmed, which indicates sufficient blockchain confirmations. Pending payments can still fail if the crypto network rejects the transaction.
- Use USDC for price-sensitive applications where payment amount volatility is unacceptable — Bitcoin and Ethereum prices fluctuate between charge creation and payment, while USDC is pegged to USD.
- Implement a payment polling endpoint that checks charge status from the Commerce API — users who complete payment but whose browser doesn't redirect back to your site need a way to check their payment status.
- Set expiry time prominently in the checkout UI — showing a countdown timer creates urgency and helps users understand they need to complete payment within the hour window.
- Cache the current crypto exchange rates shown to users and update them every 30 seconds — outdated rates can cause confusion when the displayed crypto amount doesn't match the actual charge amount.
Alternatives
Binance API is for exchange trading and market data, not merchant payment acceptance; use Binance for crypto dashboards and Coinbase Commerce for crypto checkout.
Stripe handles traditional card payments with a native Lovable connector; use Stripe for the majority of users and add Coinbase Commerce as an additional crypto payment option.
PayPal handles consumer fiat payments and international transfers; choose Coinbase Commerce specifically when buyers prefer paying with cryptocurrency rather than credit cards.
Frequently asked questions
Does Coinbase Commerce require buyers to have a Coinbase account?
No — buyers can pay with any cryptocurrency wallet, not just Coinbase. Coinbase Commerce generates a payment address for each charge, and buyers can send crypto from any wallet (Metamask, Trust Wallet, hardware wallets, etc.) to that address. The Coinbase hosted checkout shows a QR code and wallet address that works with any compatible wallet. Some buyers will have Coinbase accounts and can pay directly, but it's not required.
How do I receive the money after a crypto payment is confirmed?
Coinbase Commerce holds confirmed crypto payments in your Commerce account. You can either keep funds as crypto or convert and withdraw to your bank account in fiat currency through Coinbase. For USDC payments, the value is stable and can be withdrawn as USD. For Bitcoin and Ethereum, you choose when to convert based on your currency risk preference. Payouts to your bank typically take 1-3 business days after initiating a withdrawal.
What cryptocurrencies does Coinbase Commerce accept?
Coinbase Commerce currently supports Bitcoin (BTC), Ethereum (ETH), Litecoin (LTC), Bitcoin Cash (BCH), USD Coin (USDC), Dai (DAI), and a growing list of other tokens. The specific coins available for a charge depend on your Commerce account settings. USDC and Dai are stablecoins pegged to USD, making them the best choice for avoiding exchange rate risk on fixed-price products.
Is there a transaction fee for Coinbase Commerce?
Coinbase Commerce has no transaction fees for receiving crypto payments — it's free to use. You receive 100% of the crypto sent (minus the blockchain network gas fees paid by the buyer). When you withdraw to fiat currency through Coinbase, standard exchange fees apply (typically 0-2% depending on your Coinbase tier). This makes Coinbase Commerce significantly cheaper than traditional card processors for large transaction amounts.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation