Skip to content

For developersGuide

How to accept crypto payments in Node.js

Accept Bitcoin and USDT in a Node.js app: sign API requests with node:crypto, create a payment, redirect to the payment page and verify the webhook in Express.

A Node.js application accepts crypto with CryptoLike in three steps: sign each API request with node:crypto, create a payment for the order, and verify a signed webhook before marking the order paid. Node.js 18 or newer is enough — fetch and crypto are built in, so the integration has no dependencies beyond your web framework.

For a store on a CMS there is no code to write: see the WooCommerce plugin and the other integrations. This guide is for your own backend: a SaaS, a marketplace or an app.

What you need

A CryptoLike account, a server running Node.js 18+, and a public https:// URL for webhooks. Keep the secrets in the environment: the examples read CRYPTOLIKE_API_KEY_ID, CRYPTOLIKE_API_SECRET and CRYPTOLIKE_WEBHOOK_SECRET, plus CRYPTOLIKE_API_URL for the test API URL (unset means production). Build and test with test keys and test networks first.

1. Create a test API key

In the cabinet open Developers → API keys → Create key with the permissions create_invoice and read. A ck_test_ key works with test networks, where coins have no value. Test keys come from the test environment: CryptoLike support gives you access to its cabinet, where you create the same key, and its API URL (CRYPTOLIKE_API_URL in the examples). The production cabinet issues live keys only, and on mainnet a test key is refused with 401 auth.test_key. The secret is shown once; store it in your secret manager or environment, never in front-end code.

2. Sign API requests with node:crypto

The signature is an HMAC-SHA256 of four lines — the Unix timestamp in seconds, the method, the path with the query, and the SHA-256 of the body — keyed with the API secret. This helper signs and sends a request:

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;
}

Serialise the body once and send that same string: hashing one string and sending another is the most common cause of 401 auth.invalid_signature. Check the helper against the reference vector in API authentication.

3. Create the payment for an order

Create the payment on the server when the customer places the order. Price it in USD or EUR and let the customer choose the coin on the page, or pass a coin_id from GET /v1/currencies. Without coin_id the page lists the coins you offer for invoices, each with an approximate amount marked ≈; allowed_coin_ids (ids with invoice_enabled: true in the signed GET /v1/currencies) limits the list for this order. The order ID as the Idempotency-Key makes a retry return the same payment:

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);

Keep amounts as strings end to end — "49.90", never a JavaScript number.

4. Send the customer to the payment page

Redirect the browser to invoice.checkout_url, for example res.redirect(303, invoice.checkout_url) in Express. The payment page shows the amount, the network, the address with a QR code, the time left and the live status. To keep the customer in your own checkout, embed the page with the checkout widget; the browser only ever sees the invoice ID.

5. Verify the webhook in Express

Add a webhook endpoint in Developers → Webhooks with the invoice.* events. Read the body raw — express.json() would re-serialise it and break the signature — verify, deduplicate by event_id and answer quickly:

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);
});

Deliveries are at least once and can arrive out of order: trust the status inside data, not the order of arrival. The full procedure is in verify crypto payment webhooks.

6. Test on a test network, then go live

Create a payment with the test key and pay it with test coins, on the network the page names. You should see invoice.pending, then invoice.paid. Handle invoice.underpaid (the customer may top up to the same address until the deadline), invoice.expired (offer a new payment) and invoice.reverted (hold fulfilment). Then switch to a live key and follow the go-live checklist in the developer guide.

Common errors and fixes

Error What happened What to do
401 auth.invalid_signature The signed string differs from the request Hash the exact body string you send; use seconds, not Date.now() milliseconds
401 auth.replay Two identical requests in the same second Wait for the next second and sign again
401 on your webhook The body was parsed before verification Use express.raw() on the webhook route only
409 conflict The order already has a payment for another amount Use details.invoice_id; do not create a second payment
422 no_allowed_coins None of allowed_coin_ids is offered for invoices in this currency Send other ids, or omit the field to offer every coin
429 rate_limited Over the key's request limit (600 per minute by default) Wait Retry-After seconds

Prefer an SDK?

The official TypeScript SDK for Node.js 18+ wraps these calls with typed resources, retries on 429 and 5xx, an Idempotency-Key on every POST and verifyWebhook(). It has no runtime dependencies and refuses to run in a browser, so the secret cannot end up in front-end code.

FAQ

Can I create CryptoLike payments from the browser?

No. The API secret signs requests as your account, so it must stay on the server. Your front end asks your backend to create the payment and receives only the invoice ID or the checkout_url. The official TypeScript SDK throws an error when it detects a browser for this reason, and the checkout widget reads only public payment data.

Why does my webhook signature check fail in Express?

Almost always because express.json() parsed the body before the check: the signature covers the raw bytes, and re-serialised JSON differs. Mount express.raw({ type: "application/json" }) on the webhook route, verify first, then parse. Also check that the secret is the one of that endpoint and that your server clock is synced.

Do I need to poll the payment status?

No. Webhooks report every status change, signed and retried for about 31 hours if your server does not answer. Polling GET /v1/invoices/{id} every few seconds also works, for example on the page the customer returns to, but fulfil orders on the verified invoice.paid webhook, not on a browser event.

Terms used here

Read next

Start with a test payment

Create an account, ask support for the test environment and see a payment go from pending to paid on a test network.