# Coinbase Commerce-compatible API

> Switch from Coinbase Commerce to blockwallets by changing the SDK base URL and keys: supported charge endpoints, webhooks and signatures, and differences.
>
> HTML: https://blockwallets.io/docs/coinbase-commerce?lang=en · 中文: https://blockwallets.io/docs/coinbase-commerce.md

## Coinbase Commerce-compatible API

Charge endpoints, the `X-CC-Api-Key` header, the charge object and webhooks signed with `X-CC-Webhook-Signature` follow Coinbase Commerce’s docs, so the official SDKs work after changing one line. Coinbase Commerce stopped serving merchants outside the US and Singapore on 2026-03-31. Coming from somewhere else? See the [migration overview](https://blockwallets.io/docs/migrate?lang=en).

Underneath it is a regular blockwallets bill: the receiving addresses are yours, the payment page is our hosted checkout, and a payment only counts once it is confirmed on chain and cross-checked against a second node. Below: three steps, then what each API supports and **how it differs from the original** — parameters we don't implement return an error instead of being silently ignored.

### Three steps

- Get a merchant account ([apply on the home page](https://blockwallets.io/#join)) and add your domains under “Website domains” on “Developers › Integration & keys” in the dashboard. Callback URLs (`ipn_callback_url`) and redirect URLs (`success_url` / `cancel_url` / `redirect_url`) must be on those domains or your callback URL's domain — a leaked key can't point webhooks elsewhere.

- Generate compatible credentials in the merchant dashboard under “Developers › Integration & keys”, one set for live and one for test: NOWPayments gets an **API key + IPN secret**, Coinbase Commerce an **API key + webhook shared secret**. They are shown once; we only keep hashes / ciphertext.

- Point your code at the base URL below and use the new keys. Run it on testnets with the test credentials first, then switch to live.

| Before | After | Auth header | Webhook signature |
| --- | --- | --- | --- |
| `https://api.commerce.coinbase.com` | `https://blockwallets.io/coinbase/` | `X-CC-Api-Key` | `X-CC-Webhook-Signature` (HMAC-SHA256) |

### Coinbase Commerce

All official SDKs take a base URL — change one line (mind the trailing `/`):

```sdk
// Node(coinbase-commerce-node)
const { Client } = require("coinbase-commerce-node");
- Client.init(process.env.COINBASE_COMMERCE_API_KEY);
+ Client.init(process.env.COINBASE_COMMERCE_API_KEY, "https://blockwallets.io/coinbase/");

# Python(coinbase-commerce)
- client = Client(api_key=API_KEY)
+ client = Client(api_key=API_KEY, base_api_uri="https://blockwallets.io/coinbase/")

// PHP(coinbase/coinbase-commerce)
- ApiClient::init($apiKey);
+ ApiClient::init($apiKey, 'https://blockwallets.io/coinbase/');
```

```curl
curl -X POST https://blockwallets.io/coinbase/charges \
  -H "X-CC-Api-Key: $COINBASE_COMMERCE_API_KEY" -H "X-CC-Version: 2018-03-22" \
  -H "content-type: application/json" \
  -d '{"name":"Order #12345","description":"Pro plan","pricing_type":"fixed_price",
       "local_price":{"amount":"100.00","currency":"USD"},
       "redirect_url":"https://shop.example/success","cancel_url":"https://shop.example/cancel",
       "metadata":{"order_id":"12345"}}'
# → 201 {"data": {"id": "…", "code": "7C7V5ECK", "hosted_url": "https://blockwallets.io/pay/inv_…", "timeline": [{"status": "NEW", …}], …}}
```

#### Supported endpoints

| Endpoint | Notes |
| --- | --- |
| `POST /charges` | Create a charge; `hosted_url` is our hosted checkout; valid for 1 hour |
| `GET /charges/:code_or_id` | Look up by 8-character code or id |
| `GET /charges` | List: `limit` (≤100) / `starting_after` / `ending_before` / `order`, with the same `pagination` object |
| `POST /charges/:code_or_id/cancel` | Cancel a charge with no payment seen yet |

Not available: `/charges/:code/resolve` (400 — handle underpaid charges in the dashboard), `/checkouts`, `/events`, `/invoices` (404).

#### Webhooks

Coinbase webhooks are account-level, and so are ours: they go to the **callback URL** set in your dashboard (charges created with test credentials go to the test callback URL). The body is Coinbase's `{id, scheduled_for, attempt_number, event: {id, type, api_version, created_at, data}}` with the full charge in `data`; `X-CC-Webhook-Signature` = HMAC-SHA256(shared secret, raw body), so the SDKs' `Webhook.verifyEventBody` works as is. On retries `event.id` stays the same and `attempt_number` goes up — dedupe on `event.id`.

```nodejs
import crypto from "node:crypto";

// Same as the SDK's Webhook.verifySigHeader: HMAC-SHA256(shared secret, raw body)
function verifyWebhook(rawBody, sig, sharedSecret) {
  const hmac = crypto.createHmac("sha256", sharedSecret).update(rawBody, "utf8").digest("hex");
  return crypto.timingSafeEqual(Buffer.from(hmac), Buffer.from(sig || ""));
}
// verifyWebhook(rawBody, req.headers["x-cc-webhook-signature"], process.env.COINBASE_WEBHOOK_SECRET)
// or with the official SDK:Webhook.verifyEventBody(rawBody, signature, sharedSecret)
```

| Event | When | Last timeline entry |
| --- | --- | --- |
| `charge:confirmed` | Paid (confirmed on chain and cross-checked; overpaid counts) | `COMPLETED` |
| `charge:failed` | Expired unpaid / cancelled by you / expired underpaid | `EXPIRED` / `CANCELED` / `UNRESOLVED` (context `UNDERPAID`) |

When you query you will also see `NEW` (awaiting payment) and `PENDING` (payment seen on chain, confirming). **Fulfil on `charge:confirmed`**: we don't send `charge:created`, `charge:pending`, `charge:delayed` or `charge:resolved`.

#### Differences from Coinbase Commerce

- `pricing_type` must be `fixed_price`; `local_price.currency` must be USD (USDC / USDT at 1:1). `no_price` and other fiat return 400.

- The address appears once the customer picks a coin and network at checkout: `addresses` holds only that network, `pricing` only `local` plus `settlement` once picked; no `exchange_rates`, `checkout` or branding fields.

- `metadata` is stored and returned as is (up to 4 KB).

- Refunds don't show up in the timeline; see them in the dashboard.

### Test mode

Charges and payments created with test credentials run on each chain's testnet and their webhooks are signed with the test IPN secret / shared secret — fully separate from live: a test key can't see live payments.

Everything else (checkout, confirmations, networks and minimums, refunds) works as in the [main docs](https://blockwallets.io/docs/cryptomus?lang=en).
