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

# Placing orders

Placing an order is a single call to `POST /api/v1/orders`. It charges your
account balance and returns `201` immediately with `status: "pending"`.
Fulfillment happens asynchronously — the final result arrives on your
[webhook](/webhooks) and is always readable from `GET /api/v1/orders/{id}`.

## Item types

Every catalog item has a **product type** that determines what you must send:

| Product type | What it is                     | `quantity`                   | `fields`     |
| ------------ | ------------------------------ | ---------------------------- | ------------ |
| `key`        | Gift-card codes / license keys | Integer count, default `1`   | Not used     |
| `topup`      | Balance/currency top-ups       | Decimal amount, **required** | **Required** |
| `service`    | Manual/game services           | Omit (always `1`)            | **Required** |

You discover an item's `item_id`, product type, quantity bounds and field
definitions from the **catalog export** — a separate data feed, not part of this
API. The API does not expose a catalog-browsing endpoint.

## Request fields

* **`item_id`** *(integer, required)* — the numeric item id from the catalog.
* **`quantity`** *(number)* — see the item-type table above. Must fall within
  the item's min/max range.
  * `key`: an integer count (defaults to `1`).
  * `topup`: a decimal amount (required).
  * `service`: omit it — it is always `1`.
* **`merchant_order_id`** *(string, optional)* — your own reference id, **unique
  per account**. Use it for **idempotency** and to correlate webhook events with
  your records.
* **`fields`** *(object)* — required for `topup` and `service` items only, keyed
  by the item's **field names**. `key` items take no `fields`.

### Field values: text vs choice

Each item field is either a **text** field or a **choice** field:

* **Text field** → send the value as a **string** (e.g. `"player_id":
  "123456789"`).
* **Choice field** → send the **numeric choice id**, not the label (e.g.
  `"server": 17`, where `17` is the id of the "Europe (EU-West)" choice).

In responses, submitted `fields` are echoed back with choice values rendered as
their human-readable **display name** (e.g. `"server": "Europe (EU-West)"`),
even though you submitted the numeric id. This is display-only — keep sending
numeric choice ids on create.

## Creating an order

```bash title="Key item (gift card / code)"
curl -X POST https://api.voodoo.center/api/v1/orders \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "item_id": 4090,
    "quantity": 2,
    "merchant_order_id": "po-10231"
  }'
```

```bash title="Top-up item (fields; server is a choice id)"
curl -X POST https://api.voodoo.center/api/v1/orders \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "item_id": 4211,
    "quantity": 10,
    "merchant_order_id": "po-10232",
    "fields": { "player_id": "123456789", "server": 17 }
  }'
```

```bash title="Service item (no quantity)"
curl -X POST https://api.voodoo.center/api/v1/orders \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "item_id": 5000,
    "merchant_order_id": "po-10233",
    "fields": { "account_email": "player@example.com" }
  }'
```

```python title="create_orders.py"
import httpx

BASE = "https://api.voodoo.center"
headers = {"Authorization": f"Bearer {access_token}"}

# Key item — integer quantity, no fields
key_order = httpx.post(f"{BASE}/api/v1/orders", headers=headers, json={
    "item_id": 4090,
    "quantity": 2,
    "merchant_order_id": "po-10231",
}).json()

# Top-up item — decimal quantity + fields (server is a numeric choice id)
topup_order = httpx.post(f"{BASE}/api/v1/orders", headers=headers, json={
    "item_id": 4211,
    "quantity": 10,
    "merchant_order_id": "po-10232",
    "fields": {"player_id": "123456789", "server": 17},
}).json()

# Service item — omit quantity, send fields
service_order = httpx.post(f"{BASE}/api/v1/orders", headers=headers, json={
    "item_id": 5000,
    "merchant_order_id": "po-10233",
    "fields": {"account_email": "player@example.com"},
}).json()

print(key_order["id"], key_order["status"])  # ... pending
```

A successful create returns `201` with the order in `pending`:

```json title="201 Created"
{
  "id": "0190f8a1-6b2c-7e33-9a10-4c1d2e3f5a6b",
  "item": 4211,
  "item_name": "Sample Game Top-up",
  "item_product_type": "topup",
  "quantity": 10,
  "delivered_quantity": 0,
  "price": 12.50,
  "refund_amount": 0,
  "fields": { "player_id": "123456789", "server": "Europe (EU-West)" },
  "status": "pending",
  "error": "",
  "error_message": "",
  "codes": [],
  "merchant_order_id": "po-10232",
  "source": "api",
  "created_by": { "id": "0190f8a0-1111-7000-8000-000000000001", "email": "integrations@merchant.example" },
  "created_at": "2026-07-05T12:00:00Z",
  "completed_at": null
}
```

## The order lifecycle

An order starts `pending`, may briefly be `processing`, and then settles on one
of three terminal statuses:

| Terminal status | Meaning                            | Key fields                                |
| --------------- | ---------------------------------- | ----------------------------------------- |
| `completed`     | Fully delivered                    | `codes` (key items), `delivered_quantity` |
| `partial`       | Some units delivered               | `delivered_quantity`, `refund_amount`     |
| `failed`        | Nothing delivered, charge refunded | `error`, `error_message`, `refund_amount` |

* **`codes`** — delivered key strings, for `key` items.
* **`refund_amount`** — the amount refunded for undelivered units on a `partial`
  or `failed` order.
* **`error` / `error_message`** — set when `status` is `failed` (e.g.
  `error: "OUT_OF_STOCK"`).

An item being **out of stock at create time** is rejected up front as a `400`
validation error ("Item is not available"). If stock or fulfillment fails
**after** the order is accepted, the order settles as a terminal
`status: "failed"` (surfaced on your webhook) — not an HTTP error. See
[Errors](/errors).

## Reading an order

Fetch the current state of any of your orders with `GET /api/v1/orders/{id}`:

```bash title="Get an order"
curl https://api.voodoo.center/api/v1/orders/0190f8a1-6b2c-7e33-9a10-4c1d2e3f5a6b \
  -H "Authorization: Bearer <access_token>"
```

```python title="get_order.py"
order_id = "0190f8a1-6b2c-7e33-9a10-4c1d2e3f5a6b"
order = httpx.get(
    f"{BASE}/api/v1/orders/{order_id}",
    headers={"Authorization": f"Bearer {access_token}"},
).json()
print(order["status"], order["codes"])
```

```json title="Completed key order"
{
  "id": "0190f8a1-6b2c-7e33-9a10-4c1d2e3f5a6b",
  "item": 4090,
  "item_name": "Sample Gift Card 10 USD",
  "item_product_type": "key",
  "quantity": 2,
  "delivered_quantity": 2,
  "price": 18.00,
  "refund_amount": 0,
  "fields": {},
  "status": "completed",
  "error": "",
  "error_message": "",
  "codes": ["ABCD-1234-EFGH-5678", "IJKL-9012-MNOP-3456"],
  "merchant_order_id": "po-10231",
  "source": "api",
  "created_by": { "id": "0190f8a0-1111-7000-8000-000000000001", "email": "integrations@merchant.example" },
  "created_at": "2026-07-05T12:00:00Z",
  "completed_at": "2026-07-05T12:00:07Z"
}
```

`GET /api/v1/orders/{id}` returns `404` (`code: "not_found"`) for an order that
does not exist **or** is not owned by your account. There is no
list-orders endpoint in the API — browse orders in the dashboard.

## Webhook vs polling

Prefer the [webhook](/webhooks): Voodoo Center pushes the terminal event to your
URL as soon as the order settles — no polling needed. Polling
`GET /api/v1/orders/{id}` is a fine fallback (e.g. if a webhook delivery was
missed), but the webhook is the lower-latency, lower-load path.

Receive and verify terminal-status events.

Validation, insufficient balance, and async failures.