Почему вебхуки лучше поллинга
Опрос эндпоинта /orders/:id каждые несколько секунд для проверки завершения заказа создаёт ненужную нагрузку на API и вносит задержки. Вебхуки меняют эту логику: вместо того чтобы ваша система спрашивала «уже готово?», FoxReload уведомляет ваш сервер в момент изменения статуса заказа.
Для продуктов с мгновенной доставкой, таких как подарочные карты Google Play, вебхук срабатывает в течение 500 мс после размещения заказа. Для товаров с отложенной доставкой (редкость для стандартных подарочных карт) вебхуки ещё более критичны.
Шаг 1 — Создание эндпоинта вебхука
Ваш эндпоинт вебхука должен:
- Быть публично доступен по HTTPS
- Отвечать HTTP 200 в течение 5 секунд
- Быть идемпотентным (безопасно вызывать несколько раз с одним payload)
// Обработчик вебхука Express.js
const express = require("express");
const crypto = require("crypto");
const app = express();
// ВАЖНО: Используйте raw body parser для вебхука — проверка подписи требует сырые байты
app.post("/webhooks/foxreload",
express.raw({ type: "application/json" }),
async (req, res) => {
// 1. Сначала проверяем подпись
const isValid = verifyWebhookSignature(req);
if (!isValid) {
return res.status(401).json({ error: "Invalid signature" });
}
// 2. Парсим payload
const event = JSON.parse(req.body.toString());
// 3. Подтверждаем немедленно
res.status(200).json({ received: true });
// 4. Обрабатываем асинхронно
processWebhookEvent(event).catch(console.error);
}
);
Отвечайте 200 немедленно, до какой-либо обработки. Если ваш обработчик занимает слишком долго и истекает по таймауту, FoxReload повторит попытку — вы можете обработать одно событие дважды.
Шаг 2 — Проверка подписей вебхуков
Каждый вебхук от FoxReload содержит заголовок X-FoxReload-Signature. Всегда проверяйте его до доверия payload.
const WEBHOOK_SECRET = process.env.FOXRELOAD_WEBHOOK_SECRET;
function verifyWebhookSignature(req) {
const signature = req.headers["x-foxreload-signature"];
if (!signature) return false;
const [timestampPart, hashPart] = signature.split(",");
const timestamp = timestampPart.replace("t=", "");
const receivedHash = hashPart.replace("v1=", "");
// Отклоняем вебхуки старше 5 минут (защита от replay-атак)
const webhookAge = Date.now() / 1000 - parseInt(timestamp);
if (webhookAge > 300) return false;
// Вычисляем ожидаемую подпись
const payload = `${timestamp}.${req.body.toString()}`;
const expectedHash = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(payload)
.digest("hex");
// Сравнение с постоянным временем для защиты от timing-атак
return crypto.timingSafeEqual(
Buffer.from(receivedHash, "hex"),
Buffer.from(expectedHash, "hex")
);
}
Шаг 3 — Типы событий вебхуков
| Событие | Описание |
|---|---|
order.completed |
Заказ выполнен, коды доступны |
order.failed |
Заказ не удалось выполнить |
order.refunded |
Заказ возвращён поставщиком |
stock.low |
Запас товара ниже порога |
stock.out |
Товар закончился |
price.updated |
Изменилась оптовая цена на товар |
Шаг 4 — Обработка событий заказов
async function processWebhookEvent(event) {
console.log(`Обработка вебхука: ${event.type} для ${event.data.order_id}`);
switch (event.type) {
case "order.completed":
await handleOrderCompleted(event.data);
break;
case "order.failed":
await handleOrderFailed(event.data);
break;
case "stock.out":
await handleStockOut(event.data);
break;
case "price.updated":
await handlePriceUpdate(event.data);
break;
default:
console.warn(`Неизвестный тип события: ${event.type}`);
}
}
Обработка order.completed
async function handleOrderCompleted(data) {
const { order_id, external_order_id, codes } = data;
const code = codes[0].code;
// Сохраняем код в базе данных
await db.orders.update(
{ external_id: external_order_id },
{ code, status: "fulfilled", fulfilled_at: new Date() }
);
// Доставляем покупателю
const order = await db.orders.findOne({ external_id: external_order_id });
await displayCodeToCustomer(order.user_id, code);
await sendCodeEmail(order.user_email, code, order.product_name);
console.log(`Заказ ${external_order_id} выполнен.`);
}
Обработка order.failed
async function handleOrderFailed(data) {
const { external_order_id, reason } = data;
// Обновляем статус заказа
await db.orders.update(
{ external_id: external_order_id },
{ status: "failed", failed_reason: reason }
);
// Инициируем возврат
await issueCustomerRefund(external_order_id);
// Уведомляем покупателя
await sendFailureEmail(external_order_id);
// Оповещаем команду операций
await sendSlackAlert(`Ошибка заказа: ${external_order_id} — ${reason}`);
}
Шаг 5 — Регистрация URL вебхука
Зарегистрируйте эндпоинт в дашборде FoxReload или через API:
POST /api/v1/webhooks
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY
{
"url": "https://yourshop.com/webhooks/foxreload",
"events": ["order.completed", "order.failed", "stock.out", "price.updated"],
"active": true
}
Ответ:
{
"webhook_id": "wh-abc123",
"url": "https://yourshop.com/webhooks/foxreload",
"secret": "whsec_xxxxxxxxxxxxxxxx",
"events": ["order.completed", "order.failed", "stock.out", "price.updated"],
"active": true
}
Сохраните secret как FOXRELOAD_WEBHOOK_SECRET в вашем окружении. Он отображается только один раз.
Шаг 6 — Идемпотентная обработка событий
FoxReload может доставить один вебхук несколько раз (повторные попытки после таймаутов). Используйте event_id для дедупликации:
async function processWebhookEvent(event) {
// Проверяем, обработано ли уже
const existing = await db.webhookEvents.findOne({ event_id: event.id });
if (existing) {
console.log(`Дублирующий вебхук ${event.id} — пропускаем.`);
return;
}
// Отмечаем как обработанное
await db.webhookEvents.create({
event_id: event.id,
type: event.type,
received_at: new Date()
});
// Обрабатываем событие
await dispatch(event);
}
Шаг 7 — Локальное тестирование вебхуков
Используйте туннель для открытия локального сервера во время разработки:
# С помощью ngrok
ngrok http 3000
# Ваш URL вебхука во время разработки:
# https://abc123.ngrok.io/webhooks/foxreload
Зарегистрируйте этот временный URL в sandbox-дашборде FoxReload при тестировании.
Чек-лист надёжности вебхуков
- Эндпоинт отвечает 200 в течение 5 секунд
- Проверка подписи реализована и применяется
- Валидация временной метки предотвращает replay-атаки (5-минутное окно)
- Обработка событий идемпотентна (безопасна при двойной обработке)
- События вебхуков хранятся в базе данных для аудита
- Dead-letter queue для событий, которые не удалось обработать
- Мониторинг-оповещение, если вебхуки не поступают более 30 минут
Смотрите также
- Как настроить мгновенную доставку подарочных карт Google Play
- API для продажи подарочных карт Google Play: ключевые точки интеграции
- Как управлять инвентарём кодов Google Play
Доставка в реальном времени начинается с вебхуков. Зарегистрируйте эндпоинт на платформе FoxReload и устраните поллинг из вашей интеграции.

