Catalog

Download the full product catalog and work with it offline
View as Markdown

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

Pick your catalog URL, with fallback (Python)
1import urllib.error
2import urllib.request
3
4GLOBAL_URL = "https://downloads.voodoo.center/catalog.lmdb.zst"
5PERSONAL_URL = "https://downloads.voodoo.center/{client_id}.lmdb.zst"
6
7
8def download_catalog(client_id: str, dest: str = "catalog.lmdb.zst") -> str:
9 # Prefer the personal catalog (your negotiated prices); fall back to the
10 # global one if this account has no personal file (a 404 on the personal URL).
11 try:
12 urllib.request.urlretrieve(PERSONAL_URL.format(client_id=client_id), dest)
13 except urllib.error.HTTPError as exc:
14 if exc.code != 404:
15 raise
16 urllib.request.urlretrieve(GLOBAL_URL, dest)
17 return dest
18
19
20# Grab your client id from the dashboard's API page.
21download_catalog("your-client-id") # -> catalog.lmdb.zst
22# Now decompress, open and read it exactly as shown below.

What you get

catalog.lmdb.zst is a single-file LMDB database, compressed with zstandard:

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

Download and open it

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

Install dependencies
$pip install zstandard lmdb
Download & decompress (shell)
$# 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
Download, decompress & read (Python)
1import json
2import struct
3import urllib.request
4
5import lmdb
6import zstandard
7
8CATALOG_URL = "https://downloads.voodoo.center/catalog.lmdb.zst"
9
10
11def item_key(item_id: int) -> bytes:
12 # Records are keyed by the 8-byte big-endian item id (so they sort by id).
13 return struct.pack(">Q", item_id)
14
15
16# 1. Download the compressed snapshot.
17urllib.request.urlretrieve(CATALOG_URL, "catalog.lmdb.zst")
18
19# 2. Stream-decompress the zstd frame into a single-file LMDB database.
20dctx = zstandard.ZstdDecompressor()
21with open("catalog.lmdb.zst", "rb") as fin, open("catalog.lmdb", "wb") as fout:
22 dctx.copy_stream(fin, fout)
23
24# 3. Open the env read-only. It is a single FILE, not a directory (subdir=False),
25# and read-only readers must pass lock=False.
26env = lmdb.open("catalog.lmdb", subdir=False, readonly=True, lock=False)
27
28# Look up one item by id.
29with env.begin() as txn:
30 raw = txn.get(item_key(4211))
31 item = json.loads(raw) if raw else None
32 print(item)
33
34# Iterate the whole catalog (records come out ordered by id).
35with env.begin() as txn:
36 for key, value in txn.cursor():
37 item = json.loads(value)
38 if item["in_stock"]:
39 price = item["price"] / 100 # cents -> major units
40 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:

FieldTypeDescription
idintegerItem id. Pass this as item_id when placing an order.
namestringHuman-readable item name.
product_typestringkey, topup, or service — determines the quantity/fields rules when ordering.
min_quantitynumberMinimum orderable quantity.
max_quantitynumberMaximum orderable quantity (effective cap).
priceinteger (cents)Standard price. Divide by 100 for major units.
base_priceinteger (cents)Original/list price. When it’s greater than price, show it struck through.
subscriber_priceinteger (cents)Price with an active subscription.
amount_per_pricenumberUnits delivered per unit of price (relevant for top-ups).
in_stockbooleanWhether the item is currently orderable.
updated_atstring (ISO 8601)When this item last changed.
fieldsarrayThe item’s input form — see below.

Each entry in fields:

FieldTypeDescription
idintegerField id.
namestringField key — use this as the key in the order’s fields object.
typestringstring, integer, email, url, or choice.
requiredbooleanWhether a value must be supplied.
optionsarrayFor choice fields only: [{ "id": <int>, "value": "<label>" }, …].
Example record (a top-up item)
1{
2 "id": 4211,
3 "name": "Sample Game Top-up",
4 "product_type": "topup",
5 "min_quantity": 1,
6 "max_quantity": 1000,
7 "price": 1250,
8 "base_price": 1250,
9 "subscriber_price": 1150,
10 "amount_per_price": 1,
11 "in_stock": true,
12 "updated_at": "2026-07-05T12:00:00+00:00",
13 "fields": [
14 { "id": 88, "name": "player_id", "type": "string", "required": true, "options": [] },
15 {
16 "id": 91, "name": "server", "type": "choice", "required": true,
17 "options": [
18 { "id": 17, "value": "Europe (EU-West)" },
19 { "id": 18, "value": "North America" }
20 ]
21 }
22 ]
23}

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.
Turn a catalog record into an order (Python)
1# `item` is a record read from the catalog above; `access_token` is your
2# Bearer token (see Authentication).
3import urllib.request, json
4
5body = {
6 "item_id": item["id"],
7 "quantity": 10, # within min_quantity..max_quantity
8 "merchant_order_id": "po-10232",
9 "fields": {
10 "player_id": "123456789", # a text field -> its string value
11 "server": 17, # a choice field -> the option's id
12 },
13}
14req = urllib.request.Request(
15 "https://api.voodoo.center/api/v1/orders",
16 data=json.dumps(body).encode(),
17 headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
18 method="POST",
19)
20order = json.loads(urllib.request.urlopen(req).read())
21print(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:

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.