Skip to navigation

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_type — key, 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)
def effective_price_cents(item: dict, has_active_subscription: bool) -> int:
"""The per-unit price your balance will actually be charged, in cents."""
if has_active_subscription:
return item["subscriber_price"]
return item["price"]
def estimate_charge(item: dict, quantity: float, has_active_subscription: bool = False) -> float:
"""Estimated charge for an order, in major units (e.g. dollars)."""
return quantity * effective_price_cents(item, has_active_subscription) / 100
# `item` is a record read from the catalog snapshot (see the Catalog guide)
print(f"1 unit costs {item['price'] / 100:.2f}")
print(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.

  • Charge — quantity × 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)
import requests
BASE = "https://api.voodoo.center"
headers = {"Authorization": f"Bearer {access_token}"}
# `item` is a `key` record from the catalog, e.g. Amazon Gift Card 25 TRY
wanted = 3
assert item["product_type"] == "key" and item["in_stock"]
assert item["min_quantity"] <= wanted <= item["max_quantity"]
print(f"Expected charge: {wanted * item['price'] / 100:.2f}")
order = requests.post(f"{BASE}/v1/orders", headers=headers, json={
"item_id": item["id"],
"quantity": wanted, # integer count of codes
"merchant_order_id": "keys-2026-0001", # your idempotency key
}).json()
print(order["id"], order["status"]) # ... pending — the codes arrive
# 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)
@app.post("/webhooks/voodoo-center")
async def voodoo_webhook(request: Request):
# 1. Verify X-Signature against the RAW body, and dedupe on order_id —
# see the Webhooks guide for the full receiver.
event = await request.json()
# 2. Correlate via merchant_order_id and collect the codes.
if event["codes"]: # `completed` or `partial`
store_codes(event["merchant_order_id"], event["codes"])
if event["delivered_quantity"] < event["quantity"]:
# `partial` (or `failed`): the undelivered codes were refunded
record_refund(event["merchant_order_id"], event["refund_amount"])
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)
def rate_per_dollar(item: dict, price_cents: int | None = None) -> float:
"""How much target currency $1 buys. Pass subscriber_price to see your
subscription rate."""
price = price_cents if price_cents is not None else item["price"]
return item["amount_per_price"] / price * 100
def charge_for(item: dict, quantity: float) -> float:
"""What an order of `quantity` costs, in major units."""
return quantity * item["price"] / 100
def delivered_for(item: dict, quantity: float) -> float:
"""How much target currency an order of `quantity` delivers."""
return quantity * item["amount_per_price"]
def quantity_for(item: dict, target_amount: float) -> float:
"""The `quantity` to send so the customer receives `target_amount` units.
Raises if the item can't deliver that amount in one order."""
quantity = target_amount / item["amount_per_price"]
if not item["min_quantity"] <= quantity <= item["max_quantity"]:
raise ValueError(
f"quantity {quantity} outside "
f"{item['min_quantity']}..{item['max_quantity']}"
)
return quantity
# item = {"price": 100, "amount_per_price": 110, "min_quantity": 1, "max_quantity": 500, ...}
print(f"Rate: {rate_per_dollar(item):g} units per $1") # 110
print(f"550 units -> quantity {quantity_for(item, 550):g}, " # 5
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)
import requests
BASE = "https://api.voodoo.center"
headers = {"Authorization": f"Bearer {access_token}"}
# `item` is a `topup` record from the catalog, e.g. Steam Top-up (USD)
target_amount = 20 # what the customer should receive
quantity = quantity_for(item, target_amount)
print(f"Charging {charge_for(item, quantity):.2f} "
f"to deliver {delivered_for(item, quantity):g} units")
order = requests.post(f"{BASE}/v1/orders", headers=headers, json={
"item_id": item["id"],
"quantity": quantity, # decimal, required for topup
"merchant_order_id": "topup-2026-0001",
"fields": {
"steam_login": "customer_login", # the item's required fields,
}, # keyed by field *name*
}).json()
print(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)
import json
import lmdb
env = lmdb.open("catalog.lmdb", subdir=False, readonly=True, lock=False)
topups = []
with env.begin() as txn:
for _key, value in txn.cursor():
item = json.loads(value)
if item["product_type"] == "topup" and item["in_stock"]:
topups.append(item)
for item in sorted(topups, key=rate_per_dollar, reverse=True)[:10]:
print(f"{rate_per_dollar(item):>10.2f} units/$1 "
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.

  • Charge — price 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)
import requests
BASE = "https://api.voodoo.center"
headers = {"Authorization": f"Bearer {access_token}"}
# `item` is a `service` record from the catalog, e.g. PUBG direct top-up
assert item["product_type"] == "service" and item["in_stock"]
print(f"This service costs {item['price'] / 100:.2f}")
# Build `fields` from the record's form definition
body_fields = {}
for field in item["fields"]:
if field["type"] == "choice":
# send the numeric id of the chosen option, not the label
body_fields[field["name"]] = field["options"][0]["id"]
else:
body_fields[field["name"]] = "<value from your customer>"
order = requests.post(f"{BASE}/v1/orders", headers=headers, json={
"item_id": item["id"], # no `quantity` for services
"merchant_order_id": "svc-2026-0001",
"fields": body_fields,
}).json()
print(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.