# Refresh API

Подключите свою программу — сайт, интернет-магазин, 1С, таблицу, BI-систему или
ИИ-ассистента — к вашему магазину в Refresh. Читайте товары, остатки, клиентов,
продажи и любые отчёты (JSON или Excel); проводите продажи, принимайте и
перемещайте товар, пополняйте баланс клиентов; узнавайте об изменениях сразу, как
они происходят.

- **Базовый адрес:** `https://api.refresh.tj/public/v1`
- **Интерактивный справочник** (все методы, поля и коды): [/public/v1/reference](/public/v1/reference)
- **OpenAPI 3:** [/public/v1/openapi.json](/public/v1/openapi.json) · **Для ИИ-агентов:** [/public/v1/llms.txt](/public/v1/llms.txt)

## Быстрый старт

1. В Refresh откройте **Настройки → API ключи → Создать ключ**. Дайте название,
   отметьте, что ключ может читать и менять, выберите филиалы, создайте.
   Скопируйте ключ — он показывается один раз.
2. Вызовите API с ключом:

```bash
curl https://api.refresh.tj/public/v1/company \
  -H "Authorization: Bearer rk_ваш_ключ"
```

В ответе — ваш магазин и то, что может этот ключ (его права и филиалы).

## Ключи и что они видят

Ключ принадлежит одному магазину. Он видит только отмеченные при создании
**группы данных (права, scopes)** и **филиалы**, и **никогда больше, чем видит в
Refresh сотрудник, создавший ключ**: если сотрудник теряет право или филиал, их
теряет и ключ; если сотрудник уходит из магазина, ключ перестаёт работать
(`403 key_inactive`). Отозвать ключ можно в Настройках в любой момент — он
перестаёт работать сразу.

Передавайте его как `Authorization: Bearer rk_…` (или `X-Api-Key: rk_…`).
Храните ключ на своём сервере — никогда в странице сайта или мобильном приложении.

| Право | Ключ может |
|---|---|
| `catalog.read` | читать товары, варианты, штрихкоды, цены, категории |
| `inventory.read` | читать остатки по филиалам, перемещения, списания |
| `costs.read` | видеть закупочные цены, себестоимость и прибыль (иначе скрыты) |
| `customers.read` | читать клиентов |
| `sales.read` | читать продажи с позициями, оплатами и возвратами |
| `reports.read` | читать любые отчёты и выгружать их в Excel (для каждого нужно ещё право на сам отчёт в Refresh) |
| `stockcounts.read` | читать инвентаризации и их итоги |
| `purchasing.read` | читать поставщиков и заказы поставщикам |
| `gift_cards.read` | проверять баланс подарочной карты |
| `sales.write` | проводить продажи и возвраты |
| `customers.write` | создавать и менять клиентов, выдавать карты лояльности, пополнять баланс и выплачивать с него |
| `catalog.write` | создавать и менять товары |
| `transfers.write` | создавать, отправлять, принимать и отменять перемещения |
| `writeoffs.write` | оформлять, одобрять и отклонять списания |
| `purchasing.write` | создавать, отправлять и принимать заказы поставщикам |
| `gift_cards.write` | продавать, пополнять и аннулировать подарочные карты |
| `loyalty.write` | начислять и списывать кешбэк клиентов |

Доступ к API входит в тариф Pro.

## Общие правила

- **Списки** отвечают `{"data": [...], "next_cursor": "...", "has_more": true}`.
  Передайте `next_cursor` как `cursor` за следующей страницей; `limit` — 1–500
  (по умолчанию 100). Страницы идут по id, поэтому ничего не пропускается и не
  повторяется, пока появляются новые записи.
- **Синхронизация изменений:** большинство списков принимают `updated_since`
  (ISO 8601, например `2026-11-15T09:30:00Z`) и возвращают только изменённое с
  этого момента. Запомните время прошлой синхронизации и передайте его в
  следующий раз. У остатков нет `updated_since` — запрашивайте их целиком или
  подпишитесь на вебхук `stock.updated`.
- **Время** — UTC с буквой `Z` в конце. **Даты** (`date_from`, `date_to`) — это
  календарные дни магазина в его часовом поясе, включительно.
- **Деньги** — число с 2 знаками в валюте магазина (обычно TJS);
  **количества** — с 3 знаками (весовой товар).
- **Товары:** у обычного товара артикул, штрихкод и цена — на самом товаре, а
  `variants: []`; у товара с вариантами они у каждого варианта.
- **Ограничение:** 300 запросов в минуту на ключ. При превышении — `429` и
  заголовок `Retry-After`.

## Ошибки

Любая ошибка — `{"error": {"code": "...", "message": "..."}}`. Ориентируйтесь на
`code`; `message` написан для людей и может меняться.

| HTTP | code | Значит |
|---|---|---|
| 400 | `invalid_request` | параметр не передан или неверен (`fields` — какой) |
| 401 | `unauthorized` | нет ключа, ключ неверный, отозван или истёк |
| 402 | `subscription_inactive` | подписка магазина не активна |
| 403 | `plan_upgrade_required` | тариф магазина этого не включает |
| 403 | `insufficient_scope` / `forbidden` / `key_inactive` | ключу или его создателю это нельзя |
| 404 | `not_found` | записи нет — или ключ её не видит |
| 409 | `conflict` и конкретные коды | запись в состоянии, которое этого не позволяет |
| 422 | конкретные коды | например `unknown_product`, `account_not_allowed`, `insufficient_…_stock` |
| 429 | `rate_limited` | слишком часто — повторите через `Retry-After` секунд |

## Запись данных

Каждый изменяющий запрос (POST, PATCH) требует заголовок **`Idempotency-Key`** —
уникальное значение на каждую операцию, например UUID. Если связь оборвалась,
отправьте тот же запрос с тем же ключом ещё раз: вернётся первый ответ
(`Idempotent-Replayed: true`), и ничего не выполнится дважды. Тот же ключ с
другим телом запроса отклоняется (`idempotency_key_reused`). Ключи хранятся 7 дней.

Запись выполняется тем же кодом, что и касса Refresh, и записывается на
сотрудника, создавшего ключ, — поэтому действуют правила магазина: остатки
следуют настройке «Разрешить отрицательный остаток», скидки — политике кассиров
и т. д.

### Продажа одним запросом

```bash
curl -X POST https://api.refresh.tj/public/v1/sales \
  -H "Authorization: Bearer rk_ваш_ключ" \
  -H "Idempotency-Key: 6f1c2e8a-6c55-4b7b-9a2e-1f0d6c1b1a01" \
  -H "Content-Type: application/json" \
  -d '{
    "branch_id": 5,
    "lines": [{"sku": "MD-1001", "quantity": 2}, {"barcode": "4600000999991", "quantity": 1}],
    "payments": [{"account_id": 7}],
    "external_id": "WEB-1001"
  }'
```

- Позиция указывает товар через `product_id`, `variant_id`, `sku` или `barcode`.
- Одна оплата **без** `amount` — оплачено полностью на этот счёт. Оплаты **с**
  суммами требуют `customer_id`; неоплаченный остаток становится долгом клиента
  (укажите `debt_due_date`).
- Деньги поступают только на **безналичные** счета (карта, банк, кошелёк…)
  филиала продажи — см. `GET /payment-methods`. Наличные относятся к смене кассира.
- **Подарочная карта:** добавьте `"gift_card": {"code": "…"}` (можно с `amount`).
  Карта платит первой — `amount` или сколько на ней есть, — а `payments`
  покрывают остаток по правилам выше; карта может оплатить и всю продажу. Если её
  не хватает, а долг записать не на кого, продажа отклоняется (`payment_short`)
  и баланс карты не меняется.
- В Refresh продажа отмечена «Через API».

Возврат: `POST /sales/{id}/refunds` — целиком или
`{"lines": [{"line_id": …, "quantity": …}]}`. Деньги возвращаются на тот счёт,
куда поступила оплата; доля подарочной карты — обратно на карту.

### Клиенты

`POST /customers` с `first_name` и `phone` создаёт клиента — а если клиент с
таким телефоном уже есть, возвращает его с `"created": false` вместо дубликата.
`PATCH /customers/{id}` меняет только переданные поля.

**Баланс и кешбэк.** `GET /customers/{id}/balance` показывает баланс клиента,
кешбэк и историю баланса. `POST /customers/{id}/balance/deposits` пополняет
баланс — `"kind": "top_up"` с безналичным `account_id`, куда пришли деньги, или
`"kind": "gift"` — бонус без денег. `POST /customers/{id}/balance/withdrawals`
выплачивает часть баланса со счёта. `POST /customers/{id}/cashback/adjustments` со
знаковой `amount` и `reason` начисляет или списывает кешбэк (`loyalty.write`);
он не уходит ниже 0, а каждое изменение видно в журнале действий магазина.

### Приход, перемещение, списание

- **Принять товар:** `POST /purchase-orders` с `supplier_id`, `branch_id`,
  `lines` (товар + `quantity` + `unit_cost`) и `"receive": true` — товар сразу на
  остатке по этой цене, долг поставщику записан. Без `receive` заказ остаётся
  черновиком (`"issue": true` отправляет его поставщику); принять позже —
  `POST /purchase-orders/{id}/receive`.
- **Переместить между филиалами:** `POST /transfers` с `from_branch_id`,
  `to_branch_id` и `lines` создаёт и отправляет перемещение;
  `POST /transfers/{id}/receive` принимает его (укажите недостающие позиции или
  отправьте `{}`, если пришло всё).
- **Списать:** `POST /write-offs` с `branch_id`, `reason` и `lines`;
  `"approve": true` сразу одобряет.

Запрос с несколькими позициями выполняется целиком или никак: если позиция не
прошла, созданное отменяется.

### Товары и подарочные карты

`POST /products` создаёт обычный товар (`name`, `price`, а также `sku`,
`barcode`, `supply_price`, `category_ids`…); `PATCH /products/{id}` меняет его,
`{"is_archived": true}` отправляет в архив. Остаток напрямую не задаётся — он
приходит приёмкой или перемещением.

Товар с вариантами создаётся в Refresh; через API `PATCH /variants/{id}` меняет
у одного варианта `sku`, `barcode`, `price`, `wholesale_price`, `supply_price`
или `is_archived`.

**Свои поля** — характеристики товаров магазина (материал, страна, бренд…) —
перечислены в `GET /custom-fields` и приходят с каждым товаром в
`custom_fields`. Задаются при создании или изменении:
`"custom_fields": [{"id": 12, "value": "Хлопок"}]` — меняются только названные
поля, `null` очищает поле, а поле-список принимает id варианта или его точный текст.

`POST /gift-cards` продаёт карту (`code`, `amount`, `branch_id`, безналичный
`account_id`); `GET /gift-cards/{code}` показывает баланс; `/reload` пополняет,
`/void` аннулирует; `POST /gift-cards/batches` создаёт непроданные карты для печати.

## Отчёты

`GET /reports` перечисляет все отчёты и показывает, какие доступны этому ключу.
Каждый отчёт — тот же запрос, что и его экран в Refresh: те же цифры для тех же
филиалов и дней:

`store`, `payments`, `products`, `sales-by-category`, `sales-by-supplier`,
`stock`, `receipts`, `transfers`, `write-offs`, `returns`, `stock-counts`,
`product-performance`, `abc`, `unsold-products`, `sellers`, `sales-by-seller`,
`customers`, `customer-purchases`, `receivables`, `top-debtors`,
`overdue-debts`, `cart-audit`, `profit-and-loss`.

```bash
curl "https://api.refresh.tj/public/v1/reports/products?date_from=2026-11-01&date_to=2026-11-15" \
  -H "Authorization: Bearer rk_ваш_ключ"
```

Страницы до 200 строк (`cursor`, `limit`); фильтры `branch_id`, `category_id`,
`search` и другие — там, где они есть на экране. Добавьте `/export` — весь отчёт
файлом Excel (`GET /reports/abc/export?...`, до 10 000 строк; создателю ключа
нужно право «Скачать отчеты»). Без `costs.read` столбцы себестоимости, прибыли и
маржи убираются и там и там, а `profit-and-loss` недоступен.
`GET /reports/sales-summary` и `/reports/sales-by-product` остаются как краткие версии.

**Инвентаризации:** `GET /stock-counts` и `GET /stock-counts/{id}` — каждая с
позициями: ожидалось, посчитано, разница (`stockcounts.read`).

## Вебхуки

Refresh может отправлять событие на ваш сервер, когда что-то меняется — на
кассе, в программе или через этот API. Добавьте вебхук в
**Настройки → API ключи → Вебхуки**: адрес `https://` и нужные события.
Кнопка «Отправить тестовое событие» проверит сервер.

| Событие | Когда |
|---|---|
| `sale.created`, `sale.updated` | продажа проведена; изменилась (возврат, оплата) |
| `customer.created`, `customer.updated`, `customer.deleted` | изменились данные клиента |
| `product.created`, `product.updated` | изменился товар или его вариант |
| `stock.updated` | изменился остаток товара в филиале |

Тело запроса:

```json
{
  "event_id": "evt_afef8ae2fe2c4f688a841ec7a126eb95",
  "type": "sale.created",
  "created_at": "2026-11-15T09:28:08Z",
  "shop_id": 3,
  "data": {"type": "sale", "id": 923, "object": { "...": "продажа, как её возвращает GET /sales/923" }}
}
```

**Ответьте любым 2xx в течение 10 секунд.** Иначе Refresh повторит через 1, 2,
4, 8, 16, 32 и 60 минут, затем через 2, 4, 8 и 8 часов — в течение 24 часов.
События могут прийти больше одного раза и в любом порядке: пропускайте
`event_id`, который уже обработали. Вебхук получает только то, что видит его
создатель; закупочные цены и прибыль не отправляются никогда.

**Проверяйте подпись** каждого запроса. `X-Refresh-Signature` — это Base64 от
HMAC-SHA256 адреса вебхука и следом сырого тела запроса, ключ — секрет подписи
вебхука (Настройки → Вебхуки → «Секрет подписи»):

```python
import base64, hashlib, hmac

def is_from_refresh(secret: str, url: str, raw_body: bytes, signature: str) -> bool:
    expected = base64.b64encode(hmac.new(secret.encode(), url.encode() + raw_body, hashlib.sha256).digest()).decode()
    return hmac.compare_digest(expected, signature)
```

```javascript
const crypto = require("crypto");
function isFromRefresh(secret, url, rawBody, signature) {
  const expected = crypto.createHmac("sha256", secret).update(url).update(rawBody).digest("base64");
  return expected.length === signature.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
```

```php
function is_from_refresh($secret, $url, $rawBody, $signature) {
  $expected = base64_encode(hash_hmac('sha256', $url . $rawBody, $secret, true));
  return hash_equals($expected, $signature);
}
```

Берите адрес точно таким, как он сохранён в Refresh, а тело — точно таким, как
оно пришло. Другие заголовки: `X-Refresh-Event-Id`, `X-Refresh-Event-Type`,
`X-Refresh-Retry-Number`, `X-Refresh-Retry-Reason`.

## ИИ-ассистенты (MCP)

Refresh — ещё и **MCP-сервер** только для чтения: ИИ-ассистент может отвечать на
вопросы о магазине по живым данным — «что лучше всего продавалось на прошлой
неделе?», «кто больше всех нам должен?», «какие товары заканчиваются?».

**claude.ai или ChatGPT (в браузере):** добавьте свой коннектор с адресом
`https://api.refresh.tj/public/v1/mcp`. Приложение отправит вас войти в Refresh;
выберите, что ему можно читать и какие филиалы, и разрешите. Оно только читает,
никогда не больше, чем вы, и появляется в **Настройки → API ключи** — там его
можно отключить в любой момент. (OAuth 2.1 с PKCE и динамической регистрацией
клиента; описание — `/.well-known/oauth-protected-resource/public/v1/mcp`.
Подключение, которым не пользовались 30 дней, прекращается.)

**Инструменты разработчика** вместо этого принимают API ключ — дайте ключу только
те права на чтение, которые нужны ассистенту.

**Claude Code:**

```bash
claude mcp add --transport http refresh https://api.refresh.tj/public/v1/mcp \
  --header "Authorization: Bearer rk_ваш_ключ"
```

**Cursor** (`.cursor/mcp.json`) или любой клиент, принимающий адрес и заголовки:

```json
{"mcpServers": {"refresh": {"url": "https://api.refresh.tj/public/v1/mcp",
  "headers": {"Authorization": "Bearer rk_ваш_ключ"}}}}
```

**Claude Desktop** (`claude_desktop_config.json`, через мост `mcp-remote`):

```json
{"mcpServers": {"refresh": {"command": "npx", "args": ["mcp-remote",
  "https://api.refresh.tj/public/v1/mcp", "--header", "Authorization: Bearer rk_ваш_ключ"]}}}
```

Инструменты: `shop_overview`, `sales_summary`, `query` (группировка и фильтры по
продажам, позициям, остаткам, долгам и клиентам), `top_sellers`, `list_sales`,
`get_sale`, `find_products`, `find_customers`. Даты и часы — магазина.

## Готовые сценарии

**Остатки интернет-магазина в актуальном виде.** Подпишите вебхук на
`stock.updated`, а раз в ночь пройдите `GET /stock?branch_id=…`, чтобы поправить
пропущенное.

**Заказы с сайта — в Refresh.** При оформлении отправьте `POST /sales` с
позициями заказа, его номером в `external_id` и счётом карты или кошелька, куда
пришла оплата. Используйте номер заказа как `Idempotency-Key` — повторный запрос
никогда не проведёт его дважды.

**Ежедневный отчёт в таблицу.** Каждое утро
`GET /reports/sales-summary?date_from=ВЧЕРА&date_to=ВЧЕРА` — те же цифры, что в
отчёте по магазину в Refresh.

## Изменения

Версия API — в адресе. Внутри `/public/v1` поля и методы только добавляются,
не удаляются и не меняют смысл; несовместимые изменения выйдут как `/public/v2`.
Игнорируйте поля, которых не знаете.
