Рецепты интеграции

FazerCards API Cookbook

Готовые задачи и полные примеры решений на curl, Node.js, Python и Go. Каждый рецепт — постановка задачи, объяснение подхода и копируемый код, который работает с боевым FazerCards REST API.

quick-startorderidempotency

Оформите первый оптовый заказ

Задача

У вас есть API-ключ из кабинета FazerCards и нужно за пять минут пройти весь путь — выбрать товар, создать заказ, получить код.

Решение

Получите категории подарочных карт через `GET /giftcards`, затем офферы одной категории через `GET /giftcards/cards?category_id=…` — у каждого оффера есть `card_id`, `price_usd` и `stock`. Создайте заказ через `POST /giftcards/order` (`category_id`, `card_id`, `quantity`) с заголовком `Idempotency-Key`. Мгновенные товары возвращаются со статусом `completed` и кодами в `order.cards`; иначе заказ в статусе `processing`, и его результат читается позже через `GET /orders/{order_id}`. В проде polling заменяется webhooks (следующий рецепт).

curl
# 1. Gift card categories (cursor-paginated)
curl -s "https://api.fzr.cards/api/v2/giftcards?limit=50" \
  -H "X-Api-Key: $FAZER_API_KEY" | jq '.items[0]'

# 2. Offers of one category: card_id, price_usd, stock
curl -s "https://api.fzr.cards/api/v2/giftcards/cards?category_id=$CATEGORY_ID" \
  -H "X-Api-Key: $FAZER_API_KEY" | jq '.offers[0]'

# 3. Place the order
curl -s -X POST "https://api.fzr.cards/api/v2/giftcards/order" \
  -H "X-Api-Key: $FAZER_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d "{\"category_id\":\"$CATEGORY_ID\",\"card_id\":\"$CARD_ID\",\"quantity\":1}"

# 4. Order status + codes
curl -s "https://api.fzr.cards/api/v2/orders/$ORDER_ID" \
  -H "X-Api-Key: $FAZER_API_KEY" | jq '.order | {status, cards}'
webhookssigningevents

Обработка webhooks заказа (order.created / order.status_changed)

Задача

Не хочется опрашивать `GET /orders/{order_id}` каждые несколько секунд. Нужно чтобы FazerCards сам вызывал ваш сервер при смене статуса заказа — тогда Telegram-бот или витрина мгновенно передадут код покупателю.

Решение

Укажите HTTPS-эндпоинт в кабинете (или через `PUT /account/webhook`). FazerCards шлёт POST с JSON-конвертом — `event`, `event_id`, `timestamp`, `data` — для `order.created` и `order.status_changed`; в `data` лежат `order_id`, `type`, `status` и `previous_status`. Каждый запрос подписан: `X-Webhook-Signature: sha256=<hex HMAC-SHA256 от сырого тела с секретом webhook>`. Проверяйте подпись по сырым байтам до парсинга и отвечайте 2xx за несколько секунд. Кодов в теле намеренно нет — при `completed` забирайте их через `GET /orders/{order_id}` в фоновом джобе. Неудачные доставки повторяются через 1, 5 и 30 минут с тем же `event_id`, поэтому дедуплицируйте по нему.

node
import express from "express";
import crypto from "crypto";

const app = express();
const secret = process.env.FAZER_WEBHOOK_SECRET!;

app.post("/webhooks/fazercards", express.raw({ type: "application/json" }), (req, res) => {
  const received = Buffer.from(req.header("X-Webhook-Signature") ?? "");
  const expected = Buffer.from(
    "sha256=" + crypto.createHmac("sha256", secret).update(req.body).digest("hex")
  );
  if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) {
    return res.status(401).send("bad signature");
  }

  const { event, event_id, data } = JSON.parse(req.body.toString("utf8"));
  if (alreadyProcessed(event_id)) return res.sendStatus(200); // retries reuse event_id

  if (event === "order.status_changed") {
    switch (data.status) {
      case "completed":
        // worker: GET /orders/{order_id} → order.cards → customer
        enqueueDelivery(data.order_id);
        break;
      case "failed":
      case "refund":
        enqueueCustomerRefund(data.order_id, data.status);
        break;
    }
  }

  res.sendStatus(200);
});

app.listen(8080);
telegramtopupdirect-id

Продажа PUBG Mobile UC через Telegram-бот

Задача

Нужен Telegram-бот, который принимает PUBG ID игрока, берёт оплату, дёргает FazerCards для прямого пополнения этого аккаунта и отвечает подтверждением — полная автоматизация, доставка менее минуты.

Решение

PUBG UC — это прямое пополнение по ID, кода нет: FazerCards зачисляет валюту на аккаунт по идентификатору. Один раз найдите игру через `GET /topups` и её пакеты через `GET /topups/offers?category_id=…`: у каждого пакета есть `offer_id`, а массив `fields` перечисляет, что нужно игре (здесь `player_id`). Бот принимает в чате ID игрока и пакет, берёт оплату с покупателя (Telegram Stars, USDT, ваш локальный эквайринг — на ваш выбор), затем вызывает `POST /topups/order` с `category_id`, `offer_id` и `fields`. Сначала заказ обычно в статусе `processing`; отвечайте пользователю, когда webhook `order.status_changed` (предыдущий рецепт) сообщит `completed`. Если игра это поддерживает, `POST /topups/validate-id` проверит ID ещё до оплаты.

node
import { Bot } from "grammy";
import { randomUUID } from "crypto";

const bot = new Bot(process.env.BOT_TOKEN!);
const FAZER = "https://api.fzr.cards/api/v2";
const PUBG_CATEGORY_ID = process.env.PUBG_CATEGORY_ID!; // from GET /topups
const headers = {
  "X-Api-Key": process.env.FAZER_API_KEY!,
  "Content-Type": "application/json",
};

// /buy_uc <offer_id> <player_id>   (offer_id from GET /topups/offers)
bot.command("buy_uc", async (ctx) => {
  const [offerId, playerId] = ctx.match.split(" ");
  if (!offerId || !playerId) return ctx.reply("Usage: /buy_uc <offer_id> <player_id>");

  // (omitted: collect payment from customer first)

  const { order } = await fetch(`${FAZER}/topups/order`, {
    method: "POST",
    headers: { ...headers, "Idempotency-Key": randomUUID() },
    body: JSON.stringify({
      category_id: PUBG_CATEGORY_ID,
      offer_id: offerId,
      fields: { player_id: playerId },
    }),
  }).then(r => r.json());

  if (order.status === "completed") {
    await ctx.reply(`✓ ${order.offer_name} delivered to ${playerId}`);
  } else {
    await ctx.reply(`Order ${order.id} in progress — you'll get a confirmation in a minute.`);
    // The webhook handler (previous recipe) sends the confirmation
  }
});

bot.start();
idempotencyreliability

Используйте Idempotency-Key для безопасных повторов заказа

Задача

Сеть может дёргаться при `POST /giftcards/order`. Не хочется чтобы повтор создал второй заказ и списал с баланса дважды.

Решение

Генерируйте один UUID на логический заказ и шлите его в заголовке `Idempotency-Key` (до 255 видимых ASCII-символов). Если тот же ключ придёт снова — например, первый ответ потерялся, — FazerCards вернёт исходный заказ вместо повторного списания. Ключи привязаны к аккаунту и хранятся 7 дней; тот же ключ для другого типа заказа вернёт 409. Заголовок необязательный и работает на всех эндпоинтах заказа (`/giftcards/order`, `/topups/order`, `/gamekeys/order`, `/steam-topup/order`, `/steam-gifts/order`, `/manual-services/order`).

node
import { randomUUID } from "crypto";

async function placeOrderWithRetry(categoryId: string, cardId: string, qty: number) {
  const idempotencyKey = randomUUID(); // same key across retries
  const body = JSON.stringify({ category_id: categoryId, card_id: cardId, quantity: qty });

  for (let attempt = 0; attempt < 5; attempt++) {
    try {
      const res = await fetch("https://api.fzr.cards/api/v2/giftcards/order", {
        method: "POST",
        headers: {
          "X-Api-Key": process.env.FAZER_API_KEY!,
          "Idempotency-Key": idempotencyKey,
          "Content-Type": "application/json",
        },
        body,
      });
      if (res.status >= 500) throw new Error(`5xx ${res.status}`);
      return await res.json(); // same order on every successful retry
    } catch (err) {
      const wait = 2 ** attempt * 250;
      await new Promise(r => setTimeout(r, wait));
    }
  }
  throw new Error("all retries exhausted");
}
catalogpagination

Получите доступные SKU по категории и региону

Задача

Витрина продаёт подарочные карты. Нужно отрисовать прайс-лист по категориям — бренд и регион, например Steam USD, — с актуальными оптовыми ценами и остатками.

Решение

`GET /giftcards` отдаёт категории подарочных карт с курсорной пагинацией: передайте `limit` (по умолчанию 200, максимум 500) и идите по `meta.next_cursor`, пока `meta.has_more` равен true. Для каждой категории `GET /giftcards/cards?category_id=…` возвращает офферы с `card_id`, `name`, `price_usd`, `stock` и лимитами количества. Пополнения (`GET /topups` → `/topups/offers`) и ключи игр (`GET /gamekeys` → `/gamekeys/keys`) устроены так же, в два шага. Кешируйте результат у себя (типичный TTL 5–15 минут) — оптовые цены меняются достаточно медленно.

curl
# First page of categories
curl -s "https://api.fzr.cards/api/v2/giftcards?limit=200" \
  -H "X-Api-Key: $FAZER_API_KEY" | jq '{items: .items | length, meta}'

# Next page (cursor from meta.next_cursor)
curl -s "https://api.fzr.cards/api/v2/giftcards?limit=200&cursor=$NEXT_CURSOR" \
  -H "X-Api-Key: $FAZER_API_KEY"

# Offers and prices of one category
curl -s "https://api.fzr.cards/api/v2/giftcards/cards?category_id=$CATEGORY_ID" \
  -H "X-Api-Key: $FAZER_API_KEY" | jq '.offers[] | {card_id, name, price_usd, stock}'
balancemonitoring

Мониторинг баланса реселлера и алерт до его исчерпания

Задача

Если баланс FazerCards уходит в ноль в середине дня, заказы начинают отказываться и покупатели видят ошибки. Нужен фоновый джоб, который проверяет баланс раз в минуту и пингует в Telegram при падении ниже порога.

Решение

Опрашивайте `GET /balance` каждую минуту — он возвращает `balance` десятичной строкой и `currency: "USD"`, — сравнивайте с порогом (например, $100 от ожидаемого дневного объёма) и отправляйте сообщение в Telegram через Bot API при падении ниже него. Не отправляйте один и тот же алерт каждую минуту — храните last-alert-state в хранилище джоба.

node
import cron from "node-cron";

const THRESHOLD = 100; // USD
let lastAlertAt = 0;

cron.schedule("* * * * *", async () => {
  const res = await fetch("https://api.fzr.cards/api/v2/balance", {
    headers: { "X-Api-Key": process.env.FAZER_API_KEY! },
  });
  const balance = Number((await res.json()).balance); // "100.0000" → 100

  if (balance < THRESHOLD && Date.now() - lastAlertAt > 30 * 60 * 1000) {
    await fetch(`https://api.telegram.org/bot${process.env.BOT_TOKEN}/sendMessage`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        chat_id: process.env.ADMIN_CHAT_ID,
        text: `⚠️ FazerCards balance low: $${balance.toFixed(2)}`,
      }),
    });
    lastAlertAt = Date.now();
  }
});
rate-limitreliability

Корректно обрабатывайте HTTP 429 от лимитов

Задача

При всплесках трафика клиент периодически получает `HTTP 429 Too Many Requests`. FazerCards использует по-категорийные лимиты на API-ключ — чтение каталога 300/мин, создание заказов 60/мин, статус заказа 120/мин, чтение аккаунта 60/мин, — поэтому узкое место обычно создание заказов на пике промоакции или каталог, если его не кешировать. Нужно тормозить по подсказке сервера, а не ретраить в плотной петле.

Решение

FazerCards возвращает заголовок `Retry-After` (секунды) при 429. Спите минимум столько, потом повтор — лучше с небольшим джиттером (±15 %), чтобы воркеры не врезались в следующее окно лимита одновременно. Категории не делят бюджет, поэтому опрос статуса заказов никогда не блокирует их создание. При устойчивой нагрузке лучше throttle исходящих на своей стороне по категориям (кешируйте каталог 5–15 минут, батчите чтение баланса), а не полагайтесь на retry-loops.

node
async function callWith429Handling(input: string, init?: RequestInit) {
  for (let attempt = 0; attempt < 5; attempt++) {
    const res = await fetch(input, init);
    if (res.status !== 429) return res;

    const retryAfterSec = Number(res.headers.get("Retry-After") ?? 1);
    const jitter = Math.random() * 0.3 + 0.85; // 0.85x .. 1.15x
    await new Promise(r => setTimeout(r, retryAfterSec * 1000 * jitter));
  }
  throw new Error("rate-limited beyond retry budget");
}
migrationadapter

Миграция существующей интеграции с Reloadly / Bitrefill

Задача

Уже есть интеграция с другим reseller-API (Reloadly, Bitrefill, Codashop), и нужно переключить нижележащего поставщика на FazerCards с минимальной правкой кода приложения.

Решение

Соберите тонкий адаптер у себя, который наружу повторяет форму API, на которую уже завязано приложение, а внутри переводит вызовы в FazerCards. Большинство reseller-сценариев сводится к трём вызовам — каталог (`GET /giftcards/cards`), заказ (`POST /giftcards/order`), статус (`GET /orders/{order_id}`) — плюс приёмник webhook. Адаптер обычно 100-200 строк, после него можно менять поставщика без правок прикладного кода.

node
// Adapter that mimics a generic "ResellerSupplier" interface used by your app.
// Swap this for a Reloadly / Bitrefill implementation without touching callers.

import { randomUUID } from "crypto";

export class FazerCardsSupplier {
  private api = "https://api.fzr.cards/api/v2";
  constructor(private apiKey: string) {}

  private headers(extra: Record<string, string> = {}) {
    return { "X-Api-Key": this.apiKey, "Content-Type": "application/json", ...extra };
  }

  async listProducts(categoryId: string) {
    const url = new URL(`${this.api}/giftcards/cards`);
    url.searchParams.set("category_id", categoryId);
    const r = await fetch(url, { headers: this.headers() });
    return (await r.json()).offers; // [{ card_id, name, price_usd, stock, ... }]
  }

  async placeOrder(categoryId: string, cardId: string, qty: number) {
    const r = await fetch(`${this.api}/giftcards/order`, {
      method: "POST",
      headers: this.headers({ "Idempotency-Key": randomUUID() }),
      body: JSON.stringify({ category_id: categoryId, card_id: cardId, quantity: qty }),
    });
    return (await r.json()).order; // { id, status, cards, ... }
  }

  async getOrder(orderId: string) {
    const r = await fetch(`${this.api}/orders/${orderId}`, { headers: this.headers() });
    return (await r.json()).order;
  }
}

Полный справочник API — на странице /docs

Cookbook покрывает типовые сценарии. Полный список эндпойнтов, схем и форматов ошибок — в основной документации API + OpenAPI-спецификации.

Открыть API-документацию