Item types

Keys, top-ups and services — pricing, rate math and Python recipes for each type

View as Markdown

Every catalog item carries a product_typekey, topup, or service — that determines how the item is priced, what your POST /v1/orders request must contain, and what fulfillment delivers. This page is the deep dive on each type. The records themselves come from your catalog snapshot; the request/response mechanics live in Placing orders.

Product typeWhat it isquantityfieldsYou receive
keyGift-card codes / license keysInteger count, default 1Not usedCode strings in the order’s codes
topupBalance/currency top-upsDecimal amount, requiredRequiredBalance credited to the account named in fields
serviceManual/game servicesOmit (always 1)RequiredThe service performed on the account in fields

Pricing fields (all types)

Catalog prices are integers in cents — divide by 100 for major units. Every record carries three of them:

FieldWhat it means
priceWhat you pay per unit of quantity, in cents. This is the price used to charge your balance.
base_priceThe original/list price. When it is greater than price, the item is discounted — show base_price struck through next to price.
subscriber_priceThe per-unit price when your account has an active subscription (has_active_subscription on GET /v1/account/me).

The charge for an order is quantity × price (in cents). The order response confirms the actual amount charged in its price field, already converted to major units — e.g. a catalog price of 2400 and quantity: 2 comes back as "price": 48.00.

Pick the effective price and estimate a charge (Python)
1def effective_price_cents(item: dict, has_active_subscription: bool) -> int:
2 """The per-unit price your balance will actually be charged, in cents."""
3 if has_active_subscription:
4 return item["subscriber_price"]
5 return item["price"]
6
7
8def estimate_charge(item: dict, quantity: float, has_active_subscription: bool = False) -> float:
9 """Estimated charge for an order, in major units (e.g. dollars)."""
10 return quantity * effective_price_cents(item, has_active_subscription) / 100
11
12
13# `item` is a record read from the catalog snapshot (see the Catalog guide)
14print(f"1 unit costs {item['price'] / 100:.2f}")
15print(f"5 units cost {estimate_charge(item, 5):.2f}")

Estimates are for display and pre-checks only. The final charge is always the price on the order response — the catalog is a snapshot and prices can change between downloads.

Key items

A key item is a stock of pre-generated codes: gift cards, license keys, vouchers. You buy whole codes, so quantity is an integer count of how many codes you want (default 1), bounded by the record’s min_quantity/max_quantity. Key items take no fields.

  • Chargequantity × price cents.
  • Delivery — the order settles completed with the code strings in codes, and delivered_quantity tells you how many were delivered.
  • Partial delivery — if stock runs out mid-order, the order settles partial: codes holds what was delivered and refund_amount covers the missing units. Always reconcile delivered_quantity against what you asked for, not just the status.
Buy N codes (Python)
1import requests
2
3BASE = "https://api.voodoo.center"
4headers = {"Authorization": f"Bearer {access_token}"}
5
6# `item` is a `key` record from the catalog, e.g. Amazon Gift Card 25 TRY
7wanted = 3
8assert item["product_type"] == "key" and item["in_stock"]
9assert item["min_quantity"] <= wanted <= item["max_quantity"]
10print(f"Expected charge: {wanted * item['price'] / 100:.2f}")
11
12order = requests.post(f"{BASE}/v1/orders", headers=headers, json={
13 "item_id": item["id"],
14 "quantity": wanted, # integer count of codes
15 "merchant_order_id": "keys-2026-0001", # your idempotency key
16}).json()
17print(order["id"], order["status"]) # ... pending — the codes arrive
18 # on your webhook

The codes are pushed to your webhook when the order settles — no polling loop needed. Your handler is where you store the delivered codes and reconcile a partial delivery:

Collect the codes on your webhook (Python / FastAPI)
1@app.post("/webhooks/voodoo-center")
2async def voodoo_webhook(request: Request):
3 # 1. Verify X-Signature against the RAW body, and dedupe on order_id —
4 # see the Webhooks guide for the full receiver.
5 event = await request.json()
6
7 # 2. Correlate via merchant_order_id and collect the codes.
8 if event["codes"]: # `completed` or `partial`
9 store_codes(event["merchant_order_id"], event["codes"])
10 if event["delivered_quantity"] < event["quantity"]:
11 # `partial` (or `failed`): the undelivered codes were refunded
12 record_refund(event["merchant_order_id"], event["refund_amount"])
13
14 return {"ok": True} # ack fast; heavy work async

Polling GET /v1/orders/{id} remains a fallback for missed deliveries — see Webhook vs polling.

Top-up items

A topup item credits balance or in-game currency to an account your customer names in fields (a Steam login, a player id, …). Unlike keys, you are not buying discrete units of stock — you are buying an amount, so quantity is a decimal and is required.

How price and amount_per_price work together

A top-up record sells in units of quantity, and two fields define what one unit of quantity means:

  • price — what one unit of quantity costs you, in cents.
  • amount_per_price — what one unit of quantity delivers to the customer, in the target currency (USD of Steam balance, UC, diamonds, …).

So for any order:

charge (cents) = quantity × price
delivered (units) = quantity × amount_per_price

Calculating the rate

The rate — how much target currency $1 buys — is the ratio of the two fields. Because price is in cents, multiply by 100 to express it per dollar:

rate = (amount_per_price / price) × 100 # units delivered per $1

Worked example — a game-currency top-up record with price: 100 (i.e. $1.00 per unit of quantity) and amount_per_price: 110:

QuestionCalculationResult
Rate per $1(110 / 100) × 100110 UC per $1
Charge for quantity: 55 × 100 / 100$5.00
Delivered for quantity: 55 × 110550 UC
Quantity to deliver 1 100 UC1100 / 110quantity: 10 → $10.00

If your account has an active subscription you are charged subscriber_price instead, which improves the rate: (amount_per_price / subscriber_price) × 100.

Rate, charge and quantity helpers (Python)
1def rate_per_dollar(item: dict, price_cents: int | None = None) -> float:
2 """How much target currency $1 buys. Pass subscriber_price to see your
3 subscription rate."""
4 price = price_cents if price_cents is not None else item["price"]
5 return item["amount_per_price"] / price * 100
6
7
8def charge_for(item: dict, quantity: float) -> float:
9 """What an order of `quantity` costs, in major units."""
10 return quantity * item["price"] / 100
11
12
13def delivered_for(item: dict, quantity: float) -> float:
14 """How much target currency an order of `quantity` delivers."""
15 return quantity * item["amount_per_price"]
16
17
18def quantity_for(item: dict, target_amount: float) -> float:
19 """The `quantity` to send so the customer receives `target_amount` units.
20 Raises if the item can't deliver that amount in one order."""
21 quantity = target_amount / item["amount_per_price"]
22 if not item["min_quantity"] <= quantity <= item["max_quantity"]:
23 raise ValueError(
24 f"quantity {quantity} outside "
25 f"{item['min_quantity']}..{item['max_quantity']}"
26 )
27 return quantity
28
29
30# item = {"price": 100, "amount_per_price": 110, "min_quantity": 1, "max_quantity": 500, ...}
31print(f"Rate: {rate_per_dollar(item):g} units per $1") # 110
32print(f"550 units -> quantity {quantity_for(item, 550):g}, " # 5
33 f"charge {charge_for(item, quantity_for(item, 550)):.2f}")

Order a target amount end to end

Putting it together: the customer wants a specific amount topped up, you derive quantity from the catalog record and place the order:

Top up an exact amount (Python)
1import requests
2
3BASE = "https://api.voodoo.center"
4headers = {"Authorization": f"Bearer {access_token}"}
5
6# `item` is a `topup` record from the catalog, e.g. Steam Top-up (USD)
7target_amount = 20 # what the customer should receive
8
9quantity = quantity_for(item, target_amount)
10print(f"Charging {charge_for(item, quantity):.2f} "
11 f"to deliver {delivered_for(item, quantity):g} units")
12
13order = requests.post(f"{BASE}/v1/orders", headers=headers, json={
14 "item_id": item["id"],
15 "quantity": quantity, # decimal, required for topup
16 "merchant_order_id": "topup-2026-0001",
17 "fields": {
18 "steam_login": "customer_login", # the item's required fields,
19 }, # keyed by field *name*
20}).json()
21print(order["id"], order["status"], order["price"]) # charge in major units

Find the best rate in your catalog

Because the whole catalog is a local LMDB file, comparing rates across every top-up is a plain loop — no API calls:

Rank in-stock top-ups by rate (Python)
1import json
2
3import lmdb
4
5env = lmdb.open("catalog.lmdb", subdir=False, readonly=True, lock=False)
6
7topups = []
8with env.begin() as txn:
9 for _key, value in txn.cursor():
10 item = json.loads(value)
11 if item["product_type"] == "topup" and item["in_stock"]:
12 topups.append(item)
13
14for item in sorted(topups, key=rate_per_dollar, reverse=True)[:10]:
15 print(f"{rate_per_dollar(item):>10.2f} units/$1 "
16 f"{item['id']} {item['name']}")

Service items

A service item is fulfilled manually or by an operator against the account you name in fields — direct top-ups, boosts, activations. There is nothing to count, so omit quantity (it is always 1) and the charge is simply the record’s price.

  • Chargeprice cents, once.
  • Delivery — the order settles completed when the service is done, or failed with a full refund. fields is required and describes the target (user ids, regions, package choices, …).
  • Choice fields — services often use choice fields (e.g. a package or server picker). Send the selected option’s numeric id, not its label.
Order a service with text and choice fields (Python)
1import requests
2
3BASE = "https://api.voodoo.center"
4headers = {"Authorization": f"Bearer {access_token}"}
5
6# `item` is a `service` record from the catalog, e.g. PUBG direct top-up
7assert item["product_type"] == "service" and item["in_stock"]
8print(f"This service costs {item['price'] / 100:.2f}")
9
10# Build `fields` from the record's form definition
11body_fields = {}
12for field in item["fields"]:
13 if field["type"] == "choice":
14 # send the numeric id of the chosen option, not the label
15 body_fields[field["name"]] = field["options"][0]["id"]
16 else:
17 body_fields[field["name"]] = "<value from your customer>"
18
19order = requests.post(f"{BASE}/v1/orders", headers=headers, json={
20 "item_id": item["id"], # no `quantity` for services
21 "merchant_order_id": "svc-2026-0001",
22 "fields": body_fields,
23}).json()
24print(order["id"], order["status"])

In order responses, submitted choice fields are echoed back as their human-readable label even though you sent the numeric id — that’s display-only. Keep sending numeric ids on create.