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

FazerCards API Cookbook

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

quick-startorderidempotency

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

Задача

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

Решение

Запросите каталог через `GET /catalog`, выберите любой товар (Amazon $10 — типичная первая позиция), затем `POST /order` с id SKU и заголовком `Idempotency-Key`. Ответ или сразу содержит код (для карточных SKU), или возвращает id заказа, который проверяется через `GET /order/{id}` до статуса `completed`. В проде polling заменяется webhooks (следующий рецепт).

curl
# 1. Browse the catalog
curl -s "https://api.fzr.cards/api/v2/catalog" \
  -H "X-Api-Key: $FAZER_API_KEY" | jq '.items[0]'

# 2. Place an order (Amazon $10, US storefront)
curl -s -X POST "https://api.fzr.cards/api/v2/order" \
  -H "X-Api-Key: $FAZER_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"sku_id":"amazon-us-10","quantity":1}'

# 3. Retrieve order status + code
curl -s "https://api.fzr.cards/api/v2/order/<ORDER_ID>" \
  -H "X-Api-Key: $FAZER_API_KEY" | jq
webhookssigningevents

Обработка webhooks заказа (order.completed / failed / refunded)

Задача

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

Решение

Зарегистрируйте HTTPS-эндпойнт в кабинете в разделе Настройки → Webhooks. FazerCards шлёт POST с JSON-телом, типом события (`order.completed`, `order.failed`, `order.refunded`), id заказа и подписанным заголовком `X-FazerCards-Signature` (HMAC-SHA256 от сырого тела с секретом webhook). Проверяйте подпись до обработки и отвечайте 2xx за несколько секунд; долгие операции (запись в БД, уведомление покупателя) выносите в фоновую очередь.

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 signature = req.header("X-FazerCards-Signature") ?? "";
  const expected = crypto.createHmac("sha256", secret).update(req.body).digest("hex");
  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    return res.status(401).send("bad signature");
  }

  const event = JSON.parse(req.body.toString("utf8"));

  switch (event.type) {
    case "order.completed":
      // event.order.id, event.order.code → deliver to customer
      enqueueDelivery(event.order);
      break;
    case "order.failed":
      enqueueRetry(event.order, event.reason);
      break;
    case "order.refunded":
      enqueueCustomerRefund(event.order);
      break;
  }

  res.sendStatus(200);
});

app.listen(8080);
telegramtopupdirect-id

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

Задача

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

Решение

PUBG UC — это прямое пополнение по ID, кода нет: FazerCards зачисляет валюту на аккаунт по идентификатору. Бот принимает в чате PUBG ID и количество UC, берёт оплату с покупателя (Telegram Stars, USDT, ваш локальный эквайринг — на ваш выбор), отправляет POST с `player_id` в metadata, и либо ждёт синхронный ответ, либо дожидается webhook `order.completed` прежде чем ответить пользователю.

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 headers = {
  "X-Api-Key": process.env.FAZER_API_KEY!,
  "Content-Type": "application/json",
};

// /buy_uc 60 5123456789
bot.command("buy_uc", async (ctx) => {
  const [ucAmount, playerId] = ctx.match.split(" ");
  if (!ucAmount || !playerId) return ctx.reply("Usage: /buy_uc <amount> <player_id>");

  // (omitted: collect payment from customer first)

  const order = await fetch(`${FAZER}/order`, {
    method: "POST",
    headers: { ...headers, "Idempotency-Key": randomUUID() },
    body: JSON.stringify({
      sku_id: `pubg-uc-${ucAmount}`,
      quantity: 1,
      metadata: { player_id: playerId },
    }),
  }).then(r => r.json());

  if (order.status === "completed") {
    await ctx.reply(`✓ ${ucAmount} UC 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 /order

Задача

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

Решение

На своей стороне генерируйте свежий UUID на каждый логический заказ и шлите его в заголовке `Idempotency-Key`. Если тот же ключ приходит дважды (например, первый ответ потерялся), FazerCards возвращает результат первой операции — дубля нет. Использовать тот же ключ с другим телом — ошибка. Храните ключ как минимум столько, сколько готовы повторять при сбое (нескольких минут хватит; FazerCards хранит соответствие ключ→результат больше 24 часов).

node
import { randomUUID } from "crypto";

async function placeOrderWithRetry(sku: string, qty: number) {
  const idempotencyKey = randomUUID();
  const body = JSON.stringify({ sku_id: sku, quantity: qty });

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

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

Задача

Витрина продаёт подарочные карты. Нужно отрисовать прайс-лист, сгруппированный по бренду (Amazon, Steam, PSN, …) и региону (US, UK, TR, …).

Решение

`GET /catalog` возвращает полный каталог с категориями, брендами, регионами, номиналами и оптовыми ценами. Ответ пагинирован — используйте `cursor` query-параметр для прохода по страницам. Кешируйте ответ у себя (типичный TTL 5-15 минут) — оптовые цены меняются достаточно медленно.

curl
# First page
curl -s "https://api.fzr.cards/api/v2/catalog?category=gift-cards&limit=100" \
  -H "X-Api-Key: $FAZER_API_KEY" | jq

# Next page (use cursor from previous response)
curl -s "https://api.fzr.cards/api/v2/catalog?category=gift-cards&limit=100&cursor=eyJpZCI6IjAwYWE..." \
  -H "X-Api-Key: $FAZER_API_KEY"
balancemonitoring

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

Задача

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

Решение

Опрашивайте `GET /balance` каждую минуту, сравнивайте с порогом (например, $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_usd } = await res.json();

  if (balance_usd < 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_usd.toFixed(2)}`,
      }),
    });
    lastAlertAt = Date.now();
  }
});
rate-limitreliability

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

Задача

При всплесках трафика клиент периодически получает `HTTP 429 Too Many Requests`. FazerCards использует по-категорийные лимиты — обычно узкое место это каталог (30/мин) если не кешировать локально, или создание заказа (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-сценариев сводится к трём вызовам — каталог, заказ, статус — плюс приёмник 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(category: string) {
    const r = await fetch(`${this.api}/catalog?category=${category}`, { headers: this.headers() });
    return (await r.json()).items;
  }

  async placeOrder(sku: string, qty: number, metadata: Record<string, string> = {}) {
    const r = await fetch(`${this.api}/order`, {
      method: "POST",
      headers: this.headers({ "Idempotency-Key": randomUUID() }),
      body: JSON.stringify({ sku_id: sku, quantity: qty, metadata }),
    });
    return r.json();
  }

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

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

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

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