# POST /purchase/{productId} — Покупка товара

Создаёт заказ, списывает его стоимость с баланса и выдаёт ключ.

Стоимость резервируется с баланса в момент создания заказа. Если заказ в итоге не выполнен (`failed`/`cancelled`), деньги возвращаются на баланс автоматически.

Товар с ручной выдачей завершается сразу (`201`). Товар с автоматической выдачей покупается у поставщика: обычно это несколько секунд, но если не уложились в ~50 секунд, вы получите `202` со статусом `pending` — заказ продолжит выполняться. Узнать исход можно через вебхук или `GET /orders/{id}`.

Всегда передавайте `idempotencyKey`. Если соединение оборвалось или пришёл таймаут — повторите запрос с тем же ключом: вы получите исход исходного заказа, а не вторую покупку.

Авторизация: заголовок `X-Entity-Key`. **Списывает баланс.**

**Параметры пути**

| Имя | Тип | Обязателен | Описание |
|---|---|---|---|
| `productId` | integer | да | ID товара из каталога. Пример: `12`. |

**Тело запроса (JSON, до 10 КБ)**

| Имя | Тип | Обязателен | Описание |
|---|---|---|---|
| `idempotencyKey` | string | нет | Ваш уникальный ключ покупки: 1–128 печатных ASCII-символов без пробелов, регистр важен. Повтор с тем же ключом не создаёт второй заказ и не списывает деньги второй раз — возвращает исход первого запроса с `replayed: true`. Настоятельно рекомендуется (может стать обязательным). Пример: `order-7731`. |
| `webhookUrl` | string (URL) | нет | Куда отправить уведомление об исходе заказа. Только https, до 2048 символов, без логина/пароля в URL, не localhost и не внутренние адреса. Пример: `https://shop.example.com/hooks/merchant`. |

**Ответы**

- `201 Created` — Куплено. `key` — массив ключей от поставщика; сам ключ в поле `serial`, `type` — `text` или изображение. У товаров с ручной выдачей поля `key` нет.

```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` — Выполняется. Заказ создан, деньги зарезервированы, выдача ещё идёт. Дождитесь вебхука или опрашивайте `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` — Повтор запроса. Тот же `idempotencyKey` — возвращается исход первого запроса, второго списания нет. Код ответа совпадает с исходом заказа.

```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` — Некорректный запрос. Также: `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` — Недостаточно средств. Заказ не создан, ничего не списано. После пополнения можно повторить с тем же `idempotencyKey`.

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

- `403 Forbidden` — Покупки отключены

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

- `404 Not Found` — Товар не найден

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

- `409 Conflict` — Товар недоступен

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

- `415 Unsupported Media Type` — Не JSON

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

- `422 Unprocessable Entity` — Ключ уже использован. Этот `idempotencyKey` уже использован для другого товара.

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

- `502 Bad Gateway` — Не удалось выдать. Поставщик не выполнил заказ. Деньги возвращены на баланс.

```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` — Ключ временно недоступен. Заказ оплачен и выполнен, но ключ сейчас не удалось прочитать. Повторите `GET /orders/{id}` позже или обратитесь в поддержку — повторно покупать не нужно.

```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` — Проверка наличия недоступна

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

- `401 Unauthorized` — Нет ключа

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

- `403 Forbidden` — Неверный ключ

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

- `429 Too Many Requests` — Превышен лимит

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

Веб-версия: https://docs.merchant.dbdlobby.com/ru/reference/purchase
