# Как принимать криптовалюту в Node.js

> Приём Bitcoin и USDT в приложении на Node.js: подпись запросов через node:crypto, создание платежа, переход на страницу оплаты и проверка вебхука в Express.

Source: https://cryptolike.net/ru/guides/accept-crypto-payments-nodejs
Published: 2026-09-24 · Updated: 2026-10-04 · Author: CryptoLike team

Приложение на Node.js принимает криптовалюту через CryptoLike в три шага: подписать каждый запрос к API через `node:crypto`, создать платёж по заказу и проверить подписанный вебхук, прежде чем отметить заказ оплаченным. Хватает Node.js 18 и новее — `fetch` и `crypto` встроены, так что интеграции не нужны зависимости, кроме вашего веб-фреймворка.

Для магазина на CMS писать код не нужно: смотрите [плагин для WooCommerce](https://cryptolike.net/ru/integrations/woocommerce) и другие [интеграции](https://cryptolike.net/ru/integrations). Это руководство — для собственного бэкенда: SaaS, маркетплейса или приложения.

## Что понадобится

Аккаунт CryptoLike, сервер с Node.js 18+ и публичный адрес `https://` для вебхуков. Секреты держите в окружении: в примерах это `CRYPTOLIKE_API_KEY_ID`, `CRYPTOLIKE_API_SECRET` и `CRYPTOLIKE_WEBHOOK_SECRET`, а также `CRYPTOLIKE_API_URL` для тестового адреса API (без него — рабочий). Разрабатывайте и проверяйте сначала с тестовыми ключами в тестовых сетях.

## 1. Создайте тестовый API-ключ

В кабинете откройте **Разработчикам → API-ключи → Создать ключ** с правами `create_invoice` и `read`. Ключ `ck_test_` работает с тестовыми сетями, где монеты ничего не стоят. Тестовые ключи выдаёт тестовая среда: поддержка CryptoLike даст доступ к её кабинету, где вы создадите такой же ключ, и адрес её API (`CRYPTOLIKE_API_URL` в примерах). Рабочий кабинет выпускает только боевые ключи, а в основных сетях тестовый ключ получает `401 auth.test_key`. Секрет показывается один раз; храните его в менеджере секретов или окружении, но не во фронтенд-коде.

## 2. Подписывайте запросы через node:crypto

Подпись — это HMAC-SHA256 от четырёх строк: времени Unix в секундах, метода, пути с query и SHA-256 тела, с ключом — API-секретом. Этот помощник подписывает и отправляет запрос:

```ts
import { createHash, createHmac } from "node:crypto";

export async function cryptolike(method: string, path: string, payload?: unknown, idempotencyKey?: string) {
  const body = payload === undefined ? "" : JSON.stringify(payload);
  const ts = Math.floor(Date.now() / 1000).toString();
  const canonical = [ts, method.toUpperCase(), path, createHash("sha256").update(body).digest("hex")].join("\n");
  const res = await fetch(`${process.env.CRYPTOLIKE_API_URL ?? "https://api.cryptolike.net"}${path}`, {
    method,
    headers: {
      "X-API-Key": process.env.CRYPTOLIKE_API_KEY_ID!,
      "X-Timestamp": ts,
      "X-Signature": createHmac("sha256", process.env.CRYPTOLIKE_API_SECRET!).update(canonical).digest("hex"),
      "Content-Type": "application/json",
      ...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
    },
    body: body || undefined,
  });
  const data = await res.json();
  if (!res.ok) throw new Error(`${data.error?.code ?? res.status}: ${data.error?.message ?? ""}`);
  return data;
}
```

Сериализуйте тело один раз и отправляйте ту же строку: если хешировать одну строку, а отправить другую, получится `401 auth.invalid_signature`. Сверьте помощник с эталонным вектором из раздела [аутентификация API](https://cryptolike.net/ru/docs/auth).

## 3. Создайте платёж по заказу

Создавайте платёж на сервере, когда покупатель оформляет заказ. Укажите цену в USD или EUR и дайте выбрать монету на странице или передайте `coin_id` из `GET /v1/currencies`. Без `coin_id` страница покажет монеты, доступные для инвойсов, с ориентировочной суммой ≈ у каждой; `allowed_coin_ids` (id с `invoice_enabled: true` из подписанного `GET /v1/currencies`) ограничивает список для этого заказа. Номер заказа в `Idempotency-Key` вернёт тот же платёж при повторе:

```ts
const invoice = await cryptolike("POST", "/v1/invoices", {
  amount: "49.90",
  currency: "USD",
  order_id: order.id,
  idempotency_by_order_id: true,
  return_url: `https://example.com/orders/${order.id}`,
}, `order-${order.id}`);

await orders.saveInvoiceId(order.id, invoice.id);
```

Держите суммы строками на всём пути — `"49.90"`, а не числом JavaScript.

## 4. Отправьте покупателя на страницу оплаты

Перенаправьте браузер на `invoice.checkout_url`, например `res.redirect(303, invoice.checkout_url)` в Express. На странице оплаты видны сумма, сеть, адрес с QR-кодом, оставшееся время и статус в реальном времени. Чтобы покупатель остался в вашем оформлении заказа, встройте страницу [виджетом оплаты](https://cryptolike.net/ru/docs/widget); браузер видит только ID инвойса.

## 5. Проверьте вебхук в Express

Добавьте вебхук-эндпоинт в разделе **Разработчикам → Вебхуки** с событиями `invoice.*`. Читайте тело «сырым» — `express.json()` пересериализует его и сломает подпись, — проверяйте, отсеивайте повторы по `event_id` и отвечайте быстро:

```ts
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

app.post("/webhooks/cryptolike", express.raw({ type: "application/json" }), async (req, res) => {
  const ts = String(req.header("X-Timestamp") ?? "");
  const got = Buffer.from(String(req.header("X-Signature") ?? ""));
  const secret = process.env.CRYPTOLIKE_WEBHOOK_SECRET ?? "";
  const want = Buffer.from("sha256=" + createHmac("sha256", secret).update(`${ts}.`).update(req.body).digest("hex"));
  const fresh = /^\d+$/.test(ts) && Math.abs(Date.now() / 1000 - Number(ts)) <= 300;
  if (!secret || !fresh || got.length !== want.length || !timingSafeEqual(got, want)) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString("utf8"));
  if (await events.firstTime(event.event_id)) {
    if (event.type === "invoice.paid") await orders.markPaid(event.data.invoice.order_id);
  }
  res.sendStatus(200);
});
```

Доставка — «хотя бы один раз», и порядок может нарушаться: верьте статусу внутри `data`, а не порядку прихода. Полная процедура — в руководстве [по проверке подписи вебхуков](https://cryptolike.net/ru/guides/verify-crypto-payment-webhooks).

## 6. Проверьте в тестовой сети и запускайте

Создайте платёж тестовым ключом и оплатите его тестовыми монетами в той сети, что названа на странице. Придёт `invoice.pending`, затем `invoice.paid`. Обработайте `invoice.underpaid` (покупатель может доплатить на тот же адрес до срока), `invoice.expired` (предложите новый платёж) и `invoice.reverted` (приостановите выполнение заказа). Затем перейдите на рабочий ключ и пройдите чек-лист запуска из [руководства разработчика](https://cryptolike.net/ru/docs).

## Частые ошибки

| Ошибка | Что произошло | Что сделать |
|---|---|---|
| `401 auth.invalid_signature` | Подписанная строка не совпадает с запросом | Хешируйте ровно отправляемую строку тела; время — в секундах, а не миллисекунды `Date.now()` |
| `401 auth.replay` | Два одинаковых запроса в одну секунду | Дождитесь следующей секунды и подпишите заново |
| `401` на вашем вебхуке | Тело разобрано до проверки | Используйте `express.raw()` только на маршруте вебхука |
| `409 conflict` | У заказа уже есть платёж на другую сумму | Возьмите `details.invoice_id`; второй платёж не создавайте |
| `422 no_allowed_coins` | Ни одна монета из `allowed_coin_ids` не доступна для инвойсов в этой валюте | Передайте другие id или не передавайте поле, чтобы предложить все монеты |
| `429 rate_limited` | Превышен лимит запросов ключа (по умолчанию 600 в минуту) | Подождите `Retry-After` секунд |

## Удобнее с SDK?

Официальный [TypeScript SDK](https://cryptolike.net/ru/integrations/sdk-typescript) для Node.js 18+ оборачивает эти вызовы: типизированные ресурсы, повторы при `429` и `5xx`, `Idempotency-Key` на каждом `POST` и `verifyWebhook()`. У него нет зависимостей, и он отказывается работать в браузере — секрет не попадёт во фронтенд.

## Вопросы и ответы

### Можно ли создавать платежи CryptoLike из браузера?

Нет. API-секрет подписывает запросы от имени вашего аккаунта, поэтому он должен оставаться на сервере. Фронтенд просит бэкенд создать платёж и получает только ID инвойса или `checkout_url`. Поэтому официальный TypeScript SDK выдаёт ошибку, если обнаружит браузер, а виджет оплаты читает только публичные данные платежа.

### Почему в Express не проходит проверка подписи вебхука?

Почти всегда потому, что `express.json()` разобрал тело до проверки: подпись считается по сырым байтам, а пересериализованный JSON отличается. Подключите `express.raw({ type: "application/json" })` на маршруте вебхука, сначала проверяйте, потом разбирайте. Ещё проверьте, что секрет — от этого эндпоинта, а часы сервера синхронизированы.

### Нужно ли опрашивать статус платежа?

Нет. Вебхуки сообщают о каждой смене статуса, подписаны и повторяются примерно 31 час, если сервер не ответил. Опрос `GET /v1/invoices/{id}` раз в несколько секунд тоже работает, например на странице, куда возвращается покупатель, но выполняйте заказ по проверенному вебхуку `invoice.paid`, а не по событию в браузере.
