# Справочник ручек

Все ответы — JSON. Удачный ответ несёт `"ok": true`, отказ — `"ok": false`
и объект `error` (см. [errors.md](errors.md)).

Авторизация на всех ручках, кроме `GET /v1/health` и `GET /r/{token}`:

```
Authorization: Bearer pk_…
```

---

## `GET /v1/health`

Без ключа. Ставьте в мониторинг.

```json
{ "ok": true, "version": 1, "model": "deepseek/deepseek-v4-pro-0813" }
```

`model` — какой моделью сервис сейчас работает. Меняется без предупреждения:
это наш выбор по цене за сошедшуюся просьбу, а не часть договора.

---

## `GET /v1/usage`

Остаток монет учётной записи, на которую выдан ключ.

```json
{ "ok": true, "usage": {
  "plan": "seat", "status": "active",
  "coins_left": 940, "coins_used": 60, "coins_included": 1000,
  "extra_coins": 0,
  "period_end": "2026-10-01T00:00:00.000Z",
  "cabinet": "https://teril.example.ru/cabinet" } }
```

`status` — `trial`, `active`, `past_due` или `canceled`. Монеты добирает
мебельная компания в своём кабинете; вы можете только увидеть остаток.

---

## `POST /v1/conversations`

Завести разговор — или получить уже заведённый.

```json
{
  "external_id": "lead-7781",
  "order_id": "кухня-иванов",
  "webhook_url": "https://ваш-сервис/hooks/teril",
  "meta": { "источник": "сайт", "менеджер": 12 }
}
```

| Поле | Обяз. | Что это |
|---|---|---|
| `external_id` | нет | Ваш id лида. По нему разговор находится повторно |
| `order_id` | нет | Имя заказа. Не задано — `lead-<external_id>` |
| `webhook_url` | нет | Куда слать исход. Не задан — общий адрес партнёра |
| `meta` | нет | Любой ваш JSON. Мы его храним и не читаем |

**201** — завели новый. **200** — вернули тот же по `external_id`.

```json
{ "ok": true, "conversation": {
  "id": "3f2b…", "external_id": "lead-7781", "order_id": "кухня-иванов",
  "turn": 0, "running_turn": null, "has_project": false,
  "expires_at": "2026-10-18T09:11:00.000Z" } }
```

`running_turn` — id идущего хода или `null`. `has_project` — собрано ли уже
что-нибудь.

---

## `GET /v1/conversations/{id}`

Тот же объект `conversation`. Разговор чужого партнёра не находится вовсе
(`404`).

---

## `DELETE /v1/conversations/{id}`

**204**, без тела. Удаляются и переписка, и картинки, и история ходов.
Необратимо. Делайте, когда лид закрыт: сам разговор и так умрёт через 30 дней
после последней просьбы.

---

## `POST /v1/conversations/{id}/turns`

Просьба клиента. Отвечает **202**.

```json
{
  "text": "Кухня 3 метра вдоль окна, мойка под окном",
  "images": [{ "mime": "image/png", "b64": "iVBORw0KGgo…", "name": "план.png" }]
}
```

| Поле | Обяз. | Что это |
|---|---|---|
| `text` | **да** | Слова клиента как есть, по-русски |
| `images` | нет | До двух картинок, каждая ≤ 5 МБ. `mime`: `image/png`, `image/jpeg`, `image/webp`, `image/gif`. `name` можно слать — он принимается и никуда не идёт |

Заголовок `Idempotency-Key` (рекомендуется): повтор с тем же ключом вернёт
**тот же** ход и не купит второго вопроса модели. Без него повтор — это второй
ход и вторые деньги.

```json
{ "ok": true, "turn": { "id": "b41c…", "state": "queued" } }
```

**409 `conflict_busy`** — по этому разговору уже идёт ход. Дождитесь исхода
или остановите его.

---

## `GET /v1/turns/{id}`

Состояние и исход хода. Запасной путь к тому, что приходит вебхуком.

```json
{ "ok": true, "turn": {
  "id": "b41c…",
  "state": "done",
  "status": "built",
  "result": { "…": "см. turns.md" } } }
```

Поля `status`, `result` и `error` появляются по мере того, как им есть что
сказать: у идущего хода их нет вовсе, у неудачного вместо `result` приходит
`error`. Не полагайтесь на то, что поле present-but-null — его просто нет.

`state`: `queued` → `running` → `done` | `stopped` | `failed`.

Опрашивать имеет смысл раз в 5–10 секунд и не дольше десяти минут.

---

## `POST /v1/turns/{id}/stop`

Оборвать идущий ход: клиент передумал, дописал, ушёл.

```json
{ "ok": true, "turn_id": "b41c…", "state": "running" }
```

**409** — ход уже кончился либо идёт на другом узле сервиса и остановить его
нечем. Второй случай честно говорит об этом текстом: мы не притворяемся,
что остановили.

Остановленный ход **не списывает монет**.

---

## `GET /v1/conversations/{id}/project`

Документ заказа — то, из чего собирается мебель. Его читает проектировщик
и наши же инструменты; вам он нужен, если вы храните состояние у себя или
показываете состав словами.

```json
{ "ok": true, "project": {
  "id": "кухня-иванов", "kind": "kitchen", "ceiling": 2700,
  "room": { "walls": [ { "id": "задняя", "turn": "right", "length": 3000 } ] },
  "runs": [ { "id": "задняя-низ-1", "wall": "задняя", "tier": "base",
              "countertop": true, "plinth": true,
              "nodes": [ { "id": "мойка", "at": "module", "code": "sink-straight", "w": 800 } ] } ],
  "items": [], "obstacles": [] } }
```

**404**, пока в разговоре ничего не собрано.

Документ большой и подробный; разбирать его целиком не нужно. Практически
полезны `runs[].nodes[]` — состав по порядку, у каждого узла `id`, `code`
(модуль каталога) и `w` (ширина в миллиметрах).

---

## `GET /v1/conversations/{id}/bom`

Расход материалов. Подробно — [bom.md](bom.md).

```json
{ "ok": true, "order_id": "кухня-иванов", "turn": 3, "bom": {
  "sheets": [ { "material": "ldsp16", "name": "ЛДСП 16 мм", "parts": 78, "area_mm2": 15253450 } ],
  "linear": [],
  "edges":  [ { "edge": "pvc1carcass", "name": "Кромка корпуса", "length_mm": 70164 } ],
  "pieces": [ { "article": "LEG-ADJ-100", "name": "Опора регулируемая 100", "qty": 22 } ] } }
```

---

## `GET /v1/conversations/{id}/render`

Объёмный вид заказа.

**Вид принадлежит РАЗГОВОРУ, а не ходу.** Он доступен после ЛЮБОГО хода, пока
в разговоре что-то собрано, и просить его можно когда угодно — хоть перед
первым ходом дня. Статус последнего хода на это не влияет вовсе: `question`
и `rejected` ничего не разобрали, и картинка после них ровно та же, что была.
Не собрано ещё ничего — **404 `not_found`**.

| Параметр | Значения | По умолчанию |
|---|---|---|
| `format` | `svg`, `html` | `svg` |
| `view` | `front`, `iso` | `front` |
| `carcass` | выкрас из палитры | `clear` |
| `facade` | выкрас из палитры | `clear` |
| `link` | `1` | нет |

Без `link` — тело: `image/svg+xml` или `text/html`.

С `link=1` — подписанная ссылка вместо тела:

```json
{ "ok": true, "url": "https://teril.example.ru/r/eyJ…", "expires_at": "2026-09-19T…" }
```

`carcass` и `facade` красят вид СПЛОШНЫМИ ПАНЕЛЯМИ — порознь корпус и фасад,
потому что порознь их и называют («белый верх, дуб низ»). Выкрасы:

`clear`, `white`, `milk`, `sand`, `oak`, `walnut`, `wenge`, `grey`, `graphite`,
`black`, `olive`, `blue`.

`clear` — не цвет, а его отсутствие: вид остаётся чертежом, залитым бумагой
и обведённым краской, и это же по умолчанию. Неизвестное имя — **400
`invalid_request`** со списком: опечатка в цвете иначе уехала бы клиенту
картинкой не того тона, и узнали бы вы об этом от него.

Цвет здесь ВРЁТ НАРОЧНО и ничего не решает: декор выбирают по выкрасу
у поставщика, а не по экрану телефона, — поэтому имена родовые, а не артикулы.
Геометрия при этом та же самая, из которой выйдут детали.

На странице (`format=html`) палитра идёт строкой кнопок под видом: клиент
перебирает цвета сам, не ходя к вам за новой ссылкой. Выбранное в запросе —
то, что он увидит первым; там, где скриптов нет вовсе (предпросмотр
в мессенджере), останется оно же. Подписанная ссылка несёт выкрасы в себе:
параметром их клиенту не дать — подпись покрывает только тело.

`view=iso` показывает и маркеры техники (место под холодильник, трубу, котёл);
`front` — только мебель. Клиенту обычно нужен `front`.

`format=png` отвечает **501 `png_unsupported`**: ради картинки, которая
получается из SVG, пришлось бы держать браузер в рантайме. Нужен растр —
сконвертируйте SVG у себя.

---

## `GET /r/{token}`

Страница вида по подписанной ссылке. **Без ключа** — её открывает ваш клиент.

Страница самодостаточна: чертёж внутри неё, наружу она не ходит ни за чем.
Живёт сутки; просроченная и подделанная отвечают одинаково — `404`.

Содержимое адресуется парой «разговор + номер хода», поэтому ссылка неизменна:
после следующего хода запросите новую.

---

## `POST /v1/conversations/{id}/handoff`

Передать заказ на рабочее место мебельной компании.

```json
{ "seat": "Цех на Ленина", "order_id": "кухня-иванов-2" }
```

`seat` — id места или его название из кабинета компании. `order_id` — имя,
под которым заказ ляжет на место; не задано — имя заказа разговора.

```json
{ "ok": true, "handoff": {
  "seat_id": 5, "seat_title": "Цех на Ленина",
  "order_id": "кухня-иванов-2", "turn": 3 } }
```

**403** — такого места у этой учётной записи нет либо оно отозвано.
**400** — в разговоре ещё ничего не собрано.

Что эта ручка делает и чего не делает — [handoff.md](handoff.md).
