Skip to main content
RapidDev - Software Development Agency
flutterflow-integrationsFlutterFlow API Call

Coinbase API

Connect FlutterFlow to Coinbase Commerce by proxying charge creation through a Firebase Cloud Function, then embedding the returned hosted_url in a Custom Widget WebView. Payment confirmation must come from a verified webhook — never from the WebView redirect alone. For read-only price or balance data, a direct FlutterFlow API Call to api.coinbase.com works within the 10,000 requests per hour limit.

What you'll learn

  • Why the Coinbase Commerce API key must live in a Cloud Function and never in an FF API Call header
  • How to create a Coinbase Commerce charge via a Cloud Function and pass the hosted_url back to FlutterFlow
  • How to embed the hosted checkout URL in a Custom Widget WebView so customers can pay in-app
  • How to verify the X-CC-Webhook-Signature (HMAC-SHA256) server-side before marking an order paid
  • How to add a direct FlutterFlow API Call to api.coinbase.com for live crypto prices and account balances
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced20 min read90 minutesPaymentsLast updated July 2026RapidDev Engineering Team
TL;DR

Connect FlutterFlow to Coinbase Commerce by proxying charge creation through a Firebase Cloud Function, then embedding the returned hosted_url in a Custom Widget WebView. Payment confirmation must come from a verified webhook — never from the WebView redirect alone. For read-only price or balance data, a direct FlutterFlow API Call to api.coinbase.com works within the 10,000 requests per hour limit.

Quick facts about this guide
FactValue
ToolCoinbase API
CategoryPayments
MethodFlutterFlow API Call
DifficultyAdvanced
Time required90 minutes
Last updatedJuly 2026

Accepting Crypto Payments in FlutterFlow with Coinbase Commerce

If you want your FlutterFlow app to accept Bitcoin, Ethereum, USDC, or other cryptocurrencies, Coinbase Commerce is the right product — not the Coinbase Exchange or brokerage. Commerce is a standalone merchant payment platform. You create a charge via its REST API, send the customer to a hosted checkout page, and receive a webhook when payment is confirmed on-chain. The two products even use different base URLs: Commerce lives at api.commerce.coinbase.com while the App API (for account data and spot prices) lives at api.coinbase.com.

FlutterFlow compiles to a Flutter app that runs on the customer's device, which means any key you put in an API Call header ships inside the compiled app binary. For read-only endpoints (spot prices, currency lists) with public keys this is acceptable, but the Commerce API key that creates charges and can query order history is sensitive — exposing it lets anyone create charges on your account or probe your order data. The correct pattern is a Firebase Cloud Function that holds the key server-side; FlutterFlow calls the function, not Coinbase directly.

Crypto payments have a timing characteristic that fiat payments don't: network confirmation can take seconds to minutes depending on the asset and network congestion. This means you cannot trust the WebView redirect URL to confirm payment — a user could simply navigate to the success URL manually. The only trustworthy confirmation signal is Coinbase Commerce's webhook, which fires after the network confirms the transaction. The webhook includes an X-CC-Webhook-Signature header that you must verify using HMAC-SHA256 before updating any order status in Firestore. Coinbase Commerce charges approximately 1% per transaction (check current rates on their pricing page), and the App API allows 10,000 authenticated requests per hour for price and account data widgets.

Integration method

FlutterFlow API Call

FlutterFlow calls a Firebase Cloud Function that creates a Coinbase Commerce charge and returns the hosted_url. The app opens this URL in a Custom Widget WebView so the customer can pay. Because crypto confirmations are asynchronous, a second Cloud Function listens for the verified webhook and updates Firestore once payment is confirmed. For read-only data (live prices, account balances), a direct API Call to api.coinbase.com can be added as a secondary integration.

Prerequisites

  • A Coinbase Commerce account at commerce.coinbase.com with API key and webhook shared secret copied from Settings → API Keys
  • A Firebase project on the Blaze (pay-as-you-go) plan — Cloud Functions require Blaze to deploy and run
  • FlutterFlow project connected to Firebase (Settings & Integrations → Firebase in FlutterFlow)
  • Node.js knowledge to write and deploy the two Cloud Functions (charge creation and webhook handler)
  • Basic familiarity with FlutterFlow's Custom Code panel for adding the Custom Widget WebView

Step-by-step guide

1

Set up Coinbase Commerce and collect credentials

Go to commerce.coinbase.com and create an account if you don't have one. After logging in, navigate to Settings (gear icon, top right) → API Keys and click 'Create an API Key'. Copy the key immediately — it is shown only once. Then scroll down to the Webhook Subscriptions section and create a new webhook subscription; you will paste the Cloud Function URL here after deployment in step 4. Also note your webhook shared secret from this section — you will need it for HMAC signature verification. Next, decide whether you also want to show live crypto prices or account balances (the Coinbase App API at api.coinbase.com). If yes, go to coinbase.com → Settings → API (under the developer menu) and create a separate API key with read-only permissions — this is a different product with a different key from Commerce. Store both keys securely; you will add them as environment variables in your Cloud Functions, never in FlutterFlow directly. A quick note on the two products: Coinbase Commerce handles merchant payments (charges, webhooks, order management) at api.commerce.coinbase.com. The Coinbase App API at api.coinbase.com handles account data, prices, and portfolio information. They have separate keys, separate base URLs, and separate use cases. Mixing them up is a common mistake that produces confusing authentication errors.

Pro tip: Test mode: Coinbase Commerce has a test mode you can enable in Settings → Test Mode. Test charges don't process real crypto but let you validate the full webhook flow. Enable it during development and disable before going live.

Expected result: You have a Commerce API key, a webhook shared secret, and (optionally) an App API key stored securely. You know which base URL corresponds to each product.

2

Deploy a Cloud Function to create charges and return hosted_url

In Firebase Console, navigate to Functions and open the Cloud Functions editor, or use your local Firebase project. Create a new HTTPS function (callable or HTTP trigger — HTTP trigger is simpler here) that accepts the charge details from FlutterFlow and calls the Coinbase Commerce POST /charges endpoint. The function receives the item name, amount in the display currency (e.g. USD), and a redirect URL from FlutterFlow. It then posts to https://api.commerce.coinbase.com/charges with the X-CC-Api-Key header set to your Commerce API key (stored as a Cloud Function environment variable, never hardcoded). The response includes a hosted_url — your function extracts this and returns it to FlutterFlow as a JSON response. Use the Firebase CLI to set your environment variables: firebase functions:config:set coinbase.commerce_api_key='your_key_here'. In the function code, access it via functions.config().coinbase.commerce_api_key (for Firebase Functions v1) or process.env for v2. Deploy the function and copy its HTTPS URL. Be aware that CORS matters for FlutterFlow web builds — add the cors npm package to allow requests from your FlutterFlow app's domain. Native iOS and Android builds don't enforce CORS, but web does. Once deployed, go back to Coinbase Commerce Settings → Webhook Subscriptions and paste the second Cloud Function URL (which you will deploy in step 4) as the webhook endpoint.

index.js
1// Firebase Cloud Functions v2 — charge creation (index.js)
2const { onRequest } = require('firebase-functions/v2/https');
3const { defineSecret } = require('firebase-functions/params');
4const cors = require('cors')({ origin: true });
5const fetch = require('node-fetch');
6
7const COINBASE_COMMERCE_KEY = defineSecret('COINBASE_COMMERCE_KEY');
8
9exports.createCoinbaseCharge = onRequest(
10 { secrets: [COINBASE_COMMERCE_KEY] },
11 async (req, res) => {
12 cors(req, res, async () => {
13 if (req.method !== 'POST') {
14 return res.status(405).json({ error: 'Method not allowed' });
15 }
16 const { name, description, amount, currency, orderId } = req.body;
17 try {
18 const response = await fetch(
19 'https://api.commerce.coinbase.com/charges',
20 {
21 method: 'POST',
22 headers: {
23 'Content-Type': 'application/json',
24 'X-CC-Api-Key': COINBASE_COMMERCE_KEY.value(),
25 'X-CC-Version': '2018-03-22',
26 },
27 body: JSON.stringify({
28 name,
29 description,
30 pricing_type: 'fixed_price',
31 local_price: { amount: String(amount), currency },
32 metadata: { order_id: orderId },
33 }),
34 }
35 );
36 const data = await response.json();
37 if (!response.ok) {
38 return res.status(response.status).json({ error: data });
39 }
40 return res.json({
41 charge_id: data.data.id,
42 hosted_url: data.data.hosted_url,
43 expires_at: data.data.expires_at,
44 });
45 } catch (err) {
46 return res.status(500).json({ error: err.message });
47 }
48 });
49 }
50);

Pro tip: Coinbase Commerce charges expire after one hour by default. If your checkout flow might take longer (e.g. the user leaves the app and returns), check the expires_at field returned by the function and create a new charge if it has expired.

Expected result: The Cloud Function is deployed and accessible via its HTTPS URL. You can test it with a POST request (e.g. via Postman) and receive back a hosted_url like https://commerce.coinbase.com/charges/ABC123.

3

Build the FlutterFlow API Call and Custom Widget WebView

In FlutterFlow, click API Calls in the left navigation sidebar, then click + Add → Create API Group. Name it 'CoinbaseCommerce' and set the base URL to your Firebase Cloud Function's base domain (e.g. https://us-central1-yourproject.cloudfunctions.net). This is your Cloud Function's URL, not Coinbase's URL directly. Inside the API Group, click + Add API Call. Name it 'CreateCharge'. Set the method to POST and the endpoint to /createCoinbaseCharge (matching your function name). In the Variables tab, add variables for name (String), description (String), amount (String — always pass crypto amounts as strings to avoid float rounding), currency (String, e.g. 'USD'), and orderId (String). In the Body tab, select JSON and map each variable using the {{ variable }} syntax: { "name": "{{ name }}", "description": "{{ description }}", "amount": "{{ amount }}", "currency": "{{ currency }}", "orderId": "{{ orderId }}" }. Click Test to fire the call with sample values. In the Response & Test tab, paste the sample JSON response from your Cloud Function and click Generate JSON Paths. Map $.hosted_url, $.charge_id, and $.expires_at as extracted response values. For the WebView: go to Custom Code → + Add → Widget. Name it 'CoinbaseWebView'. In the Dependencies field, add the webview_flutter package (flutter.dev pub.dev page). Paste a minimal WebView widget that accepts a 'url' parameter (String). Build the widget, then drop it onto a page — set the url parameter to the hosted_url from the API response. Set width to double.infinity and height to a suitable value (e.g. MediaQuery.of(context).size.height * 0.85) so the hosted checkout fills the screen.

custom_widget.dart
1// Custom Widget — CoinbaseWebView (custom_widget.dart)
2import 'package:flutter/material.dart';
3import 'package:webview_flutter/webview_flutter.dart';
4
5class CoinbaseWebView extends StatefulWidget {
6 const CoinbaseWebView({super.key, required this.url, this.width, this.height});
7
8 final String url;
9 final double? width;
10 final double? height;
11
12 @override
13 State<CoinbaseWebView> createState() => _CoinbaseWebViewState();
14}
15
16class _CoinbaseWebViewState extends State<CoinbaseWebView> {
17 late final WebViewController _controller;
18
19 @override
20 void initState() {
21 super.initState();
22 _controller = WebViewController()
23 ..setJavaScriptMode(JavaScriptMode.unrestricted)
24 ..loadRequest(Uri.parse(widget.url));
25 }
26
27 @override
28 Widget build(BuildContext context) {
29 return SizedBox(
30 width: widget.width ?? double.infinity,
31 height: widget.height ?? MediaQuery.of(context).size.height * 0.85,
32 child: WebViewWidget(controller: _controller),
33 );
34 }
35}

Pro tip: Custom widgets do not render inside FlutterFlow's web Test/Run preview — you must test on a real iOS/Android device or an emulator. Build an APK or use a local run to verify the WebView loads the Coinbase hosted checkout correctly.

Expected result: The FlutterFlow API Call to CreateCharge returns a hosted_url in the Response & Test panel. The CoinbaseWebView custom widget appears in your widget library and can be dropped onto a page with the URL wired to the response variable.

4

Deploy a webhook Cloud Function to verify payment and update Firestore

Crypto payment confirmation is asynchronous — the customer's transaction must be confirmed by the blockchain network before Coinbase marks the charge as 'COMPLETED'. You cannot rely on the WebView redirect URL to confirm payment; a user could navigate to a success URL without having paid. The only trustworthy signal is the Coinbase Commerce webhook, which fires a POST request to your Cloud Function URL after network confirmation. Deploy a second HTTPS Cloud Function to handle this webhook. The function must verify the X-CC-Webhook-Signature header by computing HMAC-SHA256 of the raw request body using your webhook shared secret. Use the crypto module (built into Node.js) for this. CRITICAL: you must read the raw body bytes, not a parsed JSON object — parsing JSON first will break the HMAC calculation because byte-level formatting differs. If the signature matches, parse the event type from the body. The key events are: 'charge:confirmed' (payment received and confirmed on-chain — mark order as paid), 'charge:failed' (charge expired without payment), and 'charge:pending' (transaction broadcast but not yet confirmed — update status to 'pending' in Firestore). Extract the metadata.order_id you embedded when creating the charge, and use it to update the corresponding Firestore document. After deploying this function, go to Coinbase Commerce Settings → Webhook Subscriptions and register the function's URL. Coinbase will send a test webhook you can use to verify the signature logic is working before going live. Return HTTP 200 quickly — Coinbase will retry if it receives a non-200 response or no response within a timeout.

webhookHandler.js
1// Firebase Cloud Functions v2 — webhook handler (webhookHandler.js)
2const { onRequest } = require('firebase-functions/v2/https');
3const { defineSecret } = require('firebase-functions/params');
4const { getFirestore } = require('firebase-admin/firestore');
5const { initializeApp } = require('firebase-admin/app');
6const crypto = require('crypto');
7
8initializeApp();
9const db = getFirestore();
10
11const WEBHOOK_SECRET = defineSecret('COINBASE_WEBHOOK_SECRET');
12
13exports.coinbaseWebhook = onRequest(
14 { secrets: [WEBHOOK_SECRET], rawBody: true },
15 async (req, res) => {
16 if (req.method !== 'POST') {
17 return res.status(405).send('Method Not Allowed');
18 }
19 const signature = req.headers['x-cc-webhook-signature'];
20 const rawBody = req.rawBody; // raw Buffer — do NOT use req.body here
21 const expectedSig = crypto
22 .createHmac('sha256', WEBHOOK_SECRET.value())
23 .update(rawBody)
24 .digest('hex');
25
26 if (signature !== expectedSig) {
27 console.error('Webhook signature mismatch');
28 return res.status(401).send('Unauthorized');
29 }
30
31 const event = JSON.parse(rawBody.toString());
32 const { type, data } = event;
33 const orderId = data?.metadata?.order_id;
34
35 if (!orderId) {
36 return res.status(200).send('No order_id in metadata');
37 }
38
39 const orderRef = db.collection('orders').doc(orderId);
40
41 if (type === 'charge:confirmed') {
42 await orderRef.update({ status: 'paid', confirmedAt: new Date() });
43 } else if (type === 'charge:pending') {
44 await orderRef.update({ status: 'pending' });
45 } else if (type === 'charge:failed') {
46 await orderRef.update({ status: 'failed' });
47 }
48
49 return res.status(200).send('OK');
50 }
51);

Pro tip: Never mark an order as paid based on the WebView redirect URL or from a charge:pending event — only trust charge:confirmed. The pending event means the transaction was broadcast but not yet confirmed; a blockchain reorganization can still invalidate it, though this is extremely rare for most assets after one confirmation.

Expected result: The webhook Cloud Function is deployed and its URL is registered in Coinbase Commerce Settings. Sending Coinbase's test webhook event returns 200 and updates the Firestore order document status correctly.

5

Wire the Action Flow and show payment status in FlutterFlow

With the Cloud Function and webhook in place, you can now build the complete user flow in FlutterFlow. Create a page with a 'Pay with Crypto' button. In the Action Flow Editor, wire the button to: (1) call the CreateCharge API Call with the order details from page state, (2) store the returned hosted_url in a Page State variable (String), (3) navigate to the WebView page or open a bottom sheet containing the CoinbaseWebView widget with the url parameter bound to the hosted_url state variable. For the status screen: create a Firestore document listener on the user's order document. In FlutterFlow, use a backend query (Firestore → Collection → Single Document) with real-time updates enabled on the page. Bind the order status field to a conditional widget that shows: 'Awaiting payment' when status is 'created', 'Processing on blockchain' when status is 'pending', 'Payment confirmed!' when status is 'paid', and 'Charge expired' when status is 'failed'. Use a CircularProgressIndicator on the pending state to communicate that confirmation is in progress. Also ensure you pass a unique orderId when creating the charge — this is how the webhook handler finds the right Firestore document. A good pattern is to create the Firestore order document first (with status 'created') and use its document ID as the orderId metadata on the charge. That way the webhook can always look up the order even if the charge_id is unavailable. If you'd prefer to skip building the Cloud Functions and WebView widget yourself, RapidDev's team builds FlutterFlow integrations like this every week — including the full crypto payment flow with webhook verification. Book a free scoping call at rapidevelopers.com/contact.

Pro tip: Add an 'Open in browser' fallback button on the WebView page. Some devices or OS configurations block WebView navigation to external payment domains. If the WebView fails to load, tapping 'Open in browser' launches the hosted_url in the device's default browser, where Coinbase Commerce works reliably.

Expected result: Tapping 'Pay with Crypto' creates a charge and opens the WebView with the hosted checkout. The order status page shows real-time updates from Firestore, transitioning from 'created' → 'pending' → 'paid' as the blockchain confirms the transaction.

6

(Optional) Add a live prices API Call for crypto price widgets

If you want to show live BTC/ETH/USDC prices in your app (a price ticker, a 'current value' display on an invoice, or a currency converter), you can add a direct FlutterFlow API Call to the Coinbase App API at api.coinbase.com. This is a different product from Commerce and uses a different API key. In FlutterFlow, go to API Calls → + Add → Create API Group. Name it 'CoinbaseAppAPI' and set the base URL to https://api.coinbase.com. Add a shared header: Authorization: Bearer {{ apiKey }}. Then add an API Call named 'GetSpotPrice' with method GET and endpoint /v2/prices/{{ currencyPair }}/spot (e.g. BTC-USD). In the Variables tab, add currencyPair (String). In the Response & Test tab, map $.data.amount as a JSON Path to extract the price. Note: The App API endpoint /v2/prices/{currency_pair}/spot is actually public (no auth required for spot prices) — check Coinbase's current docs, as public endpoints may not require the Bearer header. The 10,000 requests per hour limit applies to authenticated calls. For a simple price ticker refreshed every 30 seconds, you can use the Timer action in FlutterFlow to periodically re-call this API. Remember: keep the App API key in an FF App State variable only if it is a public/read-only key. If your App API key has any write permissions, route it through a Cloud Function just as you did for Commerce charges.

typescript
1// FlutterFlow API Call config (reference)
2// API Group: CoinbaseAppAPI
3// Base URL: https://api.coinbase.com
4// Shared Header: Authorization: Bearer {{ apiKey }}
5//
6// API Call: GetSpotPrice
7// Method: GET
8// Endpoint: /v2/prices/{{ currencyPair }}/spot
9// Variables: currencyPair (String, e.g. 'BTC-USD')
10//
11// Sample response:
12// { "data": { "base": "BTC", "currency": "USD", "amount": "67432.51" } }
13//
14// JSON Paths:
15// $.data.amount → priceAmount (String)
16// $.data.base → baseCurrency (String)
17// $.data.currency → quoteCurrency (String)

Pro tip: Crypto amounts can have many decimal places and vary by asset (ETH uses 18 decimal places internally). Always store and display amounts as strings, not floats, to avoid rounding errors. Use a Custom Function in FlutterFlow to format the string for display (e.g. showing 2 decimal places for USD-quoted prices).

Expected result: The GetSpotPrice API Call returns a current price in the Response & Test panel. You can bind the $.data.amount JSON Path to a Text widget on your app to show a live BTC or ETH price that refreshes on a timer.

Common use cases

NFT marketplace app with crypto checkout

A creator marketplace built in FlutterFlow where artists list digital assets and buyers pay with USDC or ETH. When a buyer taps 'Purchase', the app calls a Cloud Function to create a Commerce charge, then opens the hosted checkout in a WebView. Firestore order status updates automatically when the webhook confirms on-chain settlement.

FlutterFlow Prompt

Build a product detail screen with a 'Pay with Crypto' button. When tapped, call my Cloud Function to create a Coinbase Commerce charge for the product price, then open the returned hosted_url in a full-screen WebView. Show a 'Pending confirmation' banner until Firestore flips the order status to paid.

Copy this prompt to try it in FlutterFlow

Freelancer invoicing app with crypto payout option

A freelancer tool where clients can pay invoices in cryptocurrency. The app displays a live price widget using the Coinbase App API to show current BTC/USD and ETH/USD rates, then creates a Commerce charge for the invoice amount when the client selects crypto payment. The invoice status updates when the webhook fires.

FlutterFlow Prompt

Add a 'Pay Invoice in Crypto' button to the invoice detail screen. Also add a small widget showing live BTC and ETH prices fetched from api.coinbase.com. When the button is tapped, call the Cloud Function to create a charge and show the hosted checkout WebView.

Copy this prompt to try it in FlutterFlow

Event ticketing app with multi-currency support

A ticketing app that accepts both fiat (via Stripe's native integration) and cryptocurrency. For crypto buyers, the app creates a per-ticket Commerce charge, shows the hosted WebView, and displays a 'Waiting for network confirmation' screen with a timer. Firestore drives a real-time listener that updates the ticket to 'confirmed' when the webhook arrives.

FlutterFlow Prompt

On the checkout screen, let users choose Stripe or Crypto payment. For crypto, call the charge creation Cloud Function, open the WebView, then show a waiting screen. Listen to the Firestore order document in real time and navigate to the ticket confirmation screen when status becomes 'paid'.

Copy this prompt to try it in FlutterFlow

Troubleshooting

Webhook returns 401 Unauthorized — signature mismatch error in Cloud Function logs

Cause: The HMAC-SHA256 signature verification is failing, most commonly because the raw request body was parsed as JSON before hashing. Once the JSON is parsed and re-serialized, whitespace and key ordering can differ from what Coinbase sent, breaking the HMAC calculation. Another cause is using the wrong secret — Commerce webhook secret and Commerce API key are different values.

Solution: In your Cloud Function, use req.rawBody (available in Firebase Functions when rawBody: true is set) or a raw body parser middleware, NOT req.body. Compute HMAC-SHA256 on the raw Buffer before any JSON parsing. Verify you are using the webhook shared secret (from Settings → Webhook Subscriptions), not the API key.

typescript
1// Correct: use raw body
2const expectedSig = crypto
3 .createHmac('sha256', WEBHOOK_SECRET.value())
4 .update(req.rawBody) // raw Buffer, not JSON.stringify(req.body)
5 .digest('hex');

WebView shows a blank page or 'ERR_CONNECTION_REFUSED' — the hosted checkout doesn't load

Cause: On FlutterFlow web builds (browser), the WebView widget uses an iframe, and Coinbase Commerce's hosted checkout page may set X-Frame-Options or Content-Security-Policy headers that block iframe embedding. On native builds this is not an issue. The charge may also have expired (1-hour TTL).

Solution: For web builds: instead of the WebView widget, use FlutterFlow's Launch URL action to open the hosted_url in the user's default browser. For native builds: ensure the webview_flutter package dependency is correctly added in Custom Code. If the charge expired, create a new charge — check the expires_at field before opening the WebView and recreate if needed.

Order status never updates to 'paid' in Firestore even after paying in the WebView

Cause: The most common causes are: (1) the webhook URL is not registered in Coinbase Commerce Settings, (2) the Cloud Function webhook URL is incorrect or returning non-200 responses causing Coinbase to stop retrying, (3) the orderId in the charge metadata does not match the Firestore document ID, or (4) the order was confirmed but the charge:confirmed event type string is spelled differently in the code.

Solution: In Coinbase Commerce Settings → Webhook Subscriptions, verify the URL matches your deployed function URL exactly. Send the test webhook and check Cloud Function logs for errors. Confirm the orderId you pass when creating the charge matches the Firestore document ID. Check that your event handler tests for 'charge:confirmed' (exact string, colon-delimited, no spaces).

XMLHttpRequest error on FlutterFlow web preview when calling the charge creation Cloud Function

Cause: CORS (Cross-Origin Resource Sharing) is being blocked by the browser. The Cloud Function is not returning the correct CORS headers for the FlutterFlow web preview origin or your deployed web app's domain.

Solution: Add the cors npm package to your Cloud Function and configure it with { origin: true } (or specify your exact domain). Make sure the cors() middleware wraps the entire async handler. This is only needed for web builds — native iOS and Android apps do not enforce CORS.

typescript
1const cors = require('cors')({ origin: true });
2exports.createCoinbaseCharge = onRequest({ secrets: [...] }, (req, res) => {
3 cors(req, res, async () => {
4 // your handler logic here
5 });
6});

Best practices

  • Never put the Commerce API key or webhook shared secret in a FlutterFlow API Call header — both must live exclusively in Cloud Function environment secrets, not in the Flutter client bundle
  • Always embed a unique orderId in the charge metadata when creating it, and create the Firestore order document first — this guarantees the webhook can always find the correct order record even if the charge_id lookup fails
  • Verify the X-CC-Webhook-Signature on every webhook event using the raw request body before trusting any payment status update — skipping this check lets anyone fake a payment confirmation by sending a POST to your webhook URL
  • Display a 'pending' state in the app after the customer completes the WebView checkout — crypto confirmations are asynchronous and can take from seconds to minutes; never assume payment is complete until the charge:confirmed webhook fires
  • Store all crypto amounts as strings, not floats — floating-point representation cannot accurately store many decimal values, and crypto amounts (especially for assets like ETH) often require precision that floats cannot provide
  • Keep Commerce (api.commerce.coinbase.com) and the App API (api.coinbase.com) clearly separated in your codebase — they have different keys, base URLs, rate limits, and use cases; mixing them produces confusing 401 errors
  • Register the webhook URL in Commerce Settings before publishing your app to production — test the full charge:confirmed event flow in test mode and verify Firestore updates correctly before enabling live charges
  • Add an expiry check before opening the WebView — if the stored hosted_url was created more than 50 minutes ago, create a new charge rather than showing a checkout that will expire mid-session

Alternatives

Frequently asked questions

Can I accept crypto payments without Firebase — for example using Supabase?

Yes. The Cloud Function pattern can be replaced with Supabase Edge Functions (Deno). Your Edge Function handles the charge creation POST to Coinbase Commerce and the webhook verification using the same HMAC-SHA256 logic. Swap the Firestore update calls for Supabase database updates via the admin client. The FlutterFlow side remains identical — you call your Edge Function URL from the API Call instead of a Firebase Function URL.

What happens if the customer closes the WebView before paying?

Nothing — the Coinbase Commerce charge simply remains open until it expires (default 1 hour). The Firestore order stays in 'created' status. You can detect this by adding a WebView navigation change listener in the Custom Widget that fires when the user navigates to the cancel URL, and update the order status to 'cancelled' accordingly. Alternatively, poll the charge status via a Cloud Function if you want real-time cancellation detection without a navigation listener.

Is Coinbase Commerce available in all countries?

Coinbase Commerce has geographic restrictions — it is not available to merchants in all jurisdictions. Check the current list of supported countries on Coinbase's support pages before building. If Coinbase Commerce is unavailable in your region, consider a crypto-native alternative like BitPay, which follows a similar hosted-checkout and webhook pattern.

How do I handle partial payments — when a customer pays less than the charge amount?

Coinbase Commerce fires a charge:pending event when a partial payment is detected, followed by a charge:failed event if the full amount is never received. You can handle this in the webhook handler by checking the data.payments array on the charge:failed event to see if any partial amounts were received. For most apps, the simplest approach is to show a 'Payment incomplete — please contact support' message and create a new charge for the remaining amount after manual review.

Can I show the Coinbase Commerce checkout inside the app without a WebView?

Not with a native UI — Coinbase Commerce does not provide a native Flutter SDK or embeddable widget. The hosted checkout WebView is the standard approach. If you need a fully branded native checkout experience, you would need to implement a raw crypto payment flow: display wallet addresses and amounts directly, then poll the blockchain for transaction confirmation. This is significantly more complex and typically not recommended for most apps.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Integrations are where projects stall

We wire FlutterFlow integrations like this one every week — auth, webhooks, edge cases included. Fixed-price builds from $13K, delivered in 6–10 weeks.

Talk to an integration engineer

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.