Skip to main content
Business & GrowthPayment Services251 lines

Coinbase Commerce

Accept cryptocurrency payments with Coinbase Commerce. Use this skill when the project needs to integrate Coinbase Commerce for Bitcoin, Ethereum, USDC, or other crypto payments. Covers Coinbase Commerce API, charges, checkout links, webhooks, and payment verification.

Quick Summary34 lines
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: 251 lines
Paste into your CLAUDE.md or agent config

Coinbase 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

EventAction
charge:createdCharge created, waiting for payment
charge:pendingPayment detected, awaiting confirmation
charge:confirmedPayment confirmed on blockchain — grant access
charge:failedPayment failed (e.g., underpayment, timeout)
charge:delayedPayment detected after charge expired
charge:resolvedPreviously 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.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

Anti-Patterns

  • Granting access on charge:pending — the transaction is not yet confirmed
  • Ignoring charge:delayed events — 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

Get CLI access →

Related Skills

Creem

Accept payments with Creem as merchant of record. Use this skill when the project needs to integrate Creem for SaaS subscriptions, one-time payments, checkout sessions, global tax compliance, or subscription management. Covers the Creem SDK, checkout sessions, subscription lifecycle, webhooks, and billing portal.

Payment Services139L

Klarna

Accept payments with Klarna. Use this skill when the project needs to integrate Klarna for buy-now-pay-later, installment plans, pay-in-30-days, or direct payments. Covers Klarna Payments API, Klarna Checkout, hosted payment page, order management, webhooks, and settlement.

Payment Services286L

Lemonsqueezy

Accept payments with Lemon Squeezy as merchant of record. Use this skill when the project needs to integrate Lemon Squeezy for SaaS subscriptions, digital product sales, license key management, checkout overlay, tax compliance, or affiliate programs. Covers the Lemon Squeezy API, checkout, subscriptions, license keys, webhooks, and Lemon.js.

Payment Services201L

Mollie

Accept payments with Mollie. Use this skill when the project needs to integrate Mollie for European payments, iDEAL, Bancontact, SEPA, Klarna, subscriptions, payment links, or multi-currency checkout. Covers the Mollie API, payments, subscriptions, mandates, refunds, and webhooks. Popular in Netherlands, Belgium, Germany, and across Europe.

Payment Services170L

Paddle

Accept payments with Paddle as merchant of record. Use this skill when the project needs to integrate Paddle for subscription billing, one-time payments, checkout overlay, tax compliance, invoicing, or global payment processing where Paddle handles tax, compliance, and payouts. Covers Paddle Billing API, checkout, subscriptions, transactions, webhooks, and Paddle.js.

Payment Services213L

Paypal

Accept payments with PayPal. Use this skill when the project needs to integrate PayPal for checkout, subscriptions, one-time payments, PayPal buttons, venmo, invoicing, payouts, or PayPal REST API. Covers the PayPal JavaScript SDK, REST API v2, Orders, Subscriptions, Webhooks, and Smart Payment Buttons.

Payment Services224L