# AGENTS.md — «Вручай» для ИИ-агента

Для агента, которому человек поручил работу с «Вручай»: как устроена предметная
область, в каком порядке действовать и обо что спотыкаются. Каждый шаг дан двумя
путями — инструментом MCP и запросом REST.

- Подключиться самому: [connect.md](connect.md). Все клиенты и протокол: [mcp.md](mcp.md).
- Вёрстка листа: [layout-guide.md](layout-guide.md).
- Способы выдачи: [integrations.md](integrations.md). Сценарии целиком: [recipes.md](recipes.md).
- Карта всех страниц: `https://vruchay.ru/llms.txt`; всё одним файлом: `https://vruchay.ru/llms-full.txt`.

## Что делает сервис

«Вручай» выпускает именные документы: сертификаты, грамоты, дипломы, благодарности.
На входе — макет листа и люди (таблицей, по одному через API или через форму),
на выходе — по PDF (или JPEG) на человека: с ФИО в нужном падеже, номером,
кодом и QR, ведущим на страницу проверки подлинности. Выданное попадает в реестр,
его можно разослать письмами, перевыпустить или отозвать.

Работа идёт от имени организации. Токен принадлежит ей, а не человеку, и видит
ровно её данные.

## Прежде чем писать свой код

Выдача уже решена готовыми способами — сначала подбери подходящий. Форма и API
сами принимают заявку, выпускают документ, списывают квоту, держат «один документ
в руки» и шлют письмо. Поэтому свой сервер, прокси между сайтом и «Вручай»,
выгрузка списков и хранение данных участников у себя не нужны.

| Задача | Готовое решение | MCP | REST |
| --- | --- | --- | --- |
| Участники получают документ на сайте: Тильда, WordPress, Битрикс, любая страница | Форма на сайте — кнопка «Получить документ», код вставки приходит готовым | `setup_form`, `kind: "site"` | `POST /api/integrations/tilda` |
| Закрытая страница с личным кабинетом (Tilda Members и другие) | Та же форма: имя и почту берёт из кабинета сама, ФИО человек может поправить; `requireAccount: true` — только вошедшим; без писем — `authMode: "none"`, `sendEmail: false` | `setup_form`, `kind: "site"` | то же |
| Своего сайта нет | Форма по ссылке на `vruchay.ru/f/…` или на своём домене | `setup_form`, `kind: "link"` | то же |
| Документ по событию в CRM, LMS или боте | Выдача одному | `issue_document` | `POST /api/documents/{id}/issue` |
| Список известен заранее | Таблица → выпуск → архив | `import_recipients` → `start_generation` | `POST …/recipients/import` → `POST …/generate` |
| Разослать выпущенное | Письма по шаблону документа | `set_mail_template` → `send_mail` | `POST /api/mailing/send` |
| Свёрстать лист, поставить ФИО и другие поля | Всё оформление делается инструментами, кабинет не нужен | `apply_preset`, `edit_layout`, `preview_sheet` | `PATCH …/sheets/{sheetId}/elements` |

Как настроить каждый путь — [integrations.md](integrations.md). Если ни один
не подходит, сначала скажи человеку, чего не хватает, и только потом пиши своё.

## Как подключиться

- **MCP** — удобнее для агента. Адрес `https://vruchay.ru/mcp`, вход через OAuth:
  человек один раз входит в кабинет и разрешает доступ, токен не нужен. Как
  подключить себя самому — по шагам в [connect.md](connect.md). Если инструменты
  `vruchay` у тебя уже есть — работай через них. Начни с `get_account`, инструкции —
  `get_guide` (`start`, `layout`, `integrations`, `recipes`). Готовые задания —
  подсказки MCP (`prompts`): `design_sheet`, `issue_from_table`, `site_form` и другие.
- **REST** — `https://vruchay.ru/api`, заголовок `Authorization: Bearer vru_…`
  (токен выдаёт владелец в кабинете). Проверка связи — `GET /api/auth/me`.

Права одинаковые: инструменты MCP вызывают тот же REST изнутри сервера. Чего нет
отдельным инструментом, делается через `api_request` с путём REST. Права токена —
«Выпускать документы» (`member`) или «Полный доступ» (`admin`); формы, удаление
и отзыв требуют второго.

Файлы инструменты MCP не передают: они ходят JSON-ом. Бланк, логотип или таблица
у тебя на диске и есть оболочка (Claude Code, Codex, Cursor) — возьми
`get_upload_link` и выполни `curl -F "file=@путь" "<ссылка>"` из ответа: ссылка
одноразовая, на 15 минут, токен в команду не попадает. Base64 в аргументах
не передавай. Без оболочки — `upload_image` по публичной ссылке или кабинет.

## Сущности

| Сущность | Что это | Ключевые поля |
| --- | --- | --- |
| **Организация** | Владелец всего остального: проба, план или оплаченный тариф старого образца | `plan` |
| **План** | Сколько документов, до какого числа, какие возможности | `limit`, `left`, `expired`, `features` |
| **Проект** | Мероприятие или сайт: свои документы, реестр, аналитика, письма. Отбор — `projectId` (UUID или `none`). Прежнее имя — папка (`folderId`) | `id`, `name` |
| **Документ** | Заготовка награждения: размер листа, листы, таблица получателей, мероприятие. В кабинете — «материал». Шаблон — документ с `isTemplate: true` | `id`, `title`, `projectId`, `sheets` |
| **Лист** (`sheet`) | Одна страница документа: фон и блоки. У листа есть `version` — растёт с каждой правкой | `id`, `version`, `backgroundFileId` |
| **Блок** (`element`) | Текст, картинка, QR, ссылка или фигура на листе. Координаты в мм от левого верхнего угла, кегль в pt | `id`, `type`, `x`, `y`, `w`, `h`, `props` |
| **Поле** | То, что подставляется в текст: `%name`, колонки таблицы, постоянные поля документа, служебные (`date`, `code`, `name_dat`…) | имя латиницей |
| **Колонка / строка** | Таблица получателей. `name` и `email` есть всегда. В выпуск идут отмеченные строки | `data`, `checked`, `source` |
| **Задание** (`job`) | Один запуск выпуска, асинхронный | `status`, `total`, `done`, `failed` |
| **Файл** (`file`) | Выпущенный документ в реестре | `fileId`, `code`, `state` |
| **Форма** | Выдача посторонним: на сайте организации (`site`) или по ссылке (`link`) | `kind`, `token`, `connect` |
| **Письмо** | Документ, отправленный получателю | `status` |

## Перед началом

Узнай, кто ты и сколько можно выпустить: `get_account` — или `GET /api/auth/me`
и `GET /api/org/usage`.

- `left` — сколько документов ещё можно выпустить (`null` — без предела);
- `expired: true` — срок плана вышел: выпуск, выдача, формы и рассылка не
  работают при любом `left`;
- `features` — что входит в план: `mailing` (письма), `tilda` (формы на сайте
  и по ссылке), `api` (токены), `awards` (правила наград); `null` — всё.

Ноль в `left` или `expired: true` — скажи человеку сразу, до вёрстки: сам собой
остаток не вырастет.

## Сценарий 1. Создать и свёрстать документ

| Шаг | MCP | REST |
| --- | --- | --- |
| Посмотреть шрифты, форматы, раскладки | `get_layout_catalog` | `GET /api/layout/catalog` |
| Завести документ со свёрстанным листом | `create_document` с `format`, `orientation`, `preset: {kind, style}` | `POST /api/documents` |
| …или взять существующий | `list_documents`, `get_document` | `GET /api/documents`, `GET /api/documents/{id}` |
| Фон или логотип по ссылке | `upload_image` (`target: background` или `element`) | `POST …/sheets/{sheetId}/background/from-url`, `POST /api/documents/{id}/assets/from-url` |
| Фон или логотип файлом с диска (агент с оболочкой) | `get_upload_link` → `curl -F "file=@…"` по ссылке из ответа | `POST /api/uploads` → [`POST /api/v1/uploads/{link}`](endpoints/public/post-v1-uploads-link.md) |
| Поправить блоки | `edit_layout` (`add`, `update`, `remove`) или `apply_preset` | `PATCH …/sheets/{sheetId}/elements`, `POST …/sheets/{sheetId}/preset` |
| Мероприятие, подписант, дата | `update_document` (`eventName`, `constants`, `issueDate`) | `PATCH /api/documents/{id}` |
| Проверить | `check_sheet` | `POST …/sheets/{sheetId}/check` |
| Посмотреть глазами | `preview_sheet` | `POST …/sheets/{sheetId}/preview` (JPEG) |

Порядок работы — цикл: правка → `check_sheet` → `preview_sheet` → правка, пока
замечаний нет и картинка выглядит как задумано. Замечания: блок за краем
(`off_page`), блоки наложились (`overlap`), текст не влез на самой длинной строке
таблицы (`text_overflow`), неизвестное поле или шрифт. Превью квоту не тратит.

- Виды раскладок (`kind`): `award` (за место), `winner`, `laureate`, `participant`,
  `attendee`, `course`, `graduate`, `school`, `thanks`, `letter`. Стили: `classic`,
  `solemn`, `academic`, `strict`, `modern`, `script`, `handwritten`.
- Бланк с напечатанным заголовком — `titlePrinted: true`, а сам бланк — фоном.
- Готовая раскладка ставит подпись из постоянных полей `signer_role` и `signer_name`
  с образцами «Руководитель» и «И. О. Фамилия» — замени их через `update_document`
  (`constants` — список целиком).
- `format` без `orientation` даёт альбомный лист.
- Поля документа и где они стоят: `get_document` → `fields` (REST — `GET /api/documents/{id}/fields`).

Как верстать вручную — координаты, кегли, шрифты, примеры блоков — в
[layout-guide.md](layout-guide.md); все поля блоков — в [справочнике макета](reference/layout.md).

## Сценарий 2. Таблица → выпуск → ссылка

| Шаг | MCP | REST |
| --- | --- | --- |
| Загрузить таблицу | `import_recipients` (`columns`, `titles`, `rows`, `mode`) | `POST /api/documents/{id}/recipients/import`; файлом — сначала `…/recipients/parse` |
| …или файлом `.xlsx`/`.csv` с диска | `get_upload_link` (`target: recipients`, `mode`) → `curl -F "file=@…"` | `POST /api/uploads` → `POST /api/v1/uploads/{link}` |
| Проверить данные | `validate_recipients` — только смотрит | `POST …/validation` |
| Принять однозначные исправления | `edit_recipients` с `applySuggestions: true` | `POST …/validation/fix` с `fixes` из `suggestion` замечаний; снять строки — `…/validation/exclude` |
| Отметить, кого выпускать | `edit_recipients` (`checkAll`, `update[].checked`) | `POST …/recipients/checked` |
| Посмотреть на самой длинной строке | `preview_sheet` с `rowId` | `POST …/preview` с `{"rowId": …}` |
| Показать человеку план и получить «да» | — | — |
| Выпустить | `start_generation` (`format: pdf` или `jpg`) | `POST /api/documents/{id}/generate`, тело минимум `{}` |
| Дождаться | `get_job` с `waitSeconds: 60`, повторять | `GET /api/jobs/{jobId}` раз в 2–3 секунды |
| Остановить ошибочный выпуск | `cancel_job` | `POST /api/jobs/{jobId}/cancel` |
| Доделать прерванный | `start_generation` с `resume: true` | `POST /api/jobs/{jobId}/resume` |
| Отдать человеку | `get_download_link` (`format: zip` или `pdf`) | `POST /api/jobs/{jobId}/archive-link` |

- `columns` — имена латиницей: `name` (ФИО), `email`, свои (`place`, `course`, `hours`…).
  Колонки создаются сами. До 30 колонок и 10 000 строк за раз.
- `mode: "replace"` стирает прежние строки (кроме выданных формой), `append` дописывает.
- Через MCP тело ограничено 2 МБ: большую таблицу шли частями с `append`.
- Задание кончается статусом `done`, `failed` или `canceled`. Причины сбоев
  `get_job` показывает сам; в REST — `GET /api/jobs/{jobId}/failures`. Забирать
  их нужно до продолжения: оно стирает прежние причины. Прошлые выпуски документа
  и их `jobId` — `get_document` → `jobs`.
  `stuck: true` при `queued` — задание висит больше 15 минут, его пора продолжить.
- Ссылка от `get_download_link` открывается в браузере без входа и живёт час.
  `name` — шаблон имён файлов в архиве, до 120 знаков: `"%name"`, `"%number %name"`.
  Архив по адресу задания (`GET /api/jobs/{jobId}/archive`) отдаётся только
  с заголовком `Authorization` и потоком — для программ, а не для человека.

## Сценарий 3. Один документ одному человеку

Когда документы выдаются не пачкой, а по одному — человек закончил курс, сдал
тест, записался, — таблица не нужна:

1. Документ с макетом уже есть (сценарий 1).
2. MCP: `issue_document` с `documentId`, `data: {"name": "Иванова Анна Сергеевна"}`,
   `email`, `send: true` и `idempotencyKey`.
   REST: `POST /api/documents/{id}/issue` с телом
   `{"data": {"name": "Иванова Анна Сергеевна"}, "email": "…", "send": true}`
   и заголовком `Idempotency-Key`.
3. Ключ — номер заказа или заявки, 8–100 знаков: латиница, цифры и `_ - : .`.
   В MCP он обязателен. В REST обязателен по смыслу, хоть и не по схеме: без него
   повтор после обрыва связи выпустит и спишет второй документ.
4. Ответ `201` (`status: "done"`) — документ готов: в `file` код проверки, номер,
   ссылка на страницу проверки и ссылка получателя на PDF (`recipientUrl`, 30 дней).
   `202` (`processing`) — опрашивай `statusUrl` раз в 1–2 секунды. `200` с
   `status: "failed"` — не вышел, причина в `error`; повтор с тем же ключом
   заведёт новое задание.

Поля `data` — колонки и поля листа этого документа (список — `get_document` → `fields.issue`,
в REST — `GET /api/documents/{id}/fields`, раздел `issue`); лишнее имя — `400` со списком допустимых. Квота та же, что
у выпуска; отказ по ней — сразу, `400`. Предел — 120 документов в минуту на
организацию. Письмо (`send: true`) требует `mailing` в плане и адреса: без
возможности — `403`, без адреса — `400`, и документ тогда не выпускается вовсе.
Если письмо не ушло уже после выпуска, документ остаётся выпущенным, а причина —
в `emailError`.

## Сценарий 4. Форма по ссылке или на сайте

Люди получают документ сами — заполняют форму. Создавать формы может только
токен с «Полным доступом», а в плане должна быть возможность `tilda`.

| Шаг | MCP | REST |
| --- | --- | --- |
| Форма по ссылке (своего сайта не нужно) | `setup_form` с `kind: "link"`, `name`, `documentIds: [id]`, `formFields: ["name"]` | `POST /api/integrations/tilda` |
| Форма на сайте организации | `setup_form` с `kind: "site"`, `name`, `documentIds`, `allowedDomains: ["example.ru"]` | `POST /api/integrations/tilda` |
| Изменить, выключить | `setup_form` с `integrationId` (`active: false`) | `PATCH /api/integrations/tilda/{id}` |
| Найти готовые | `list_forms` | `GET /api/integrations/tilda` |
| Заявки | `api_request` `GET /api/integrations/tilda/requests` | то же |

`name` — название формы для кабинета, при создании обязательно.

В ответе — `connect`: `connect.formUrl` — ссылка `https://vruchay.ru/f/{token}`
для формы по ссылке; `connect.embedHtml[documentId]` — готовый HTML для вставки
на сайт. Отдай человеку ровно это. Форма по ссылке выдаёт один документ, форма
на сайте — до 50; поддомены разрешённого домена тоже считаются своими. Форма
на закрытой странице личного кабинета подставляет имя и почту вошедшего сама —
см. «Закрытая страница и личный кабинет» в [integrations.md](integrations.md).

Каждая заявка выпускает документ и списывает квоту. Кончилась квота или вышел
срок плана — форма перестаёт выдавать, и человек видит просьбу написать организатору.
`authMode` — подтверждать ли почту кодом: по умолчанию `email_code` у формы
на сайте и `none` у формы по ссылке;
`singleFilePerEmail: true` (по умолчанию) — один документ в руки. Подробнее —
[integrations.md](integrations.md).

## Сценарий 5. Письма

| Шаг | MCP | REST |
| --- | --- | --- |
| Шаблон письма | `set_mail_template` (`subject`, `bodyHtml`, `attachGeneratedFile`) | `POST /api/mail/templates/{documentId}` |
| Пробное письмо себе | `api_request` `POST /api/mail/templates/{documentId}/test-send`, тело — как у шаблона | то же |
| Кому уйдёт и кому нет — показать человеку, получить «да» | `get_mail` → `audience` | `POST /api/mailing/audience` |
| Разослать | `send_mail` с `documentId` | `POST /api/mailing/send` с `{"documentIds": [id], "kind": "transactional", "source": "table"}` |
| Судьба писем | `get_mail` → `log` (`problemsOnly: true` — только недоставленные) | `GET /api/mailing/log?documentId=…` |
| Повторить недоставленные | `send_mail` с `documentId` и `onlyFailed: true` | `POST /api/mailing/resend` |

- В теме и тексте подстановки — `%name` и другие поля строки.
- Письма уходят отмеченным строкам, у которых документ уже выпущен; кому не
  поставлено — в `skipped` с причиной.
- Пробное письмо по токену приходит тому, кто выдал токен (или разрешил
  подключение OAuth), а не на адрес из запроса.
- С общего адреса сервиса — не больше 300 писем за раз и 500 за сутки на
  организацию; со своего подтверждённого домена предела нет.
- Повторный `send_mail` безопасен: кто уже получил письмо, второй раз его не
  получит — так можно дослать письма строкам, выпущенным позже.

## Сценарий 6. Исправить выданное

Опечатка в ФИО, письмо не дошло, документ выдан по ошибке:

| Шаг | MCP | REST |
| --- | --- | --- |
| Найти выданное | `search_registry` (`search` — имя, почта или код) | `GET /api/registry` |
| Карточка и файл одного документа | `search_registry` с `fileId` | `GET /api/registry/files/{fileId}`, ссылка — `GET /api/documents/files/{fileId}/url` |
| Поправить строку таблицы | `edit_recipients` (`update`) | `PATCH …/recipients/rows/{rowId}` |
| Перевыпустить по исправленной строке | `reissue_documents` с `fileIds` | `POST /api/registry/reissue` |
| Переслать письмо | `send_mail` с `fileIds` | `POST /api/registry/resend` |
| Отозвать | `revoke_documents`: без `expectedCount` — предпросмотр, затем с ним | `POST /api/registry/revoke/preview`, затем `POST /api/registry/revoke` |

- Перевыпуск тратит квоту; новый документ получает новый номер, старый
  помечается заменённым, и его страница проверки ведёт на новый.
- Отзыв виден сразу на странице проверки по QR. Вернуть проверку можно
  (`restore: true`), но человек с бумагой мог уже увидеть «отозван».
- Перевыпуск и отзыв — только с «Полным доступом».

## Что важно знать заранее

- **Асинхронный только выпуск.** Остальное отвечает результатом сразу. Вебхуков
  о готовности нет — состояние узнают опросом (`get_job` ждёт сам до 60 секунд).
- **Тело `POST /generate` обязательно.** Пустое тело — `400`; посылай `{}`.
- **Выпускаются только отмеченные строки.** Импорт отмечает строки сам; снятые
  проверкой и строки, выданные формой или по API, в пакет не попадают.
- **Один выпуск на документ.** Пока по документу идёт задание, второе не начнётся;
  у организации — не больше трёх заданий в очереди.
- **Чужое отвечает `404`, а не `403`.** Так не подтверждается существование
  чужих объектов.
- **Лист правят по очереди.** Редактор кабинета записывает лист с версией и твою
  правку молча не затрёт: человек с открытым листом увидит «Лист изменили в другом
  месте» и сам выберет, взять твою версию или записать свою поверх. Чтобы обойтись
  без выбора, попроси не трогать лист, пока ты верстаешь. Сам передавай
  `baseVersion` — версию листа из `get_document`: если лист успел измениться, ответ
  `409` с текущей `version`; перечитай лист и повтори.
- **Превью — образец.** Код и номер на нём ненастоящие; данные — первой строки
  таблицы или образца, `rowId` — конкретной строки.
- **Только человек:** сотрудники и роли, пароль и вход, токены и разрешение
  OAuth, оплата картой и счета, удаление учётной записи. Такие запросы отвечают
  `403`, `api_request` в эти разделы не пускает.
- **Подпись PDF** ставится, только когда у организации отметка `paid` и на сервере
  настроен сертификат подписи. JPEG подписи не носит.
- **Выданное остаётся действительным** и после истечения плана.

## Что подтверждать у человека

Перед этими действиями покажи, что и сколько будет сделано, и дождись согласия:

- выпуск (`start_generation`, `POST …/generate`) — тратит квоту: сколько документов,
  по какому документу, сколько останется;
- выдача одному (`issue_document`) — тратит квоту; в потоке из CRM согласие даётся
  один раз на всю интеграцию;
- рассылка (`send_mail`) — необратима: сколько писем, с какого адреса, текст;
- форма (`setup_form`) — открывает выдачу посторонним и тратит квоту на каждую заявку;
- отзыв (`revoke_documents`) — сначала предпросмотр, число документов показать
  человеку; перевыпуск (`reissue_documents`) — тратит квоту;
- удаление — листа (`delete_sheet`), документа в корзину (`delete_document`),
  формы; удалить насовсем может только владелец в кабинете;
- `import_recipients` и `get_upload_link` для таблицы с `mode: "replace"` — стирают прежнюю таблицу.

## Если что-то пошло не так

| Ответ | Что значит | Что делать |
| --- | --- | --- |
| `400` с `errors[]` (в MCP — «Поля: …») | Не прошла проверка полей | Исправить по `errors[].field` и `errors[].message` |
| `400` про остаток или план | Не хватает документов или вышел срок | Сказать человеку; состояние — `get_account` |
| `401` | Токена нет, он отозван или заменён | Проверить заголовок; для OAuth — переподключиться |
| `403` | Не та роль, действие только для человека, возможности нет в плане или план истёк | Не повторять, объяснить человеку |
| `404` | Объекта нет или он чужой | Проверить идентификатор |
| `409` у листа | Лист изменился после чтения | Перечитать (`get_document`), повторить с новой `baseVersion` |
| `409` у выдачи | `Idempotency-Key` уже использован для другого документа | Ключ — на каждого получателя, а не на программу |
| `422` у превью | Лист не отрисовался | `check_sheet`, проверить `fileId` картинок |
| `429` | Слишком часто | Подождать столько секунд, сколько названо в тексте |
| `job.status = failed` | Задание не доделано | Причины (`get_job` или `…/failures`), потом `start_generation` с `resume: true` (REST — `POST /api/jobs/{jobId}/resume`) |
| `503` при запуске выпуска | Очередь недоступна; задание заведено и помечено `failed` | Продолжить его (`start_generation` с `resume: true`), а не запускать второе |
| `202` у выдачи | Документ ещё печатается | Опрашивать `statusUrl`, а не повторять выдачу |

Разбор форматов — в [Кодах ошибок](errors.md).

## Границы приличия

- Токен отправляется только на `vruchay.ru` и никуда больше: ни в сторонние
  сервисы, ни в публичные чаты, ни в код, который уйдёт в репозиторий.
- Таблица участников и выпущенные документы — персональные данные. Не выкладывай
  их в сторонние сервисы и не показывай больше, чем нужно для задачи.
- Ссылка на архив открывается без входа: отдавай её только тому, кто поручил работу.
- Сомневаешься, тратит ли действие квоту или можно ли его отменить, — спроси.
