# Online order from an external system

> End-to-end scenario: find the customer, build the sale in the right shop, apply a discount and take the payment — using only the methods available to an integration key.

Task: an online store or a marketplace places an order on its side and must post it in BILLZ as a sale of a specific shop — with the buyer, a discount and a payment.

## Terminology fork

The word “order” is already taken in BILLZ.

| Task wording                             | BILLZ term                           | Methods                                               |
| ---------------------------------------- | ------------------------------------ | ----------------------------------------------------- |
| A buyer ordered a product on the website | **Sale** (`Order`, sales receipt)    | `/v2/order`, `/v2/order-product`, `/v2/order-payment` |
| A product is ordered **from a supplier** | **Purchase order** (`SupplierOrder`) | `/v2/supplier-order`, `/v2/supplier-order/add-item`   |

An online order needs the **first row**.

> **There is no separate “online order” entity in the API yet**
>
> An order from the website is posted as a regular sale. If an intermediate status is needed between placing and handing over the order, it is stored on the external system’s side, and the sale in BILLZ is created at the moment of actual handover or payment.

## Scenario diagram

```plaintext
GET  /v1/shop, /v1/company-payment-type      reference data, once
     │
GET  /v1/client?phone_number=…               find the customer
POST /v1/client                              create if not found
     │
POST /v2/order   {shop_id, cashbox_id}       create the sale ──> order_id
POST /v2/order-product/:order_id             add a product (one per request)
PUT  /v2/order-customer-new/:order_id        attach the customer
POST /v2/order-manual-discount/:order_id     discount
POST /v1/recalculate-order-bill/:order_id    verify the total
POST /v2/order-payment/:order_id             complete the sale
     │
GET  /v2/order/:id                           details of the completed sale
```

Creating a sale and the methods that change the receipt contents are **asynchronous**: the `Billz-Response-Channel: HTTP` header is required, otherwise an event identifier is returned instead of the result. Recalculation and payment are synchronous and do not need the header.

## 1. Customer

### Search

```bash
curl -X GET 'https://api-admin.billz.ai/v1/client?phone_number=%2B998900000000&limit=10&page=1' \
  -H 'Authorization: Bearer TOKEN'
```

Parameters: `phone_number`, `search`, `chat_id`, `limit`, `page`. The response is `{count, clients[]}`. The detailed card for a found ID is `GET /v1/customer/:id` (it has a different, wider set of fields than an element of `clients[]`).

`search` matches a substring in the first name, last name, middle name, phone numbers, `external_id`, `chat_id`, date of birth, loyalty card code and name. Several words separated by spaces are combined with AND. Special characters are stripped: `+998901234567` and `998901234567` are equivalent. `phone_number` matches a substring, `chat_id` — an exact value.

> **A customer that has just been created may not be found immediately**
>
> The UUID is saved right away from the `POST /v1/client` response.

### Creation

```bash
curl -X POST 'https://api-admin.billz.ai/v1/client' \
  -H 'Authorization: Bearer TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "first_name": "John",
    "last_name": "Smith",
    "phone_number": "+998900000000",
    "gender": 1,
    "date_of_birth": "1990-05-14"
  }'
```

The response is `{ "id": "..." }`: the customer UUID, which is stored in the external system. `gender`: `1` — male, `2` — female, other values are saved as empty.

> **The method does not check for duplicates by phone number**
>
> A repeated request with the same number creates a second UUID and a second loyalty card. Run `GET /v1/client?phone_number=…` before creating, and create only when `count == 0`.

## 2. Create the sale in the right shop

The sale is created by the asynchronous method `POST /v2/order`. The response channel is set by the `Billz-Response-Channel: HTTP` header or by the query parameter of the same name, as in the example below.

```bash
curl -X POST 'https://api-admin.billz.ai/v2/order?Billz-Response-Channel=HTTP' \
  -H 'Authorization: Bearer TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "shop_id": "c1056fcc-539b-47ea-8bc4-d9e337c433f0",
    "cashbox_id": "9b4a7f8c-a8ef-4870-8a1b-89869fdd4141"
  }'
```

```json
{
  "session_id": "fd8d352a-6299-4489-8a6e-a5c037f3f65b",
  "status_code": 200,
  "id": "38c3920a-ffdd-4233-a823-8af3266f5f4f",
  "error": { "code": "", "message": "" },
  "data": {
    "id": "38c3920a-ffdd-4233-a823-8af3266f5f4f",
    "order_number": "5632631379",
    "order_type": "SALE"
  },
  "correlation_id": "3674f28e-7b96-4683-87c3-80892698f421"
}
```

The sale UUID is the `id` field (the same value as in `data.id`); it is passed to all subsequent steps. `order_number` is the short sale number that the cashier sees in the interface.

**The shop is selected by the `shop_id` field, the cash register by the `cashbox_id` field.** The list of shops is `GET /v1/shop`.

> **Without a response channel the sale ID is not returned**
>
> Without `Billz-Response-Channel` the method responds with `200` and the body `{"message": "<correlation_id>"}`.

## 3. Add products

One product item per request.

```bash
curl -X POST 'https://api-admin.billz.ai/v2/order-product/SALE_ID' \
  -H 'Authorization: Bearer TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Billz-Response-Channel: HTTP' \
  -d '{
    "product_id": "2ce40c63-4527-4898-a975-7c30c255fd50",
    "sold_measurement_value": 2,
    "used_wholesale_price": false
  }'
```

> **Only this method adds line items**
>
> The “cart calculated” flag checked by the payment is set only by the recalculation triggered by `POST /v2/order-product/:order_id`.

This is an **upsert with an absolute quantity**: a repeated call with the same `product_id` replaces the quantity. `sold_measurement_value: 0` removes the line item from the receipt.

## 4. Attach the customer

```bash
curl -X PUT 'https://api-admin.billz.ai/v2/order-customer-new/SALE_ID' \
  -H 'Authorization: Bearer TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Billz-Response-Channel: HTTP' \
  -d '{ "customer_id": "CUSTOMER_ID", "check_auth_code": false }'
```

The response comes in an envelope: the object is in the `data` field.

> **SMS confirmation and the `check_auth_code` field**
>
> Whether an SMS code is required when attaching a customer is determined by a role permission. With `check_auth_code: false` no confirmation is requested.

## 5. Discount

```bash
curl -X POST 'https://api-admin.billz.ai/v2/order-manual-discount/SALE_ID' \
  -H 'Authorization: Bearer TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Billz-Response-Channel: HTTP' \
  -d '{ "discount_unit": "PERCENTAGE", "discount_value": 10 }'
```

`discount_unit` is `PERCENTAGE` or `CURRENCY`. The optional `product_id` applies the discount to a single line item; without it, the discount applies to the whole cart.

> **`CURRENCY` is the final price**
>
> With `discount_unit: "CURRENCY"` the `discount_value` becomes the **final price**: `50000` on a cart of 900 000 means “sell everything for 50 000”. A fixed discount amount is subtracted in the external system, and the resulting price is passed.

> **A cart discount and a line-item discount erase each other**
>
> A cart discount applied after a line-item discount cancels it, and vice versa. Use one level only.

> **A discount cannot be removed with an integration key**
>
> To cancel a discount, set it again with a zero value or rebuild the receipt.

A manual discount restarts the cart recalculation: call the payment after the asynchronous response to the discount.

## 6. Verify the total

```bash
curl -X POST 'https://api-admin.billz.ai/v1/recalculate-order-bill/SALE_ID' \
  -H 'Authorization: Bearer TOKEN'
```

The method is synchronous and returns the current receipt with line items, discounts and the total.

> **The method does not set the “cart calculated” flag**
>
> On `20084` at payment, repeating the recalculation does not help: wait for the asynchronous operation that changed the receipt to complete.

For a held sale the method returns an error.

## 7. Complete the sale

```bash
curl -X POST 'https://api-admin.billz.ai/v2/order-payment/SALE_ID' \
  -H 'Authorization: Bearer TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "payments": [
      { "company_payment_type_id": "c6e84d9b-...", "paid_amount": 900000, "returned_amount": 0 }
    ],
    "comment": "Order #12345 from the website",
    "without_cashback": false
  }'
```

| Field                                | Meaning                                                                                  |
| ------------------------------------ | ---------------------------------------------------------------------------------------- |
| `payments[]`                         | list of payments; several elements — payment with different methods                      |
| `payments[].company_payment_type_id` | payment type from `GET /v1/company-payment-type`                                         |
| `payments[].paid_amount`             | amount paid                                                                              |
| `payments[].returned_amount`         | change                                                                                   |
| `payments[].gift_card_id`            | **integer**; required for payment with a gift card                                       |
| `with_cashback`                      | **amount** (not a flag) redeemed from the customer’s bonus balance                       |
| `without_cashback`                   | `true` — do not accrue cashback for this sale                                            |
| `sold_at`                            | date and time of the sale in the `YYYY-MM-DD HH:MM:SS` format — for backdated posting    |
| `comment`                            | order number of the external system; an empty string does not overwrite the previous one |
| `skip_ofd`                           | skips the fiscal data operator (OFD) **checks** before payment, see below                |

The method is synchronous. The response is `{"order_type": "SALE", "should_print_cheque": true}` with status `200`.

> **`skip_ofd` does not disable fiscalization**
>
> The flag **skips the check** that the receipt has been fiscalized before payment. The method sends nothing to the fiscal data operator (OFD).

### Payment error codes

An error comes with HTTP `400`, and the reason code is **inside `data`**:

```json
{
  "status_code": 400,
  "error": { "code": "", "message": "status: 400 - errors: Bad request - causes: ..." },
  "data": { "error_code": 20084, "message": "payment is possible only for calculated order" }
}
```

The reason is determined by `data.error_code`: the `error.code` field in this response is empty.

| Code    | Message                                         | Meaning                                                                               |
| ------- | ----------------------------------------------- | ------------------------------------------------------------------------------------- |
| `20009` | cashbox shop id and order shop id are different | the user’s current cash register belongs to another shop                              |
| `20010` | user’s current cashbox id is empty              | the sale’s shop has no open cash register                                             |
| `20011` | cashbox has not enough money                    | the cash register does not have enough money for the return                           |
| `20015` | order does not have items                       | the receipt has no line items                                                         |
| `20016` | not enough product                              | not enough stock; the available quantity is in `data.data`                            |
| `20017` | order is deleted                                | the sale has been deleted                                                             |
| `20020` | wrong payment amount                            | the payment amounts do not add up; `data` contains `total_price` and `claimed_amount` |
| `20047` | not enough funds in customer balance            | not enough bonus points for `with_cashback`                                           |
| `20063` | order not sent to epos                          | the receipt has not been sent to the OFD (bypassed with the `skip_ofd` flag)          |
| `20082` | cannot make payment with closed cashbox         | the cash register is closed                                                           |
| `20084` | payment is possible only for calculated order   | the cart has not been recalculated, see step 3                                        |

`claimed_amount` is the sum of `paid_amount` minus `returned_amount` over all elements, plus `with_cashback`, plus the debt; it is compared with the receipt total.

## 8. Sale ID and details

The ID is returned at step 2. Full details of the completed sale — line items, payments, discounts, customer:

```bash
curl -X GET 'https://api-admin.billz.ai/v2/order/SALE_ID' \
  -H 'Authorization: Bearer TOKEN'
```

## Answers to frequent questions

### Which field selects the shop

The `shop_id` field in the body of `POST /v2/order`; the cash register is set there too — `cashbox_id`. The list of shops is `GET /v1/shop`.

### Can a sale be made when stock is insufficient

This is controlled by the product setting **“allow negative stock”**. If it is disabled, adding a product beyond the stock fails; if enabled, the stock goes negative.

The error is HTTP `400`, `data.error_code` = **20016**, `data.message` = `not enough product`; the available quantity is returned in `data.data`.

A preliminary stock check is `GET /v2/products` → `shop_measurement_values[].active_measurement_value` for the required `shop_id`. Between the check and the payment the product may be sold offline: a refusal at payment is handled as well.

### In what form is the invoice returned

**The public API does not return a ready-made invoice or a printable form for a sale.**

| What is needed        | What covers it                                                                                                      |
| --------------------- | ------------------------------------------------------------------------------------------------------------------- |
| Own printable form    | `GET /v2/order/:id` — line items, prices, discounts, payments, shop, customer; layout on the external system’s side |
| Export for accounting | reports `/v1/general-report-table`, `/v1/transaction-report-table`                                                  |

### Which identifier to store

The **sale UUID** (`id` from the response envelope at step 2). `order_number` is intended for correspondence with support.

1. The mapping “external system order number → BILLZ sale UUID” is stored on the external system’s side; **the UUID is saved right after step 2**, before adding products.
2. Before creating a sale, check whether a record for this order already exists.
3. The external system’s order number is passed in `comment` at payment: a duplicate is visible in the BILLZ interface and in reports.

## Permissions

A breakdown of `403` is in [troubleshooting](/en/troubleshooting/403-and-404/), the full list of what is available to the key is in the [reference](/en/reference/permissions/).

Issuing a key opens the basic set of methods; sensitive methods are opened separately, by configuring the key’s permissions.

## Method summary

| Step            | Method                                      | Type  |
| --------------- | ------------------------------------------- | ----- |
| List of shops   | `GET /v1/shop`                              | sync  |
| Payment types   | `GET /v1/company-payment-type`              | sync  |
| Find customer   | `GET /v1/client`                            | sync  |
| Customer card   | `GET /v1/customer/:id`                      | sync  |
| Create customer | `POST /v1/client`                           | sync  |
| Update customer | `PUT /v1/client/:id`                        | sync  |
| Create sale     | `POST /v2/order`                            | async |
| Add product     | `POST /v2/order-product/:order_id`          | async |
| Attach customer | `PUT /v2/order-customer-new/:order_id`      | async |
| Discount        | `POST /v2/order-manual-discount/:order_id`  | async |
| Verify total    | `POST /v1/recalculate-order-bill/:order_id` | sync  |
| Complete sale   | `POST /v2/order-payment/:order_id`          | sync  |
| Sale details    | `GET /v2/order/:id`                         | sync  |