# POST /sandbox/purchase/{productId} — Test purchase

A simulated purchase with no money.

`sandbox-success` → `201`, `sandbox-insufficient-balance` → `402`, `sandbox-unavailable` → `409`, any other ID → `404` with a hint.

`idempotencyKey` and `webhookUrl` work as in the live API. A webhook is sent only for a successful order, signed with the same secret and containing `"sandbox": true`; up to 3 attempts.

Test orders are kept for 24 hours. The sandbox has its own, stricter rate limit.

Authorization: header `X-Entity-Key`.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `productId` | string | yes | Sandbox product ID. Example: `sandbox-success`. |

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

| Name | Type | Required | Description |
|---|---|---|---|
| `idempotencyKey` | string | no | As in the live API, up to 128 characters. 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` — Success

```json
{
  "status": "success",
  "order": {
    "id": "sandbox_px8cqEgyiFCA",
    "clientId": 7,
    "productId": "sandbox-success",
    "quantity": 1,
    "idempotencyKey": "sb-1",
    "price": 100,
    "currency": "RUB",
    "status": "completed",
    "failureReason": null,
    "webhookUrl": null,
    "webhookStatus": null,
    "webhookAttempts": 0,
    "sandbox": true,
    "createdAt": "2026-11-13T12:36:52.979Z",
    "updatedAt": "2026-11-13T12:36:52.980Z"
  }
}
```

- `201 Created` — Replayed request

```json
{
  "status": "success",
  "order": {
    "id": "sandbox_px8cqEgyiFCA",
    "clientId": 7,
    "productId": "sandbox-success",
    "quantity": 1,
    "idempotencyKey": "sb-1",
    "price": 100,
    "currency": "RUB",
    "status": "completed",
    "failureReason": null,
    "webhookUrl": null,
    "webhookStatus": null,
    "webhookAttempts": 0,
    "sandbox": true,
    "createdAt": "2026-11-13T12:36:52.979Z",
    "updatedAt": "2026-11-13T12:36:52.980Z"
  },
  "replayed": true
}
```

- `402 Payment Required` — Insufficient balance

```json
{
  "status": "error",
  "message": "insufficient_balance",
  "order": {
    "id": "sandbox_LO3g828mKFl_",
    "clientId": 7,
    "productId": "sandbox-insufficient-balance",
    "quantity": 1,
    "idempotencyKey": null,
    "price": 100,
    "currency": "RUB",
    "status": "failed",
    "failureReason": "insufficient_balance",
    "webhookUrl": null,
    "webhookStatus": null,
    "webhookAttempts": 0,
    "sandbox": true,
    "createdAt": "2026-11-13T12:36:52.979Z",
    "updatedAt": "2026-11-13T12:36:52.980Z"
  }
}
```

- `409 Conflict` — Unavailable

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

- `404 Not Found` — Unknown product

```json
{
  "status": "error",
  "message": "Product not found",
  "hint": "Sandbox product ids: sandbox-success, sandbox-insufficient-balance, sandbox-unavailable"
}
```

- `429 Too Many Requests` — Sandbox rate limit

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

- `503 Service Unavailable` — Sandbox unavailable

```json
{
  "status": "error",
  "message": "Sandbox is temporarily unavailable"
}
```

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