Интеграция с Coinbase Commerce
Coinbase Commerce — non-custodial платёжный процессинг: средства поступают напрямую на ваш кошелёк, минуя Coinbase. Поддерживает BTC, ETH, USDC, DAI, LTC, BCH и ряд других активов. Ключевое отличие от custodial-процессингов: Coinbase не держит ваши деньги, нет KYC для получателя, нет риска заморозки средств со стороны процессора.
Что интегрируем
Два основных объекта API:
- Charge — одноразовый платёжный запрос с фиксированной суммой, привязанный к конкретному заказу
- Checkout — переиспользуемая платёжная страница (подходит для донатов, где сумма произвольная)
Для e-commerce используем Charges.
Создание Charge через API
const axios = require("axios");
async function createCharge(orderId, amountUSD, description) {
const response = await axios.post(
"https://api.commerce.coinbase.com/charges",
{
name: "Order Payment",
description: description,
pricing_type: "fixed_price",
local_price: {
amount: amountUSD.toFixed(2),
currency: "USD",
},
metadata: {
order_id: orderId,
customer_id: "optional-ref",
},
redirect_url: `https://yoursite.com/orders/${orderId}/success`,
cancel_url: `https://yoursite.com/orders/${orderId}/cancel`,
},
{
headers: {
"X-CC-Api-Key": process.env.COINBASE_COMMERCE_API_KEY,
"X-CC-Version": "2018-03-22",
},
}
);
return response.data.data; // содержит hosted_url, code, addresses
}
hosted_url — готовая страница Coinbase Commerce, куда редиректим пользователя. Coinbase показывает адреса в разных сетях на выбор пользователя, QR, countdown таймер (15 минут по умолчанию для крипто-курса).
Обработка webhook
Самая важная часть. Coinbase Commerce шлёт события при каждом изменении статуса:
const crypto = require("crypto");
app.post("/webhooks/coinbase", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-cc-webhook-signature"];
const webhookSecret = process.env.COINBASE_COMMERCE_WEBHOOK_SECRET;
// Верификация подписи — HMAC-SHA256 от raw body
const expectedSig = crypto
.createHmac("sha256", webhookSecret)
.update(req.body)
.digest("hex");
if (signature !== expectedSig) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(req.body);
switch (event.type) {
case "charge:confirmed":
// Достаточно для товаров с низким риском
await orderService.markConfirmed(event.data.metadata.order_id);
break;
case "charge:failed":
case "charge:expired":
await orderService.markFailed(event.data.metadata.order_id);
break;
case "charge:resolved":
// Финальный успешный статус после underpayment-resolve или delayed payment
await orderService.markResolved(event.data.metadata.order_id);
break;
}
res.json({ received: true });
});
Важно: req.body должен быть raw Buffer при верификации подписи — не парсить через express.json() до верификации, иначе подпись не сойдётся.
Статусная машина Charge
NEW → PENDING (получена первая транзакция) → CONFIRMED → RESOLVED
→ EXPIRED (истёк таймер)
→ FAILED (underpayment, timeout)
→ UNRESOLVED (требует ручного разбора)
CONFIRMED наступает после достаточного количества confirmations (зависит от сети: Bitcoin — 3, Ethereum — 12). Для большинства товаров достаточно CONFIRMED. RESOLVED — финальный статус, означает полную обработку включая overpayment-возвраты.
Polling как fallback
Webhook может не дойти — настройте периодическую сверку. Coinbase Commerce API позволяет получить статус Charge по его коду:
// Запускать раз в 5 минут для pending charges
async function syncPendingCharges() {
const pending = await db.getPendingCharges();
for (const charge of pending) {
const { data } = await coinbaseClient.get(`/charges/${charge.code}`);
const timeline = data.data.timeline;
const latestStatus = timeline[timeline.length - 1].status;
if (["CONFIRMED", "RESOLVED"].includes(latestStatus)) {
await orderService.markPaid(charge.orderId);
}
}
}
Что нужно реализовать
- Эндпоинт создания Charge и редирект на
hosted_url - Webhook handler с верификацией HMAC
- Сохранение
charge.codeв базе для reconciliation - Fallback polling для pending charges
- UI-страница ожидания с polling статуса (GET
/charges/:codeкаждые 10 сек)







