# POST /purchase/{productId} — Purchase a product

Creates an order, charges its price to the balance and returns the key.

The price is reserved from the balance when the order is created. If the order ends up not fulfilled (`failed`/`cancelled`), the money returns to the balance automatically.

A manually fulfilled product completes immediately (`201`). An automatically fulfilled product is bought from the supplier: usually a few seconds, but if it takes longer than ~50 seconds you get `202` with status `pending` — the order keeps going. Learn the outcome from the webhook or `GET /orders/{id}`.

Always send an `idempotencyKey`. If the connection dropped or timed out, repeat the request with the same key: you get the outcome of the original order, not a second purchase.

Authorization: header `X-Entity-Key`. **Charges the balance.**

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `productId` | integer | yes | Product ID from the catalog. Example: `12`. |

**Request body (JSON, up to 10 KB)**

| Name | Type | Required | Description |
|---|---|---|---|
| `idempotencyKey` | string | no | Your unique purchase key: 1–128 printable ASCII characters, no spaces, case-sensitive. A repeat with the same key creates no second order and charges nothing twice — it returns the outcome of the first request with `replayed: true`. Strongly recommended (may become mandatory). Example: `order-7731`. |
| `webhookUrl` | string (URL) | no | Where to send the notification about the order outcome. https only, up to 2048 characters, no username/password in the URL, not localhost or internal addresses. Example: `https://shop.example.com/hooks/merchant`. |

**Responses**

- `201 Created` — Purchased. `key` is the supplier's array of keys; the key itself is in `serial`, `type` is `text` or an image. Manually fulfilled products have no `key` field.

```json
{
  "status": "success",
  "order": {
    "id": 1842,
    "productId": 12,
    "quantity": 1,
    "price": 1490,
    "currency": "RUB",
    "status": "completed",
    "failureReason": null,
    "idempotencyKey": "order-7731",
    "webhookStatus": "pending",
    "createdAt": "2026-11-13T12:36:53.000Z",
    "updatedAt": "2026-11-13T12:37:10.000Z",
    "key": [
      {
        "serial": "ABCDE-FGHIJ-KLMNO",
        "type": "text",
        "name": "Dead by Daylight"
      }
    ]
  }
}
```

- `202 Accepted` — In progress. The order is created and the money reserved; fulfillment is still running. Wait for the webhook or poll `GET /orders/{id}`.

```json
{
  "status": "pending",
  "message": "Purchase is still being processed",
  "order": {
    "id": 1842,
    "productId": 12,
    "quantity": 1,
    "price": 1490,
    "currency": "RUB",
    "status": "pending",
    "failureReason": null,
    "idempotencyKey": "order-7731",
    "webhookStatus": null,
    "createdAt": "2026-11-13T12:36:53.000Z",
    "updatedAt": "2026-11-13T12:37:10.000Z"
  }
}
```

- `201 Created` — Replayed request. Same `idempotencyKey` — the outcome of the first request is returned, nothing is charged again. The status code matches the order's outcome.

```json
{
  "status": "success",
  "order": {
    "id": 1842,
    "productId": 12,
    "quantity": 1,
    "price": 1490,
    "currency": "RUB",
    "status": "completed",
    "failureReason": null,
    "idempotencyKey": "order-7731",
    "webhookStatus": "delivered",
    "createdAt": "2026-11-13T12:36:53.000Z",
    "updatedAt": "2026-11-13T12:37:10.000Z"
  },
  "replayed": true
}
```

- `400 Bad Request` — Bad request. Also: `Invalid idempotencyKey (1-128 printable ASCII characters, no spaces)`, `Invalid webhookUrl (must_be_https)`, `Malformed JSON body`.

```json
{
  "status": "error",
  "message": "Invalid product id"
}
```

- `402 Payment Required` — Insufficient balance. No order is created and nothing is charged. After a top-up you can repeat with the same `idempotencyKey`.

```json
{
  "status": "error",
  "message": "insufficient_balance"
}
```

- `403 Forbidden` — Purchases disabled

```json
{
  "status": "error",
  "message": "purchase_disabled"
}
```

- `404 Not Found` — Product not found

```json
{
  "status": "error",
  "message": "Product not found"
}
```

- `409 Conflict` — Product unavailable

```json
{
  "status": "error",
  "message": "Product is not available for purchase"
}
```

- `415 Unsupported Media Type` — Not JSON

```json
{
  "status": "error",
  "message": "Content-Type must be application/json"
}
```

- `422 Unprocessable Entity` — Key already used. This `idempotencyKey` was already used for a different product.

```json
{
  "status": "error",
  "code": "idempotency_key_reused",
  "message": "This idempotencyKey was already used for a different product"
}
```

- `502 Bad Gateway` — Fulfillment failed. The supplier did not fulfill the order. The money is returned to the balance.

```json
{
  "status": "error",
  "message": "fulfillment_failed",
  "order": {
    "id": 1842,
    "productId": 12,
    "quantity": 1,
    "price": 1490,
    "currency": "RUB",
    "status": "failed",
    "failureReason": "fulfillment_failed",
    "idempotencyKey": "order-7731",
    "webhookStatus": "delivered",
    "createdAt": "2026-11-13T12:36:53.000Z",
    "updatedAt": "2026-11-13T12:37:10.000Z"
  }
}
```

- `500 Internal Server Error` — Key temporarily unavailable. The order is paid and fulfilled, but its key can't be read right now. Retry `GET /orders/{id}` later or contact support — do not buy again.

```json
{
  "status": "error",
  "code": "key_unavailable",
  "message": "The order is paid but its key can't be retrieved right now; contact support",
  "order": {
    "id": 1842,
    "productId": 12,
    "quantity": 1,
    "price": 1490,
    "currency": "RUB",
    "status": "completed",
    "failureReason": null,
    "idempotencyKey": "order-7731",
    "webhookStatus": "delivered",
    "createdAt": "2026-11-13T12:36:53.000Z",
    "updatedAt": "2026-11-13T12:37:10.000Z"
  }
}
```

- `503 Service Unavailable` — Availability check down

```json
{
  "status": "error",
  "message": "Availability check temporarily unavailable"
}
```

- `401 Unauthorized` — No key

```json
{
  "status": "error",
  "message": "Missing X-Entity-Key header"
}
```

- `403 Forbidden` — Invalid key

```json
{
  "status": "error",
  "message": "Invalid entity key"
}
```

- `429 Too Many Requests` — Rate limited

```json
{
  "status": "error",
  "message": "Too many requests, try again later"
}
```

Web version: https://docs.merchant.dbdlobby.com/en/reference/purchase
