# HTTP API

> Транспорт, авторизация и полный список маршрутов /v1 — запрос, тело, ответ, коды ошибок.

> [!TIP]
> Машиночитаемое описание всех маршрутов — [/openapi.json](/openapi.json) (OpenAPI 3.1).
> Схему с полями конкретной интеграции печатает [`sparku openapi <адрес>`](/docs/api/ru/cli.md#openapi).

## Транспорт

SparkU слушает HTTP/1.1 на UNIX-сокете `~/.sparku/run/api.sock`. Сокет существует,
только пока в Настройках включены «Свои интеграции» и SparkU запущен — если файла
нет, функция выключена или приложение не работает (см. [Если не работает](/docs/api/ru/troubleshooting.md)).

Права на сокет — `0700` на каталог и `0600` на файл: доступ только тому же
пользователю. Никакого TCP-порта нет, поэтому запросы можно слать только с этого же
Mac.

```bash
curl --unix-socket ~/.sparku/run/api.sock http://sparku/v1/health
```

Хост в URL (`http://sparku/...`) не резолвится — соединение всё равно идёт через
сокет, имя нужно только `curl`, чтобы собрать корректный запрос.

Тело запросов и ответов — JSON (`Content-Type: application/json; charset=utf-8`).
Кодировка тела — `Content-Length`; `Transfer-Encoding: chunked` не поддерживается.

## Авторизация

Все маршруты интеграции (`/v1/integrations/{slug}/...`) требуют заголовок:

```
Authorization: Bearer <токен>
```

Токен выдаётся на интеграцию при её создании (Настройки → Свои интеграции →
«slug» → Подключение) или при импорте `.sparku` ([Файлы .sparku и импорт](/docs/api/ru/packages.md)). Без
заголовка — `401 unauthorized`, с чужим или отозванным токеном — `401 invalid_token`.

`GET /v1/health` и `POST /v1/imports` токена не требуют: первому нечего защищать, а
второй ещё не знает, к какой интеграции относится запрос — интеграции пока нет.

## Маршруты

| Метод | Путь | Назначение |
|---|---|---|
| `GET` | `/v1/health` | версия протокола и приложения |
| `GET` | `/v1/integrations/{slug}` | схема полей и текущие значения |
| `POST` | `/v1/integrations/{slug}/fields` | частичная запись значений (upsert) |
| `DELETE` | `/v1/integrations/{slug}/fields` | очистить значения |
| `POST` | `/v1/integrations/{slug}/notify` | вспышка и строка на несколько секунд |
| `POST` | `/v1/integrations/{slug}/glow` | пятна света в заданных местах контура |
| `DELETE` | `/v1/integrations/{slug}/glow` | погасить пятна |
| `POST` | `/v1/integrations/{slug}/dialogs` | вопрос человеку — [Вопросы человеку](/docs/api/ru/dialogs.md) |
| `GET` | `/v1/integrations/{slug}/events` | поток нажатий (SSE) — [Кнопки и события](/docs/api/ru/events.md) |
| `POST` | `/v1/imports` | импорт `.sparku` — [Файлы .sparku и импорт](/docs/api/ru/packages.md) |

Метод, которого маршрут не поддерживает, отвечает `405 method_not_allowed` с
заголовком `Allow`. Неизвестный путь под `/v1` — `404 not_found`.

### `GET /v1/health`

Без токена. Не проверяет, что интеграция существует, — просто «SparkU здесь».

```bash
curl --unix-socket ~/.sparku/run/api.sock http://sparku/v1/health
```

```json
{"status": "ok", "api_version": 1, "app_version": "2.4.0"}
```

### `GET /v1/integrations/{slug}`

Схема полей интеграции и текущие значения (если они ещё не истекли по TTL).

```bash
curl --unix-socket ~/.sparku/run/api.sock \
  -H "Authorization: Bearer $SPARKU_TOKEN_DEPLOY" \
  http://sparku/v1/integrations/deploy
```

```json
{
  "slug": "deploy",
  "name": "Деплой",
  "enabled": true,
  "fields": [
    {"key": "progress", "type": "progress", "label": "Прогресс"},
    {"key": "stage", "type": "state", "label": "Этап", "states": ["running", "done", "failed"]},
    {"key": "title", "type": "text", "label": "Сервис", "max_length": 60}
  ],
  "values": {"progress": 0.4, "stage": "running", "title": "api · prod"},
  "updated_at": "2026-09-28T10:15:00Z",
  "expires_at": "2026-09-28T11:15:00Z"
}
```

`expires_at` в ответе нет, если у значений нет срока жизни. Если значений ещё не
было или они истекли, `values` — пустой объект, `updated_at`/`expires_at` отсутствуют.

Коды: `200`, `404 unknown_integration`, `401`.

### `POST /v1/integrations/{slug}/fields`

Частичная запись значений — upsert: перечисленные поля меняются, остальные не
трогаются. `null` у поля очищает его. Запись **всё или ничего**: если хоть одно
поле не проходит проверку, не применяется ни одно.

```bash
curl --unix-socket ~/.sparku/run/api.sock \
  -H "Authorization: Bearer $SPARKU_TOKEN_DEPLOY" \
  -H "Content-Type: application/json" \
  -d '{"progress": 0.72, "stage": "running", "title": "api · prod"}' \
  http://sparku/v1/integrations/deploy/fields
```

```json
{"ok": true, "updated": ["progress", "stage", "title"], "cleared": []}
```

Типы значений в теле — см. [Типы полей](/docs/api/ru/fields.md). Тело `{"progress": null}`
попадает в `cleared`, а не в `updated`.

**`?ttl=`** — на сколько запомнить значения этой записи: число секунд (`?ttl=90`)
или относительное время (`?ttl=5m`, `?ttl=1h30m`), не больше 7 дней. Без параметра
действует срок жизни из настроек интеграции (по умолчанию — «до перезапуска
SparkU»). Отсчёт начинается заново от каждой записи. Если срок задан, ответ
дополнительно содержит `expires_at`:

```json
{"ok": true, "updated": ["progress"], "cleared": [], "expires_at": "2026-09-28T11:20:00Z"}
```

**`?bind=`** — привязать значения к процессу: `process` (тот, что прислал запрос),
PID (`?bind=$$` из shell) или `none` (снять привязку). Процесс завершился — значения
сняты. Ответ с привязкой содержит `bound_pid`. Подробнее —
[привязка к процессу](/docs/api/ru/concepts.md#привязка-к-процессу).

Коды: `200`, `400 empty_body`, `400 invalid_json`, `400 invalid_ttl`, `400 invalid_bind`,
`409 integration_disabled`, `422 invalid_fields`, `401`, `404`.

`422 invalid_fields` перечисляет каждое неверное поле:

```json
{"error": {"code": "invalid_fields", "message": "progress: ожидалось 0…1, пришло 70",
 "fields": [{"field": "progress", "message": "progress: ожидалось 0…1, пришло 70"}]}}
```

### `DELETE /v1/integrations/{slug}/fields`

Очищает все значения интеграции — эквивалент `sparku end`. Ответ — `204 No
Content`, тела нет.

```bash
curl --unix-socket ~/.sparku/run/api.sock -X DELETE \
  -H "Authorization: Bearer $SPARKU_TOKEN_DEPLOY" \
  http://sparku/v1/integrations/deploy/fields
```

Коды: `204`, `401`, `404`. Работает и для выключенной интеграции.

### `POST /v1/integrations/{slug}/notify`

Вспышка на окантовке и/или переключение строки на несколько секунд. Нужно хотя бы
одно из `text` и `state`.

```bash
curl --unix-socket ~/.sparku/run/api.sock \
  -H "Authorization: Bearer $SPARKU_TOKEN_DEPLOY" \
  -d '{"text": "Сборка упала", "state": "failed", "color": "red", "duration": 4}' \
  http://sparku/v1/integrations/deploy/notify
```

| Поле | Тип | Правило |
|---|---|---|
| `text` | строка | обрезается до 120 символов (с `…`) |
| `state` | строка | значение любого поля типа `state` у интеграции |
| `color` | строка | одно из семантических имён (`blue`, `red`, `success`, …) |
| `duration` | число | 1…10 секунд, по умолчанию 4 |

```json
{"ok": true}
```

Не чаще раза в 2 секунды на интеграцию: следующий вызов раньше — `429
too_many_notices` с заголовком `Retry-After` (секунды до разрешённого вызова).

Коды: `200`, `400 invalid_json`, `409 integration_disabled`, `409 notify_disabled`
(notify выключен в настройках интеграции), `422 invalid_notice`, `429
too_many_notices`, `401`, `404`.

### `POST /v1/integrations/{slug}/glow`

Координатная подсветка: свет загорается в заданных местах контура островка и стоит
там, пока не придёт следующий кадр. Подходит для визуализации звука, нескольких
параллельных задач «по участкам», шкал.

```bash
curl --unix-socket ~/.sparku/run/api.sock \
  -H "Authorization: Bearer $SPARKU_TOKEN_DEPLOY" \
  -d '{"spots": [{"position": 0.2, "color": "green"},
                 {"position": 0.8, "color": "#FF3B30", "intensity": 0.6, "width": 0.15}]}' \
  "http://sparku/v1/integrations/deploy/glow?ttl=10"
```

| Поле | Тип | Правило |
|---|---|---|
| `spots` | массив | обязателен, не больше 64 пятен; пустой массив — темно |
| `spots[].position` | число | 0…1 слева направо по ширине островка, 0.5 — середина под вырезом |
| `spots[].color` | строка | семантическое имя (`blue`, `red`, `success`, …) или `#RRGGBB` |
| `spots[].intensity` | число | 0…1, по умолчанию 1 |
| `spots[].width` | число | полуширина пятна в долях ширины островка, 0.005…1, по умолчанию 0.08 |
| `intensity` | число | толщина обводки и сила свечения, 0.2…3, по умолчанию 1 |

```json
{"ok": true}
```

- Каждый запрос заменяет кадр целиком. Соседние пятна, которые перекрываются,
  смешивают цвета.
- Частота не ограничена: шлите кадры хоть 60 раз в секунду, на островок попадёт не
  больше ~30.
- `?ttl=` — сколько кадр горит без следующего: секунды или `5m` / `1h30m`, по
  умолчанию 5 секунд. Скрипт упал — подсветка гаснет сама.
- Пока горит вспышка или статус другого виджета, она принимает форму ваших пятен:
  цвета её, а яркость вдоль контура — от пятен (не ниже 30 %). Между интеграциями
  с пятнами решает приоритет интеграции.
- `DELETE …/glow` и `sparku end` гасят пятна сразу.

Коды: `200`, `400 invalid_ttl`, `409 integration_disabled`, `422 invalid_glow`, `401`,
`404`.

### `POST /v1/integrations/{slug}/dialogs`

Вопрос человеку на островке. Подробности — [Вопросы человеку](/docs/api/ru/dialogs.md).

### `GET /v1/integrations/{slug}/events`

Поток нажатий в карточке (SSE). Подробности — [Кнопки и события](/docs/api/ru/events.md).

### `POST /v1/imports`

Импорт `.sparku`. Подробности — [Файлы .sparku и импорт](/docs/api/ru/packages.md).

## Единый формат ошибок

Любая ошибка — JSON вида:

```json
{"error": {"code": "invalid_fields", "message": "человекочитаемый текст"}}
```

`message` пишется для человека и рассчитан на то, что клиент — чаще всего
shell-скрипт: текст можно выводить в терминал как есть. Некоторые ошибки добавляют
поля в объект `error` (например `fields` у `invalid_fields`).

## Коды ошибок

| Код | HTTP | Где |
|---|---|---|
| `not_found` | 404 | путь не под `/v1` или не существует |
| `unknown_integration` | 404 | нет интеграции с таким `slug` |
| `unauthorized` | 401 | нет заголовка `Authorization: Bearer …` |
| `invalid_token` | 401 | токен не подходит к интеграции |
| `method_not_allowed` | 405 | метод не поддерживается на этом пути |
| `empty_body` | 400 | тело запроса пустое (`fields`) |
| `invalid_json` | 400 | тело — не JSON-объект |
| `invalid_ttl` | 400 | `?ttl=` не разобрать |
| `invalid_bind` | 400 | `?bind=` не `process`, `none` или PID живого процесса |
| `invalid_fields` | 422 | значение(я) не прошли проверку типа |
| `invalid_notice` | 422 | `notify`: неверные `text`/`state`/`color`/`duration` |
| `invalid_glow` | 422 | `glow`: неверные пятна, цвет или интенсивность |
| `invalid_dialog` | 422 | `dialogs`: тело вопроса не разобрать |
| `invalid_package` | 422 | `imports`: файл `.sparku` не разобрать |
| `integration_disabled` | 409 | интеграция выключена в Настройках |
| `notify_disabled` | 409 | notify выключен в настройках интеграции |
| `dialog_pending` | 409 | у интеграции уже есть вопрос без ответа |
| `import_pending` | 409 | SparkU уже предлагает человеку другой импорт |
| `import_declined` | 403 | человек отклонил предложенную интеграцию |
| `too_many_notices` | 429 | `notify` чаще раза в 2 секунды |
| `too_many_connections` | 503 | больше 64 одновременных соединений |
| `unavailable` | 503 | SparkU ещё не готов принимать запросы |
| `withdrawn` | 503 | SparkU закрывается или API выключили во время ожидания |
| `bad_request` | 400 / 411 / 413 / 431 / 505 | нарушение HTTP: плохие заголовки, нет `Content-Length`, тело больше 1 МБ, слишком большие заголовки, неподдерживаемая версия |

> [!NOTE]
> `bad_request` — единый код для ошибок разбора HTTP; конкретную причину и статус
> смотрите в `message`.

## Коалесценция и лимиты

- **`fields`** принимает запросы без ограничения частоты, но на островок попадает
  не чаще одного обновления за кадр отрисовки — быстрый цикл `push` не мигает
  промежуточными значениями.
- **`notify`** — не чаще раза в 2 секунды на интеграцию (`429` при превышении).
- **`glow`** — без ограничения частоты, на островок попадает не больше ~30 кадров в
  секунду.
- **Диалог** — не больше одного ожидающего вопроса на интеграцию (`409
  dialog_pending`).
- **Импорт** — не больше одного предложения на всё приложение одновременно (`409
  import_pending`).
- **Соединения** — не больше 64 одновременных на весь сервер (`503
  too_many_connections`).
- **Тело запроса** — не больше 1 МБ (лимит рассчитан на поле `image` в base64).
- **История чисел** для графика и тренда — не больше точки в секунду, 120 точек на
  поле, только в памяти процесса SparkU.

## Совместимость `/v1`

Внутри `/v1` изменения только добавляющие: новые маршруты и новые поля ответа, но
не изменение уже существующего поведения. Скрипт, написанный сегодня, продолжит
работать после обновления SparkU без правок.
