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

# Catalog

The full product catalog — every orderable item with its price, stock, product
type and input form — is published as a single downloadable snapshot. You
download it, read it locally, and use each item's `id` and `fields` to place
orders. There is no paginated "list products" API endpoint for programmatic
use; the snapshot **is** the catalog.

Download URL:

```
https://downloads.voodoo.center/catalog.lmdb.zst
```

Served via the `cdn.voodoo.center` CDN. No authentication is required to
download it.

## Personal catalog

The URL above serves the **global** catalog, which uses standard pricing. If
your account has **personal (negotiated) pricing** — per-item price overrides —
there is a separate **personal catalog** published at a URL that includes your
account's **client id**. It holds the same records, but `price` and
`subscriber_price` reflect *your* account's pricing. Use the personal file when
it exists; otherwise use the global one.

Personal download URL:

```
https://downloads.voodoo.center/{client_id}.lmdb.zst
```

Find your **Client ID** — and your ready-to-use personal catalog URL — on the
**API** page of the dashboard (the same page where you manage API keys and
webhooks). Not every account has personal pricing: if yours doesn't, this URL
returns **404** — in that case fall back to `catalog.lmdb.zst`.

It is the **same format and schema** as the global catalog (a zstd-compressed
single-file [LMDB](http://www.lmdb.tech/doc/)); the only difference is the price
values. So: try your personal URL, fall back to the global one on a 404, then
decompress, open and read the file **exactly as below**.

```python
import urllib.error
import urllib.request

GLOBAL_URL = "https://downloads.voodoo.center/catalog.lmdb.zst"
PERSONAL_URL = "https://downloads.voodoo.center/{client_id}.lmdb.zst"


def download_catalog(client_id: str, dest: str = "catalog.lmdb.zst") -> str:
    # Prefer the personal catalog (your negotiated prices); fall back to the
    # global one if this account has no personal file (a 404 on the personal URL).
    try:
        urllib.request.urlretrieve(PERSONAL_URL.format(client_id=client_id), dest)
    except urllib.error.HTTPError as exc:
        if exc.code != 404:
            raise
        urllib.request.urlretrieve(GLOBAL_URL, dest)
    return dest


# Grab your client id from the dashboard's API page.
download_catalog("your-client-id")   # -> catalog.lmdb.zst
# Now decompress, open and read it exactly as shown below.
```

## What you get

`catalog.lmdb.zst` is a single-file [LMDB](http://www.lmdb.tech/doc/) database,
compressed with [zstandard](https://facebook.github.io/zstd/):

* **One file, one keyspace.** Items live in LMDB's default database. Each item
  is one record — **key** = the 8-byte big-endian item id, **value** = a
  compact JSON object.
* **Self-contained records.** Every item embeds its full input form (`fields`,
  each choice field with its `options`) inline, so a single key lookup gives you
  everything you need to order that item.
* **Small and fast.** A full catalog compresses \~45× (a \~1.5 MB database → \~34
  KB). You decompress once, then memory-map the database and read it with no
  server round-trips.

Prices in the catalog are in **cents** (integers) and reflect standard pricing.
Your final charge is always confirmed by the order response and your balance —
see [Placing orders](/orders).

## Download and open it

Install the two readers, then download → decompress → open read-only.

```bash title="Install dependencies"
pip install zstandard lmdb
```

```bash
# Download the compressed snapshot
curl -o catalog.lmdb.zst https://downloads.voodoo.center/catalog.lmdb.zst

# Decompress the zstd frame to a single-file LMDB database
zstd -d catalog.lmdb.zst -o catalog.lmdb    # or: unzstd catalog.lmdb.zst
```

```python
import json
import struct
import urllib.request

import lmdb
import zstandard

CATALOG_URL = "https://downloads.voodoo.center/catalog.lmdb.zst"


def item_key(item_id: int) -> bytes:
    # Records are keyed by the 8-byte big-endian item id (so they sort by id).
    return struct.pack(">Q", item_id)


# 1. Download the compressed snapshot.
urllib.request.urlretrieve(CATALOG_URL, "catalog.lmdb.zst")

# 2. Stream-decompress the zstd frame into a single-file LMDB database.
dctx = zstandard.ZstdDecompressor()
with open("catalog.lmdb.zst", "rb") as fin, open("catalog.lmdb", "wb") as fout:
    dctx.copy_stream(fin, fout)

# 3. Open the env read-only. It is a single FILE, not a directory (subdir=False),
#    and read-only readers must pass lock=False.
env = lmdb.open("catalog.lmdb", subdir=False, readonly=True, lock=False)

# Look up one item by id.
with env.begin() as txn:
    raw = txn.get(item_key(4211))
    item = json.loads(raw) if raw else None
    print(item)

# Iterate the whole catalog (records come out ordered by id).
with env.begin() as txn:
    for key, value in txn.cursor():
        item = json.loads(value)
        if item["in_stock"]:
            price = item["price"] / 100  # cents -> major units
            print(item["id"], item["product_type"], f"{price:.2f}", item["name"])
```

`★ Insight ─────────────────────────────────────`
LMDB is memory-mapped, so opening the file is essentially free and lookups read
straight from the OS page cache — you can keep the env open and query it
millions of times without re-parsing anything. That's why the catalog ships as
LMDB rather than one giant JSON array.
`─────────────────────────────────────────────────`

## Record schema

Each value is a JSON object:

| Field              | Type              | Description                                                                        |
| ------------------ | ----------------- | ---------------------------------------------------------------------------------- |
| `id`               | integer           | Item id. Pass this as `item_id` when [placing an order](/orders).                  |
| `name`             | string            | Human-readable item name.                                                          |
| `product_type`     | string            | `key`, `topup`, or `service` — determines the quantity/fields rules when ordering. |
| `min_quantity`     | number            | Minimum orderable quantity.                                                        |
| `max_quantity`     | number            | Maximum orderable quantity (effective cap).                                        |
| `price`            | integer (cents)   | Standard price. Divide by 100 for major units.                                     |
| `base_price`       | integer (cents)   | Original/list price. When it's greater than `price`, show it struck through.       |
| `subscriber_price` | integer (cents)   | Price with an active subscription.                                                 |
| `amount_per_price` | number            | Units delivered per unit of price (relevant for top-ups).                          |
| `in_stock`         | boolean           | Whether the item is currently orderable.                                           |
| `updated_at`       | string (ISO 8601) | When this item last changed.                                                       |
| `fields`           | array             | The item's input form — see below.                                                 |

Each entry in **`fields`**:

| Field      | Type    | Description                                                           |
| ---------- | ------- | --------------------------------------------------------------------- |
| `id`       | integer | Field id.                                                             |
| `name`     | string  | Field key — use this as the key in the order's `fields` object.       |
| `type`     | string  | `string`, `integer`, `email`, `url`, or `choice`.                     |
| `required` | boolean | Whether a value must be supplied.                                     |
| `options`  | array   | For `choice` fields only: `[{ "id": <int>, "value": "<label>" }, …]`. |

```json title="Example record (a top-up item)"
{
  "id": 4211,
  "name": "Sample Game Top-up",
  "product_type": "topup",
  "min_quantity": 1,
  "max_quantity": 1000,
  "price": 1250,
  "base_price": 1250,
  "subscriber_price": 1150,
  "amount_per_price": 1,
  "in_stock": true,
  "updated_at": "2026-07-05T12:00:00+00:00",
  "fields": [
    { "id": 88, "name": "player_id", "type": "string", "required": true, "options": [] },
    {
      "id": 91, "name": "server", "type": "choice", "required": true,
      "options": [
        { "id": 17, "value": "Europe (EU-West)" },
        { "id": 18, "value": "North America" }
      ]
    }
  ]
}
```

## Using the catalog with the Orders API

Everything you need to build a `POST /api/v1/orders` request is in the record:

* **`item_id`** = the record's `id`.
* **`quantity`** — bounded by `min_quantity`/`max_quantity`. Required for
  `topup` (decimal) and `key` (integer, default `1`); omit for `service`.
* **`fields`** — required for `topup` and `service`. Key each entry by the
  field's **`name`**. For a **`choice`** field, send the selected option's
  **`id`** (the integer), not its `value`. For text fields, send the string.

```python
# `item` is a record read from the catalog above; `access_token` is your
# Bearer token (see Authentication).
import urllib.request, json

body = {
    "item_id": item["id"],
    "quantity": 10,                 # within min_quantity..max_quantity
    "merchant_order_id": "po-10232",
    "fields": {
        "player_id": "123456789",   # a text field -> its string value
        "server": 17,               # a choice field -> the option's id
    },
}
req = urllib.request.Request(
    "https://api.voodoo.center/api/v1/orders",
    data=json.dumps(body).encode(),
    headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
    method="POST",
)
order = json.loads(urllib.request.urlopen(req).read())
print(order["id"], order["status"])
```

## Staying up to date

The snapshot is regenerated regularly. Its freshness is the object's HTTP
`ETag` / `Last-Modified` — use a conditional request so you only re-download
when it actually changed:

```bash title="Re-download only if changed"
# -z makes curl send If-Modified-Since based on the local file's mtime;
# a 304 leaves your file untouched.
curl -o catalog.lmdb.zst -z catalog.lmdb.zst https://downloads.voodoo.center/catalog.lmdb.zst
```

A simple, robust pattern for a running integration: refresh on a schedule (e.g.
every few minutes) with the conditional request above, and when a new file
arrives, decompress it and swap the open LMDB env.

Use an item's id and fields to place an order.

Get the Bearer token you need to call the Orders API.