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.
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) 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 /):
// 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 -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.
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_typemust befixed_price;local_price.currencymust be USD (USDC / USDT at 1:1).no_priceand other fiat return 400.- The address appears once the customer picks a coin and network at checkout:
addressesholds only that network,pricingonlylocalplussettlementonce picked; noexchange_rates,checkoutor branding fields. metadatais 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.