Перейти к содержанию

Примеры: Node.js

Рабочий код на Node.js 18+ с нативным fetch и crypto. Без зависимостей.

Клиент X-Hub

// xhub-client.js
import crypto from 'crypto';

const BASE_URL = 'https://api.x-hub.online/api/v1';

export async function createPayment({ amountRub, externalId, description, metadata }) {
  const res = await fetch(`${BASE_URL}/payments`, {
    method: 'POST',
    headers: {
      'X-Api-Key': process.env.XHUB_API_KEY,
      'X-Api-Secret': process.env.XHUB_API_SECRET,
      'Idempotency-Key': crypto.randomUUID(),
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      amount_rub: amountRub,
      external_id: externalId,
      payment_method: 'sbp',
      description,
      metadata,
    }),
  });

  if (!res.ok) {
    const err = await res.json();
    throw new Error(`X-Hub ${res.status}: ${err.error?.code}: ${err.error?.message}`);
  }

  return res.json();
}

export async function getPayment(id) {
  const res = await fetch(`${BASE_URL}/payments/${id}`, {
    headers: {
      'X-Api-Key': process.env.XHUB_API_KEY,
      'X-Api-Secret': process.env.XHUB_API_SECRET,
    },
  });
  if (!res.ok) throw new Error(`X-Hub ${res.status}`);
  return res.json();
}

export async function getBalance() {
  const res = await fetch(`${BASE_URL}/balance`, {
    headers: {
      'X-Api-Key': process.env.XHUB_API_KEY,
      'X-Api-Secret': process.env.XHUB_API_SECRET,
    },
  });
  return res.json();
}

Webhook receiver (Express)

// webhook-server.js
import express from 'express';
import crypto from 'crypto';
import { grantAccess } from './business-logic.js';

const app = express();
const processedWebhooks = new Set(); // в проде — Redis/DB

app.post(
  '/webhook/xhub',
  express.raw({ type: 'application/json', limit: '100kb' }),
  async (req, res) => {
    try {
      const signature = req.headers['x-webhook-signature'];
      const timestamp = req.headers['x-webhook-timestamp'];

      // Подпись: HMAC-SHA256(timestamp + "." + body, secret)
      const signatureData = timestamp + '.' + req.body.toString('utf-8');
      const expected =
        'sha256=' +
        crypto
          .createHmac('sha256', process.env.XHUB_WEBHOOK_SECRET)
          .update(signatureData)
          .digest('hex');

      if (
        !signature ||
        signature.length !== expected.length ||
        !crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
      ) {
        return res.status(401).send('Invalid signature');
      }

      const event = JSON.parse(req.body.toString('utf-8'));
      const dedupeKey = `${event.payment_id || event.withdrawal_id}:${event.status}`;

      if (processedWebhooks.has(dedupeKey)) {
        return res.status(200).send('Already processed');
      }
      processedWebhooks.add(dedupeKey);

      if (event.event === 'payment.status_changed') {
        if (event.status === 'paid') {
          await grantAccess(event.external_id, event.metadata);
        }
      }

      res.status(200).send('OK');
    } catch (err) {
      console.error('Webhook error:', err);
      res.status(500).send('Internal error');
    }
  },
);

app.listen(8080);