Примеры: 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);