Coinbase Commerce
Accept cryptocurrency payments with Coinbase Commerce. Use this skill when the
You are a payments specialist who integrates Coinbase Commerce into projects.
Coinbase Commerce lets merchants accept cryptocurrency payments (BTC, ETH, USDC,
and others) without holding crypto themselves — funds can auto-convert to fiat.
## Key Points
- Use small amounts on mainnet (e.g., $0.50 USDC) for end-to-end testing
- Use the charge API to create charges and inspect the response structure
- Use webhook test events from the Coinbase Commerce dashboard
- For local development, use a tunnel (ngrok) to receive webhooks
- Always verify webhook signatures with HMAC-SHA256 before processing
- Store `charge.id` and the `metadata` you set for reconciliation
- Only grant access on `charge:confirmed` — not `charge:pending`
- Handle `charge:delayed` gracefully — the customer did pay, just late
- Set `metadata` with your internal user/order IDs on every charge
- Display the charge `hosted_url` or addresses so customers can pay from any wallet
- Implement a status page that polls charge status for real-time UX updates
- Granting access on `charge:pending` — the transaction is not yet confirmed
## Quick Example
```typescript
const { data: charge } = await commerceRequest('GET', `/charges/${chargeId}`);
console.log(charge.timeline); // Array of status events
console.log(charge.payments); // Array of detected payments
// Status: NEW, PENDING, COMPLETED, EXPIRED, UNRESOLVED, RESOLVED, CANCELED
```
```typescript
await commerceRequest('POST', `/charges/${chargeId}/cancel`);
```skilldb get payment-services-skills/Coinbase CommerceFull skill: 252 linesCoinbase Commerce — Payment Integration
You are a payments specialist who integrates Coinbase Commerce into projects. Coinbase Commerce lets merchants accept cryptocurrency payments (BTC, ETH, USDC, and others) without holding crypto themselves — funds can auto-convert to fiat.
Core Philosophy
Crypto payments are push-based
Unlike card payments where you pull funds, crypto payments are push-based. You create a charge with an amount, the customer sends crypto to a generated address, and you wait for blockchain confirmation via webhooks.
Charges expire
Each charge generates a unique payment address and has a time window (typically 60 minutes). If the customer does not send funds in time, the charge expires.
Blockchain confirmations take time
Bitcoin transactions need multiple block confirmations (typically 2-6) before they are considered final. Webhooks notify you of each confirmation stage — pending, confirmed, and failed.
Setup
API key
Get your API key and webhook shared secret from the Coinbase Commerce dashboard
at commerce.coinbase.com.
const COINBASE_COMMERCE_API = 'https://api.commerce.coinbase.com';
async function commerceRequest(method: string, path: string, body?: object) {
const res = await fetch(`${COINBASE_COMMERCE_API}${path}`, {
method,
headers: {
'Content-Type': 'application/json',
'X-CC-Api-Key': process.env.COINBASE_COMMERCE_API_KEY,
'X-CC-Version': '2018-03-22',
},
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) throw new Error(`Coinbase Commerce ${res.status}: ${await res.text()}`);
return res.json();
}
Key Techniques
Create a charge
const { data: charge } = await commerceRequest('POST', '/charges', {
name: 'Pro Plan',
description: 'Monthly subscription — Pro Plan',
pricing_type: 'fixed_price',
local_price: {
amount: '29.99',
currency: 'USD',
},
metadata: {
user_id: userId,
order_id: orderId,
},
redirect_url: 'https://yourdomain.com/success',
cancel_url: 'https://yourdomain.com/cancel',
});
// charge.hosted_url — redirect customer here
// charge.id — store for webhook matching
// charge.addresses — BTC, ETH, USDC addresses for direct payment
Create a checkout (reusable product link)
const { data: checkout } = await commerceRequest('POST', '/checkouts', {
name: 'Pro Plan',
description: 'Monthly Pro Plan subscription',
pricing_type: 'fixed_price',
local_price: {
amount: '29.99',
currency: 'USD',
},
requested_info: ['email', 'name'],
});
// checkout.hosted_url — shareable payment link
Retrieve a charge
const { data: charge } = await commerceRequest('GET', `/charges/${chargeId}`);
console.log(charge.timeline); // Array of status events
console.log(charge.payments); // Array of detected payments
// Status: NEW, PENDING, COMPLETED, EXPIRED, UNRESOLVED, RESOLVED, CANCELED
Cancel a charge
await commerceRequest('POST', `/charges/${chargeId}/cancel`);
List charges
const { data: charges, pagination } = await commerceRequest(
'GET',
'/charges?limit=25&order=desc'
);
Webhook Processing
| Event | Action |
|---|---|
charge:created | Charge created, waiting for payment |
charge:pending | Payment detected, awaiting confirmation |
charge:confirmed | Payment confirmed on blockchain — grant access |
charge:failed | Payment failed (e.g., underpayment, timeout) |
charge:delayed | Payment detected after charge expired |
charge:resolved | Previously unresolved charge manually resolved |
import crypto from 'crypto';
export async function POST(req: Request) {
const body = await req.text();
const signature = req.headers.get('x-cc-webhook-signature');
// Verify webhook signature
const expected = crypto
.createHmac('sha256', process.env.COINBASE_COMMERCE_WEBHOOK_SECRET)
.update(body)
.digest('hex');
if (signature !== expected) {
return new Response('Invalid signature', { status: 401 });
}
const event = JSON.parse(body);
const charge = event.event.data;
const userId = charge.metadata?.user_id;
switch (event.event.type) {
case 'charge:pending':
// Payment sent, waiting for blockchain confirmation
await markOrderPending(userId, charge.id);
break;
case 'charge:confirmed':
// Payment confirmed — safe to grant access
await grantAccess(userId, charge.id);
break;
case 'charge:failed':
// Payment expired or underpaid
await markOrderFailed(userId, charge.id);
break;
case 'charge:delayed':
// Payment arrived after expiry — decide how to handle
await handleDelayedPayment(userId, charge.id, charge.payments);
break;
}
return new Response('OK', { status: 200 });
}
Handle underpayments and overpayments
function checkPaymentAmount(charge: any) {
for (const payment of charge.payments) {
const expected = parseFloat(payment.value.local.amount);
const received = parseFloat(payment.value.crypto.amount);
const status = payment.status; // CONFIRMED, PENDING
if (payment.block?.confirmations_required > payment.block?.confirmations) {
// Still awaiting confirmations
continue;
}
// Coinbase reports underpayment/overpayment in the charge timeline
// Check charge.timeline for UNDERPAID or OVERPAID context
}
const lastEvent = charge.timeline[charge.timeline.length - 1];
if (lastEvent.context === 'UNDERPAID') {
// Customer sent less than required — contact them or resolve manually
return 'underpaid';
}
if (lastEvent.context === 'OVERPAID') {
// Customer sent more — grant access, consider partial refund
return 'overpaid';
}
return 'exact';
}
Embedding a payment button
<div>
<a href="${charge.hosted_url}" target="_blank" rel="noopener">
Pay with Crypto
</a>
</div>
<!-- Or use Coinbase Commerce button -->
<script src="https://commerce.coinbase.com/v1/checkout.js"></script>
<div>
<a class="buy-with-crypto" href="https://commerce.coinbase.com/checkout/${checkoutId}">
Pay with Crypto
</a>
</div>
Testing
Coinbase Commerce does not provide a sandbox. Testing approaches:
- Use small amounts on mainnet (e.g., $0.50 USDC) for end-to-end testing
- Use the charge API to create charges and inspect the response structure
- Use webhook test events from the Coinbase Commerce dashboard
- For local development, use a tunnel (ngrok) to receive webhooks
Best Practices
- Always verify webhook signatures with HMAC-SHA256 before processing
- Store
charge.idand themetadatayou set for reconciliation - Only grant access on
charge:confirmed— notcharge:pending - Handle
charge:delayedgracefully — the customer did pay, just late - Set
metadatawith your internal user/order IDs on every charge - Display the charge
hosted_urlor addresses so customers can pay from any wallet - Implement a status page that polls charge status for real-time UX updates
Anti-Patterns
- Granting access on
charge:pending— the transaction is not yet confirmed - Ignoring
charge:delayedevents — the customer sent real money after expiry - Not handling underpayments — the customer may have sent partial funds
- Relying on redirect URL alone without webhook verification for fulfillment
- Creating charges without
metadata— impossible to match to internal orders - Expecting instant confirmation — Bitcoin can take 10-60 minutes
- Not informing users about the payment time window — charges expire
- Building subscription logic on top of one-time charges without automation
Install this skill directly: skilldb add payment-services-skills
Related Skills
Adyen
Accept payments with Adyen. Use this skill when the project needs to integrate
Braintree
Accept payments with Braintree (PayPal). Use this skill when the project needs
Checkout Com
Accept payments with Checkout.com. Use this skill when the project needs to
Creem
Accept payments with Creem as merchant of record. Use this skill when the project
Klarna
Accept payments with Klarna. Use this skill when the project needs to integrate
Lemonsqueezy
Accept payments with Lemon Squeezy as merchant of record. Use this skill when