Integration recipes

FazerCards API Cookbook

Concrete problems with full solutions in curl, Node.js, Python and Go. Each recipe has the problem statement, the approach narrative, and copyable code that works against the live FazerCards REST API.

quick-startorderidempotency

Place your first wholesale order

Problem

You have an API key from the FazerCards panel and want to confirm the full path — pick a product, place an order, retrieve the code — in under five minutes.

Solution

List gift card categories with `GET /giftcards`, then the offers of one category with `GET /giftcards/cards?category_id=…` — each offer has a `card_id`, `price_usd` and `stock`. Place the order with `POST /giftcards/order` (`category_id`, `card_id`, `quantity`) and an `Idempotency-Key` header. Instant products come back with status `completed` and the codes in `order.cards`; otherwise the order is `processing` and you read it later with `GET /orders/{order_id}`. Webhooks (next recipe) replace polling in production.

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

Handle order webhooks (order.created / order.status_changed)

Problem

You don't want to poll `GET /orders/{order_id}` every few seconds. You want FazerCards to call your server when an order changes status, so your Telegram bot or storefront can deliver the code to the customer instantly.

Solution

Set your HTTPS endpoint in the reseller panel (or via `PUT /account/webhook`). FazerCards POSTs a JSON envelope — `event`, `event_id`, `timestamp`, `data` — for `order.created` and `order.status_changed`; `data` carries `order_id`, `type`, `status` and `previous_status`. Every request is signed: `X-Webhook-Signature: sha256=<hex HMAC-SHA256 of the raw body, keyed with your webhook secret>`. Verify it on the raw bytes before parsing and respond 2xx within a few seconds. The body deliberately carries no codes — on `completed`, fetch them with `GET /orders/{order_id}` from a background job. Failed deliveries are retried after 1, 5 and 30 minutes with the same `event_id`, so de-duplicate on it.

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

Sell PUBG Mobile UC through a Telegram bot

Problem

You want a Telegram bot that accepts a player's PUBG ID, takes payment, calls FazerCards to top up that account directly, and replies with confirmation — fully automated, sub-minute fulfillment.

Solution

PUBG UC is a direct-ID top-up — there is no code, FazerCards credits the player by ID. Look up the game once with `GET /topups` and its packs with `GET /topups/offers?category_id=…`: each pack has an `offer_id`, and the `fields` array lists what the game needs (here `player_id`). The bot collects the player ID and pack in chat, charges the customer (Telegram Stars, USDT, your local rail — your choice), then calls `POST /topups/order` with `category_id`, `offer_id` and `fields`. The order is usually `processing` at first; reply once the `order.status_changed` webhook (previous recipe) reports `completed`. Where the game supports it, `POST /topups/validate-id` can check the ID before you take payment.

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

Use Idempotency-Key to make order POSTs safe to retry

Problem

Your network can flake on `POST /giftcards/order`. You don't want a retry to create a second order and double-charge your balance.

Solution

Generate one UUID per logical order on your side and send it in the `Idempotency-Key` header (up to 255 visible ASCII characters). If the same key arrives again — because the first response was lost — FazerCards returns the original order instead of charging again. Keys are scoped to your account and kept for 7 days; reusing a key for a different order type returns 409. The header is opt-in and works on every order endpoint (`/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

Discover available SKUs by category and region

Problem

Your storefront sells gift cards. You want to render a price list grouped by category — brand and region, such as Steam USD — with current wholesale prices and stock.

Solution

`GET /giftcards` lists gift card categories with cursor pagination: pass `limit` (default 200, max 500) and follow `meta.next_cursor` while `meta.has_more` is true. For each category, `GET /giftcards/cards?category_id=…` returns its offers with `card_id`, `name`, `price_usd`, `stock` and order-quantity limits. Top-ups (`GET /topups` → `/topups/offers`) and game keys (`GET /gamekeys` → `/gamekeys/keys`) follow the same two-step shape. Cache the result on your side (5–15 min TTL is typical) — wholesale prices move slowly enough.

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

Monitor reseller balance and alert before it runs out

Problem

If your FazerCards balance hits zero mid-day, orders start failing and your customers see errors. You want a background job that checks balance every minute and pings you on Telegram below a threshold.

Solution

Poll `GET /balance` on a one-minute schedule — it returns `balance` as a decimal string plus `currency: "USD"` — compare against your threshold (e.g. $100 of expected daily volume), and send a Telegram message via the Bot API if you drop under it. Don't re-send the same alert every minute — track last-alert state in your job's storage.

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

Handle HTTP 429 rate-limit responses correctly

Problem

On bursty traffic your client occasionally gets `HTTP 429 Too Many Requests`. FazerCards uses per-category limits per API key — catalog reads 300/min, order creation 60/min, order status 120/min, account reads 60/min — so the usual bottleneck is order creation on a promotion peak, or the catalog if you do not cache locally. You want to slow down per the response, not just hammer in a tight loop.

Solution

FazerCards returns a `Retry-After` header (seconds) on 429. Sleep for at least that long, then retry — preferably with a small jitter (±15 %) so multiple workers don't desync into the next limit window together. Categories don't share budget, so polling order status never blocks placing orders. For sustained load, throttle outbound calls on your side per category (cache the catalog for 5–15 min, batch balance/account reads) instead of relying on 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

Migrate an existing integration from Reloadly / Bitrefill

Problem

You already integrated against another reseller API (Reloadly, Bitrefill, Codashop) and want to switch the underlying supplier to FazerCards with minimal application-code changes.

Solution

Build a thin adapter on your side that exposes the API shape your app already uses, and translates to FazerCards underneath. Most reseller flows boil down to three calls — catalog (`GET /giftcards/cards`), order (`POST /giftcards/order`), status (`GET /orders/{order_id}`) — plus a webhook receiver. The adapter is usually 100–200 lines and lets you swap suppliers without touching app code.

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;
  }
}

Full API reference is on /docs

The cookbook covers typical workflows. The full list of endpoints, schemas and error shapes is in the main API docs + OpenAPI spec.

Open API docs