> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.voodoo.center/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.voodoo.center/_mcp/server.

# Item types

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](/catalog)** snapshot; the
request/response mechanics live in **[Placing orders](/orders)**.

| Product type                | What it is                     | `quantity`                   | `fields`     | You receive                                       |
| --------------------------- | ------------------------------ | ---------------------------- | ------------ | ------------------------------------------------- |
| [`key`](#key-items)         | Gift-card codes / license keys | Integer count, default `1`   | Not used     | Code strings in the order's `codes`               |
| [`topup`](#top-up-items)    | Balance/currency top-ups       | Decimal amount, **required** | **Required** | Balance credited to the account named in `fields` |
| [`service`](#service-items) | Manual/game services           | Omit (always `1`)            | **Required** | The 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:

| Field              | What it means                                                                                                                        |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| `price`            | What you pay **per unit of `quantity`**, in cents. This is the price used to charge your balance.                                    |
| `base_price`       | The original/list price. When it is greater than `price`, the item is discounted — show `base_price` struck through next to `price`. |
| `subscriber_price` | The 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)

```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)

```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](/webhooks)** 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)

```python
@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](/orders#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:

```text
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:

```text
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`:

| Question                     | Calculation         | Result                       |
| ---------------------------- | ------------------- | ---------------------------- |
| Rate per \$1                 | `(110 / 100) × 100` | **110 UC per \$1**           |
| Charge for `quantity: 5`     | `5 × 100 / 100`     | **\$5.00**                   |
| Delivered for `quantity: 5`  | `5 × 110`           | **550 UC**                   |
| Quantity to deliver 1 100 UC | `1100 / 110`        | **`quantity: 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)

```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)

```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)

```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)

```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.

#### [Catalog](/catalog)

Download the snapshot these records come from.

#### [Placing orders](/orders)

Request fields, the order lifecycle and terminal statuses.

#### [Webhooks](/webhooks)

Receive the terminal result instead of polling.

#### [Errors](/errors)

Validation failures, insufficient balance and async errors.