Для AI-агентов: markdown этой страницы — /docs/sites-storefront-and-forms.mdиндекс документации — /llms.txt
Сайт, витрина и формы
Обновлено
Опубликуйте сайт своего бизнеса, настройте форму обращения и переход к покупке. В TelegaFirst сайт помогает начать разговор с AI-фронт-офисом в Telegram, а команда продолжает работу в Telegram-супергруппах. Заявка с сайта сохраняется сразу: переход посетителя в мессенджер помогает установить его личность, но не является условием сохранения обращения.
Подготовка и адреса
Сначала пройдите вход и настройку бизнеса, проверьте активный slug и доступный tools/list. Для сайта нужны полномочия владельца и scopes конкретной операции. MCP использует предусмотренный credential владельца или интеграции; перечисленные REST-маршруты принимают X-Api-Key либо Authorization: Bearer tgf_*, а не surface-session JWT. Полномочия проверяются при каждом вызове.
| Действие | Scope |
|---|---|
| Статус своего домена | domain:read |
| Запрос подключения, проверка DNS, отключение | domain:write |
| Загрузка/публикация сайта, формы, выдача checkout key | site:write |
| Manifest, чтение файла через MCP, список имён секретов | site:read |
| Настройки витрины | storefront:read / storefront:write |
| Анонимный checkout relay | orders |
| Чтение ответов форм через MCP | pull:form_submissions |
| Отправка и отметка обработки через Hub/MCP | push:form_submissions |
Client.slug — единственный читаемый tenant-адрес публичной поверхности. Товар, форма, событие и публичная заявка используют opaque UniversalLink.code; публичную ссылку нельзя построить из их номера. Для управления формой используется отдельный числовой seqNum своего tenant, для управления ответом — его собственный seq_num. Внутренний PK не передаётся ни в одну из этих поверхностей. Идентификаторы.
Две зоны .ru и .com обязательны на запуске. Это отдельные юридические и платёжные зоны; русский — язык по умолчанию, а язык оператора определяется настройкой бизнеса. Домены и примеры ниже — контрактные иллюстрации: acme.ru, UUID и ссылки *.test не подтверждают зарегистрированный домен или опубликованный сайт. Хосты сервисов берите из актуального ответа и настройки окружения.
Прямая ссылка, открывающая страницу как мини-приложение
site_list_pages / GET /api/v1/tools/site/pages отдают у каждой страницы mini_app_links, а на верхнем уровне — mini_app_home: по ссылке на Telegram, MAX и VK. Такая ссылка открывает опубликованную страницу внутри мини-приложения бота (mini_app_home — главную мини-приложения: ?startapp без значения, в VK — без #), её можно вставить в текст рассылки. Ссылка несёт только opaque-код страницы, без номера. Если канал не откроет мини-приложение, url равен null, а reason говорит почему: channel_not_connected — мессенджер не подключён; channel_surface_not_native — мессенджер не открывает мини-приложения по ссылке; mini_app_not_opened — адрес мини-приложения ещё не зарегистрирован: он записывается при первом открытии мини-приложения бота, адрес которого задаётся в настройках бота (в Telegram — Mini App URL в BotFather); page_not_published — страница не показывается посетителям; page_not_addressed — у страницы ещё нет кода, опубликуйте её снова. По такой ссылке не считаются переходы /r/l/, а имя бота зашито в саму ссылку.
Один адрес на зону
У бизнеса ровно один публичный адрес в каждой зоне. Пока свой домен этой зоны не активен, это технический адрес платформы вида {slug}.<зона сайтов>: возьмите его из site_list_pages → hosts (kind: "platform"), не собирайте руками. Когда свой домен зоны становится ACTIVE, он занимает это место, а технический адрес отвечает постоянным редиректом 308 на тот же путь своего домена; ссылки, уже выданные на технический адрес, продолжают работать. Поддомена shop. нет: сайт, вход, витрина, оформление заказа и кабинет покупателя живут на одном адресе.
На этом адресе пути делятся на три класса (без учёта регистра, путь /x включает /x/...):
| Класс | Пути | Кто отвечает |
|---|---|---|
| Платформенные | /auth, /checkout, /cart, /account, /refund, /l, /f, /product, /events, /booking, /widget, /miniapp, /api, /__telegafirst, /.well-known/acme-challenge | всегда платформа; bundle с файлом под таким путём отклоняется целиком с 422 SITE_PATH_RESERVED |
| Заменяемые | /, /shop, /privacy, /terms, /refunds, /bio, /robots.txt, /sitemap.xml, /llms.txt, /.well-known | ваш файл, если он есть в bundle; иначе автоматическая страница платформы |
| Сайт | всё остальное | ваш bundle |
Каталог всегда открывается по /shop. Корень / показывает ваш index.html. Без него на оплаченном тарифе там тот же каталог с rel="canonical" на /shop, поэтому index.html можно снять отдельно через site_delete_page; на бесплатном уровне корень без index.html ведёт себя как прежде. Карточка товара — /product/{code} с opaque кодом; ссылка «купить сейчас» — /product/{code}?buy=1: страница сама проводит вход и создаёт заказ, сам переход по ссылке ничего не записывает. В MCP товар каталога несёт public_url и buy_url; берите адреса оттуда. Скрипты service worker (sw.js, service-worker.js, *-sw.js) не принимаются ни под каким путём: тот же отказ SITE_PATH_RESERVED.
Подключение своего домена
Прочитайте
domain_statusилиGET /api/v1/tools/domain. Параметрzone=ru/zone=comвыбирает свою зону; без него возвращается массив текущих подключений.Вызовите
domain_add_requestилиPOST /api/v1/tools/domainс непустым HTTPIdempotency-Key. Передавайте bare hostname без схемы, пути и порта. Пример тела:
{
"domain": "acme.ru",
"zone": "ru"
}REST 201 не означает, что DNS уже проверен. Например, при anti-phishing admission ответ содержит ownerStatus:"screening", created:false, domain.status:"PENDING_DNS" и failedReason:"domain_screening_pending". Следуйте instructions: пока проверка безопасности не завершена, не публикуйте ни записи направления трафика, ни verification TXT. Повторный domain_add_request сообщает текущий admission status; не заменяйте его угадыванием DNS token.
После допуска опубликуйте точные DNS-записи из возвращённой инструкции, затем вызовите
domain_verify/POST /api/v1/tools/domain/verifyс{"zone":"ru"}и HTTP idempotency key. Проверка принимает только свою зону, не чужой hostname. Пример ещё незавершённого результата REST 201:
{
"domain": {
"domain": "acme.ru",
"zone": "ru",
"status": "PENDING_DNS",
"dnsVerifiedAt": null,
"certIssuedAt": null,
"failedReason": "domain_screening_pending"
},
"verified": false,
"pendingReason": "screening_pending"
}verified:false — ожидаемый результат незавершённой проверки. После screening DNS может сообщать nxdomain, mismatch, ambiguous, timeout или resolver_error; сверяйте возвращённые pendingReason и записи. Распространение DNS зависит от вашего TTL. Успешная проверка переводит свой домен в VERIFIED; сертификат выпускается при первом HTTPS-обращении, а не доказывается одним ответом 201.
Чтобы отключить домен через MCP, сначала вызовите
domain_prepare_unlink. Покажите человеку замороженные домен/зону изpayloadи срок действияexpires_atвозвращённой квитанции. Только после финального подтверждения человеком вызовитеdomain_execute_unlink: его JSON-тело содержит ровноpreparation_id(UUID) иpayload_hash(64 строчных hex-символа), скопированные без изменений из одной и той же квитанции подготовки. Подготовка не разрешает автоматическое отключение. Исполнение прекращает routing и обслуживание сертификата; новое подключение снова требует DNS-проверки. Существующий RESTDELETE /api/v1/tools/domain/{zone}— отдельная прямая операция владельца. У неё нет выдуманных REST prepare/execute twins; человек должен заранее понимать эффект.
Загрузка файлов и публикация
Сайт, витрина, вход и кабинет живут на одном хосте вашего бизнеса, поэтому любой скрипт ваших страниц — ваш собственный или сторонний, который вы подключили, — выполняется рядом с сессией покупателя на этом хосте. Такой скрипт может от имени покупателя читать его профиль, заказы и платежи в вашем магазине, создавать заказы и рисовать страницу, похожую на оплату. Данные других бизнесов и платёжные данные карт ему недоступны. Подключайте только скрипты, которым доверяете так же, как своей CRM.
Публикуйте на каноническом UGC-хосте бизнеса в *.telegafirstsite.ru или *.telegafirstsite.com, либо на его подключённом и проверенном custom-домене. Tenant UGC на основном домене платформы запрещён. Возьмите допустимый хост из настройки своего бизнеса; пример ниже иллюстративный и не подтверждает существующий сайт. Допустимый хост и подходящий тариф — предпосылки публикации.
site_request_upload / POST /api/v1/tools/site/request-upload принимает host, необязательный mode и непустой массив файлов. По умолчанию mode равен merge: объявите новые или изменённые файлы; omitted bio, бинарные assets, gate-конфигурация и references сохранённых файлов остаются на месте. Переданные файлы заменяют одноимённые пути вместе с их declarations. Явный mode: "replace" означает полный snapshot: перечислите весь целевой сайт; отсутствующие файлы и правила доступа намеренно удаляются. Этот режим выбирается при выдаче ticket и не меняется при publish.
Укажите путь относительно bundle, фактический размер UTF-8/бинарных байтов и MIME. references, когда нужны, — пути зависимостей внутри того же bundle; сервер проверяет их после объединения с сохранёнными файлами, и отсутствующая или недопустимая зависимость отклоняется. Квота проверяется по всему результирующему сайту, включая сохранённые файлы, а не только по patch. Пример добавления одной страницы с default merge:
{
"host": "demo.telegafirstsite.ru",
"files": [
{
"path": "p/tripwire/index.html",
"sizeBytes": 15,
"contentType": "text/html"
}
]
}REST 201 возвращает uploadId UUID, канонический host, mode, baseManifestDigest (строчный SHA-256 или null, если исходного manifest нет) и uploads с path, staging key, presigned putUrl, expiresAt. Base определяется сервером и закрепляется ticket; caller baseManifestDigest, baseManifest и clientId запрещены. Ссылки действуют 300 секунд. Не сочиняйте UUID или staging key: возьмите их из своего ответа. Загрузите каждый файл HTTP PUT непосредственно по его putUrl, сохранив заявленные content type и byte length. Неправильный размер не превращает ticket в разрешение загрузить дополнительные байты.
Только после успешных PUT вызовите site_publish / POST /api/v1/tools/site/publish, снова с HTTP idempotency key:
{
"host": "demo.telegafirstsite.ru",
"uploadId": "11111111-1111-4111-8111-111111111111"
}UUID иллюстративный: нужен ticket того же tenant и закреплённого host. Перепривязать ticket к другому host нельзя; publish не принимает новый mode или base. Если исходный manifest или правила доступа изменились, ответ — 409 SITE_BUNDLE_STALE: получите новый ticket, выполните его PUT и затем publish. Не повторяйте stale ticket как разрешение перезаписать чужое изменение.
Пример результата REST 201:
{
"host": "demo.telegafirstsite.ru",
"uploaded": ["p/tripwire/index.html"],
"deleted": [],
"unchanged": ["bio/index.html", "assets/app.js"],
"manifestDigest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"attributionSnippet": "<script src=\"https://gateway.example.test/pixel/telega-pixel.js\" async></script>",
"pageUrls": [
{
"path": "p/tripwire/index.html",
"url": "https://demo.telegafirstsite.ru/p/tripwire/index.html"
}
]
}uploaded — записанные пути, deleted — удалённые, unchanged — сохранённые без записи. Delta Sync вычисляет изменения по фактическим staged bytes; index.html записывается последним. pageUrls содержит только HTML-пути этого ticket, которые действительно завершили публикацию. Сохранённая bio-страница не выдаётся как новая landing. Открывайте точный URL из результата; не выводите homepage, extensionless URL или live-ссылку из одного успешного upload ticket.
После успеха staged файлы потреблены. Для нового deploy получите новые upload URLs; старый ticket нельзя использовать как постоянный источник контента. Размер всего сайта, размер одного видеофайла и суточный бюджет загрузок ограничиваются тарифом; число файлов сайта ограничено одинаково на всех тарифах. Суточный бюджет считает тикеты — один на запрос site_request_upload, сколько бы файлов он ни объявлял, — и байты объявленных файлов. Сервер проверяет заявленные размеры до выдачи URLs и реальные размеры перед публикацией. Конкретные числа своего тарифа берите из details отказа или из описания тарифа: документация их не повторяет.
Сборка сайта из HTML-прототипа
HTML, сохранённый из браузера или собранный генератором, часто несёт изображения, видео и шрифты прямо в странице как data: URL. Такой файл не публикуется: один HTML-тег читается только до фиксированного размера, и тег с крупным встроенным ресурсом отклоняется кодом SITE_HTML_TAG_TOO_LARGE. Встроенные <style> и <script> сами по себе разрешены; причина отказа — размер одного тега, а не всего файла. Собственное хранилище или ключ S3 не нужны: файлы загружаются в хранилище платформы по URL из site_request_upload.
Соберите bundle: HTML-страницы и все файлы, которые они используют, — изображения, видео, шрифты, при желании CSS и JS. Каждый ресурс — отдельный файл bundle.
Каждый крупный
data:URL декодируйте в файл с теми же байтами: тип возьмите из префиксаdata:<тип>;base64,, содержимое — base64-декодированием остатка. У URL без;base64(напримерdata:image/svg+xml,%3Csvg%20…) содержимое — остаток после запятой, раскодированный из percent-encoding:%3Cстановится<,%20— пробелом. Дайте файлу имя по содержимому, напримерassets/hero.3f2a9c.png(первые символы SHA-256 байтов): такой файл кэшируется браузером надолго, а изменённый получает новое имя.Замените
data:URL в атрибуте относительной ссылкой на файл (путь считается от папки страницы: дляp/tripwire/index.htmlи файлаp/tripwire/assets/hero.3f2a9c.pngэтоsrc="assets/hero.3f2a9c.png", дляp/tripwire/quiz/index.html—src="../assets/hero.3f2a9c.png"). Проверьте, что в страницах не осталось локальных путей видаfile://илиC:\и что соседние страницы ссылаются на те же файлы.Объявите все файлы в одном
site_request_uploadс фактическими размером и MIME; ресурсы страницы перечислите в еёreferences. Видео (.mp4,.webm) принимается в пределах размера одного видеофайла по тарифу и проверяется раньше размера всего сайта: больше —SITE_VIDEO_TOO_LARGE; сожмите ролик или встройте его с YouTube, VK Видео или Rutube.Выполните PUT каждого файла, затем
site_publish. По умолчаниюmergeсохраняет bio-страницу и уже опубликованные страницы; пути выбираете вы, префиксp/из примера обязателен только на платформенном apex.
Пример объявления лендинга и квиза с общими ресурсами:
{
"host": "demo.telegafirstsite.ru",
"files": [
{ "path": "p/tripwire/index.html", "sizeBytes": 48211, "contentType": "text/html", "references": ["p/tripwire/assets/hero.3f2a9c.png", "p/tripwire/assets/promo.81d0e4.mp4"] },
{ "path": "p/tripwire/quiz/index.html", "sizeBytes": 31777, "contentType": "text/html", "references": ["p/tripwire/assets/hero.3f2a9c.png"] },
{ "path": "p/tripwire/assets/hero.3f2a9c.png", "sizeBytes": 182131, "contentType": "image/png" },
{ "path": "p/tripwire/assets/promo.81d0e4.mp4", "sizeBytes": 726493, "contentType": "video/mp4" }
]
}Любой отказ загрузки или публикации оставляет живой сайт без изменений. Если причина в самих файлах, исправьте их и получите новый ticket: исправленные байты имеют другой размер и по старым URL не загрузятся. Ответ pageUrls перечисляет ровно опубликованные страницы — откройте их и проверьте, что изображения и видео загрузились.
Если страницы ссылаются на вашего бота, вставьте точный attributionSnippet из ответа в шаблон HTML. В приведённом ответе URL gateway.example.test — адрес стенда; его не следует копировать в живой сайт. Переход к боту может работать и без скрипта, но источник страницы/кампании не будет корректно атрибутирован.
Для правки существующего bundle через MCP сначала site_get_manifest с {"host":"acme.ru"}, затем site_fetch_file с {"host":"acme.ru","path":"index.html"}. Manifest несёт checksum/size и manifestDigest; fetch возвращает UTF-8 текст файла, а не секреты или конфигурацию access policy. После правки загрузите изменённые файлы через default merge; полный bundle нужен при явном replace. Эти чтения не вводят новых REST routes.
site_pages_set_visibility управляет существующими страницами по их tenant-номерам; корневые и системные страницы защищены, показ не отменяет тариф или модерацию. site_page_preview возвращает короткоживущий opaque URL на отдельном origin и не публикует страницу. Для удаления сайта используйте site_prepare_delete, покажите человеку frozen manifest digest, после подтверждения — site_execute_delete. Подготовка сама не удаляет байты. Точные схемы этих MCP операций запрашивайте через актуальное discovery.
Секреты сайта и подтверждение владельца
Секрет сервера не должен находиться в HTML, JavaScript bundle или literal destinations. Для server-side отправки используйте сохранённое имя, например API_TOKEN. Site secret хранится с шифрованием AES-256-GCM и привязкой к tenant/полю. Список возвращает только name, createdAt, lastRotatedAt; прочитать значение назад через API нельзя.
secret_put с site:write либо POST /api/v1/tools/site/secrets начинает отдельное owner approval. Пример с фиктивным значением, которое не является credential:
{
"name": "API_TOKEN",
"value": "secret-fixture-value"
}Первый ответ означает только ожидание подтверждения, а не сохранение:
{
"status": "pending_approval",
"actionId": "11111111-1111-4111-8111-111111111111"
}Владелец подтверждает действие в Telegram. Затем повторите соответствующий вызов с теми же именем/значением и возвращённым UUID в поле action_id. Для REST leg2 используйте новый HTTP idempotency key: повтор leg1 с прежним ключом воспроизведёт pending answer или отклонит изменённое тело. Подтверждение проверяет реального владельца, tenant и действие; одной строки scope без необходимого provenance недостаточно. Не создавайте фиктивный ExternalApp для OAuth/M2M credential.
secret_rotate / PUT /api/v1/tools/site/secrets/{name} меняет значение с такой же двухшаговой проверкой; REST body содержит value, затем action_id. secret_delete / DELETE /api/v1/tools/site/secrets/{name} также требует подтверждения; во второй REST-вызов action_id передаётся query-параметром. Новый секрет под занятым live именем —409 CONFLICT: используйте rotate. Для чтения имён — secret_list с {} / GET /api/v1/tools/site/secrets и site:read. Маршрута GET /secrets/{name} нет.
Обычная публикация и декларация формы не требуют этой secret approval. Их право — проверенный site:write; это не разрешение изменить чужой домен или обойти другие ограничения.
Декларация формы до создания HTML
Сначала вызовите site_form_declare / POST /api/v1/tools/site/forms. Форма получает адрес до первой публикации, поэтому HTML может сразу использовать возвращённый code. Пример создания с разрешённым origin:
{
"zone": "ru",
"title": "Обращение",
"description": "Ответим в течение часа",
"submitLabel": "Отправить заявку",
"afterSubmit": {
"mode": "auto",
"title": "Спасибо!",
"text": "Продолжим в мессенджере.",
"buttonLabel": "Забрать гайд"
},
"allowedOrigins": [
"https://acme.ru"
],
"captchaRequired": false,
"funnelMode": "none",
"enabled": true
}REST отвечает 201 с seqNum, clientSlug, code, zone, title, description, submitLabel, afterSubmit, enabled, created, updatedAt. seqNum нужен управлению за tenant guard, code — публичной ссылке. Для обновления передайте свой числовой seqNum; публичные slug/code сохраняются. Без seqNum или с null создаётся новая форма с новым адресом. Если timeout не позволил узнать созданный номер, не повторяйте MCP create вслепую; это не idempotent create. Для REST receipt повторите исходный запрос с тем же HTTP key.
description (до 1000 символов) и submitLabel (до 40) показывает автостраница формы /f/{code}; null или пропуск — текст зоны. afterSubmit задаёт, что посетитель увидит после отправки, на любой поверхности: автостранице, странице с forms.v1.js или вашей собственной.
mode: "auto"(по умолчанию) — экран результата с вашимиtitle(до 120) иtext(до 1000) и кнопками действий, которые выбирает платформа.buttonLabel(до 32 символов, пробелы по краям отбрасываются) — текст кнопки, ведущей в бота: и кнопки в мини-приложении, и ссылок на каналы в браузере (при нескольких каналах к нему добавляется имя канала). Пропущенный текст берётся из языка зоны формы; для кнопки это «Перейти в бота» в мини-приложении (и опознанному посетителю, и анонимному) и «Продолжить в {канал}» в браузере.mode: "redirect"— посетитель уходит на вашredirectUrl. Он обязателен в этом режиме и запрещён вauto;buttonLabelв этом режиме запрещён — кнопки нет. Адрес толькоhttps://и только на хост этого бизнеса: его адрес сайта, подключённый custom-домен или origin изallowedOriginsтой же декларации. Чужой хост — 422FORM_REDIRECT_HOST_NOT_ALLOWED, форма при этом не записывается.
afterSubmit — часть полной декларации, а не патч: пропуск при redeclare возвращает auto с текстами зоны.
Декларация задаёт envelope: destinations, allowed origins, captcha и funnel. По умолчанию captcha включена; её отключение в примере задано явно. Рисуйте inputs в HTML самостоятельно. Необязательный submissionSchema валидирует совпадающие с HTML имена и значения; он не строит UI. Всегда давайте каждому полю человеческий label — полный текст вопроса, например {"name": "q1", "type": "text", "label": "Как вас зовут?"}: оператор видит его над каждым ответом в карточке заявки и в чате, а без него — только техническое имя поля (q1). До 20origins и 20destinations; до 50variants и 100 элементов submissionSchema. funnelMode — none, deterministic, ai_agent. Для лестницы отправляйте полный список variants: пропуск при redeclare удаляет лестницу. В deterministic используется when, в ai_agent — hint; подготовленные шаги адресуются stepSeqNum, а не PK. Для checkout outcome нужен свой catalogSeqNum.
Destinations имеют существующие формы: Telegram topic, HTTPS webhook или external API с secretName вместо значения credential. Разрешайте точные origin своего сайта; пустой allowlist не допускает отправку. Остановите приём redeclare с enabled:false, сохранив необходимую конфигурацию. Отдельного удаления формы нет: исторические ответы должны сохранять смысл.
Отправка и немедленная анонимная заявка
Посетитель отправляет JSON в публичный адрес https://fn.telegafirst.ru/f/{clientSlug}/{code}; для COM-зоны — fn.telegafirst.com. Оба значения возьмите из своей декларации. Пример тела:
{
"payload": {
"question": "Записаться"
}
}Browser передаёт свой Origin; при включённой captcha нужен действительный captcha_token. Если страница открыта как мини-приложение и форму отправляете вы сами, без forms.v1.js, добавьте "mini_app_channel": "telegram", "max" или "vk" — мессенджер, в чьём мини-приложении открыта страница. Тогда ответ предложит close_mini_app или open_bot, в том числе анонимному посетителю. Это только подсказка для кнопок результата: посетителя она не опознаёт. Это публичная поверхность P1: посетитель не подставляет tenant, form number, submission number или внутренний ID. После проверки origin, captcha, размера, схемы и media сервер сохраняет новую заявку сразу и выделяет её собственный tenant-номер, даже если посетитель остаётся анонимным.
Публичный HTTP 202 одинаков для всех поверхностей:
{
"result": {
"title": "Спасибо!",
"text": "Продолжим в мессенджере.",
"actions": [
{ "kind": "open_channel", "channel": "telegram", "url": "https://…/r/l/…/telegram?z=ru" },
{ "kind": "open_channel", "channel": "max", "url": "https://…/r/l/…/max?z=ru" }
],
"button_label": "Забрать гайд"
},
"continue_url": "https://acme.telegafirstsite.ru/l/…"
}result — то, что показать посетителю. Виды действий в actions:
close_mini_app— посетитель опознан и отправил форму из мини-приложения: закройте окно, разговор продолжится в чате.open_bot— посетитель не опознан, но отправил форму из мини-приложения (telegram,max,vk): откройтеurl— бота этого мессенджера со ссылкой на заявку — средствами самого мессенджера (в TelegramTelegram.WebApp.openTelegramLink), затем закройте мини-приложение. Старт бота по этой ссылке связывает заявку с посетителем. Если мессенджер не умеет открыть ссылку внутри себя, перейдите поurl.open_channel— кнопка перехода в канал (telegram,max,vk). Опознанному посетителю предлагается только его канал, анонимному — все включённые каналы бизнеса.redirect— переход наurlприafterSubmit.mode: "redirect"; тот же адрес приходит вredirect_url.
button_label — ваш afterSubmit.buttonLabel или null, если показывать текст платформы.
continue_url — страница продолжения /l/{code} с текстами формы и кнопками каналов, на которую можно вернуться позже. Все адреса — opaque P1: ответ не несёт seq_num, отдельный submission_code, PK или данные посетителя. Переходите по выданным url, не собирайте ссылки на мессенджер самостоятельно: ссылку на бота платформа выдаёт сама в open_bot. Открытие мессенджера связывает личность с уже существующей заявкой. Бизнес может увидеть анонимную заявку раньше перехода и отличает её от linked.
При redirect анонимного посетителя платформа добавляет к вашему redirectUrl параметр tgf_continue с адресом страницы продолжения (до #fragment, если он есть). Покажите на своей странице кнопку «Продолжить в мессенджере», ведущую на значение tgf_continue: иначе анонимная заявка останется без перехода в чат. Опознанный посетитель получает redirectUrl без изменений.
Своя HTML-форма через forms.v1.js
Чтобы не писать отправку, валидацию ответа и экран результата самому, подключите помощник одной строкой и пометьте свою форму code из декларации:
<script src="https://acme.telegafirstsite.ru/widget/forms.v1.js" async></script>
<form data-tgf-form="CODE">
<input name="question" required>
<button type="submit">Отправить</button>
</form>Скрипт берите с адреса сайта своего бизнеса (site_list_pages → hosts). Поля рисуете вы: помощник не создаёт ни одного поля. Он перехватывает submit, собирает значения по name, отправляет их в публичный ingest и показывает результат из ответа 202 рядом с формой, скрыв её. На время отправки форма несёт data-tgf-state="sending", затем sent или failed. Токен captcha положите в атрибут data-tgf-captcha-token формы.
После успешной отправки на форме возникает отменяемое событие tgf:submitted, в event.detail — тот же ответ 202. Чтобы нарисовать результат самостоятельно, вызовите event.preventDefault():
document.addEventListener('tgf:submitted', (event) => {
event.preventDefault();
renderMyResult(event.detail.result);
});При ошибке возникает отменяемое tgf:failed с detail.status (HTTP-статус или 0 при сетевой ошибке); без отмены помощник выводит текст ошибки в элемент [data-tgf-error] формы или создаёт его. Origin страницы должен быть в allowedOrigins формы.
Этот публичный ingest имеет собственные ошибки:400 malformed body/media,403 отсутствие допуска/неверный origin/captcha,413 превышение размера,422 violations объявленной схемы,429 ограничение частоты. Не приписывайте ему Hub Problem Details envelope или Hub scope: это другой HTTP boundary.
Для серверной отправки от интеграции есть отдельный действующий P3 route: POST /api/v1/form-submissions, scope push:form_submissions, HTTP key обязателен. Пример полного тела и результата 202 из принятой проверки:
{
"form_seq_num": 12,
"payload": {
"email": "ada@example.test"
}
}{
"outcome": "accepted",
"seq_num": 1042,
"submission_code": "opaque-form-answer",
"status": "anonymous"
}Здесь 12 — номер формы,1042 — собственный номер нового ответа; submission_code — отдельный публичный code. Числа передавайте JSON numbers, положительные int4 до 2147483647. MCP submit_form_submission использует тот же контракт, но каждый вызов создаёт новый ответ: HTTP replay не распространяется на него по умолчанию.
Прочитайте pull_form_submissions с {"form_seq_num":12,"status":"anonymous","limit":50}. Scope pull:form_submissions; действуют права и paid capability. Значение limit по умолчанию 50, диапазон 1..200. Cursor зашифрован, привязан к tenant/resource/эффективным фильтрам и живёт 15 минут: передавайте выданную строку как есть, не расшифровывайте PK, не меняйте фильтры между страницами. Просроченный/чужой cursor —400 INVALID_CURSOR; начните заново.
Для detail используйте get_form_submission с {"seq_num":1042}. Ответ показывает seq_num, отдельный submission_code, form_seq_num, title, created_at и статус; linked ответ дополнительно несёт пользовательский tenant-номер и channel. Сам payload опущен, пока человек явно не попросил прочитать ответы. Тогда используйте единственное значение include:"full_payload"; disclosure записывается в аудит. Оно не требуется для подсчёта заявок или проверки работы формы. Статус заявки «Новая ↔ Обработана» переключается в обе стороны: MCP set_form_submission_handled с {"seq_num":1042,"handled":true} или REST PATCH /api/v1/form-submissions/1042 с телом {"handled":true}; handled:false возвращает заявку в новые. Scope push:form_submissions. Ответ outcome: changed — статус изменили вы; unchanged — он уже был таким, повтор безопасен; not_found — такой заявки у бизнеса нет (REST отвечает 404 NOT_FOUND). Карточка в Telegram-топике операторов переключается и показывает, что статус сменила ваша интеграция.
Витрина и checkout
get_storefront / GET /api/v1/tools/storefront читает singleton с storefront:read. update_storefront / PATCH меняет разрешённые поля с storefront:write. Частичный patch сохраняет пропущенные поля, null очищает subtitle/title. Текущий slug допускается как no-op, другой —409 SLUG_IMMUTABLE. Пример настроек из проверки:
{
"enabled": false,
"slug": "sample",
"title": "After",
"subtitle": null
}Ответ 200:
{
"updated_at": "2026-09-30T00:00:00.000Z",
"enabled": false,
"slug": "sample",
"title": "After",
"subtitle": null
}Публичный checkout key выпускает site_checkout_key с {} / bodyless POST /api/v1/tools/site/checkout-key при site:write. Ключ tgf_pub_* показывается один раз, имеет только orders и допускается только на отмеченном relay. При ротации прежний ключ остаётся допустимым 24 часа; разверните новый на своём сайте. Такой ключ не читает каталог, заказы или настройки. Origin разрешается в форме, не при выдаче ключа.
Для POST /api/v1/storefront/checkout нужны свой public key, допустимый Origin и непустой Idempotency-Key. Форма должна быть включённой, иметь checkout outcome и свой продукт. Body содержит только opaque code:
{
"code": "aB3dE6gH9jK2mN5p"
}Пример ответа 201; pay.example — искусственный адрес fixture:
{
"order_id": "qR7tY2uI8oP4aS6d",
"payment_urls": {
"stripe": "https://pay.example/fixture"
}
}order_id здесь — публичный opaque code, не PK и не buyer number. Price, product/user ID, redirect URL и external order reference из браузера не принимаются. Сервер сам разрешает форму в tenant и выбирает продукт/условия; origin/form/product admission повторяется перед cached replay. В личном кабинете покупателя /checkout/{seq_num} — P2 с отдельной customer session и проверкой владения: покупатель видит тот же номер заказа, что в диалоге. Публичный relay не заменяет этот guard.
Checkout receipt сохраняется 24 часа отдельно для tenant/credential/relay: тот же key/request воспроизводит 201 и тело, иной запрос с тем же key —409 IDEMPOTENCY_CONFLICT. Для settings PATCH HTTP key необязателен; иной body под переданным ключом —422 IDEMPOTENCY_KEY_MISMATCH. Внутренние AI-агенты не меняют деньги; агент клиента использует денежные сценарии через подготовку, review и финальное подтверждение человека.
Ошибки Hub и дальнейшие действия
Для REST POST с непустым body нужен HTTP key; для bodyless checkout-key он необязателен. Generic HTTP replay хранится 24 часа по tenant/credential/handler/key, сохраняет исходный status; mismatch 422 IDEMPOTENCY_KEY_MISMATCH, processing 409 CONFLICT. Ошибка не является доказательством готовой публикации. У этих site/domain/settings ответов нет cursor pagination и ETag/If-Match; form pull cursor описан отдельно выше. У media ingress собственный контракт: site upload UUID и bundle paths нельзя подставлять в /api/v1/media/uploads.
Для показанного выше запроса подключения POST /api/v1/tools/domain с телом domain:"acme.ru", zone:"ru" аутентифицированный credential в активном бизнесе, у которого нет domain:write, получает следующий отказ Hub P3. Это составленный контрактный пример: основные поля подтверждены принятой boundary fixture, а hint добавлен текущим каноническим фильтром ошибок. Он не является записью запроса к живому хосту и не подтверждает подключение домена.
HTTP/1.1 403 Forbidden
Content-Type: application/problem+jsonПолное тело ответа:
{
"type": "https://telegafirst.ru/docs/errors#INSUFFICIENT_SCOPE",
"title": "Forbidden",
"status": 403,
"instance": "/api/v1/tools/domain",
"code": "INSUFFICIENT_SCOPE",
"traceId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"detail": "Missing required scope(s): domain:write",
"hint": {
"reason": "The credential lacks a scope this operation requires.",
"recovery": "Compare missing_scopes at GET /api/v1/me with this operation and use a key that includes them."
}
}Сравните отсутствующий domain:write с описанием доступа и используйте credential с этим scope для нужного бизнеса. Полномочия владельца, допуск домена и DNS-проверка сохраняют силу. Этот Problem Details относится к управлению доменом через Hub; у анонимной отправки формы P1 остаётся собственный протокол ошибок, описанный выше.
| Status / код Hub REST | Что делать |
|---|---|
400 VALIDATION_ERROR, IDEMPOTENCY_KEY_REQUIRED | Исправьте strict schema/UUID/zone/тип номера или обязательный HTTP key |
401 INVALID_API_KEY | Проверьте допустимый credential и отзыв доступа |
403 INSUFFICIENT_SCOPE, FORBIDDEN | Проверьте live права, owner provenance/approval, Origin и допуск формы |
404 NOT_FOUND | Хост не является своим live подключённым хостом либо secret отсутствует; не перебирайте чужие хосты |
409 CONFLICT | Разберите занятый хост, неверный bundle/dependency, чужой form number, занятое secret name или текущую публикацию |
409 SLUG_IMMUTABLE, IDEMPOTENCY_CONFLICT | Сохраните выбранный slug; для другого checkout используйте новый key |
422 SITE_PATH_OUTSIDE_PUBLISHABLE_PREFIX | На платформенном apex перенесите свои пути под p/ |
422 SITE_PATH_RESERVED | Перенесите файлы с платформенных путей и уберите скрипты service worker; список путей — в ответе |
422 SITE_HTML_TAG_TOO_LARGE | Вынесите встроенный data: ресурс из тега details.tag, атрибут details.attr, страницы details.path в отдельный файл bundle |
422 SITE_VIDEO_TOO_LARGE | Сожмите видео details.path ниже details.limitBytes или встройте его с видеохостинга |
422 SITE_BUNDLE_TOO_LARGE | Уменьшите сайт до details.limitBytes или перейдите на тариф с большим объёмом сайта |
422 SITE_TOO_MANY_FILES | Объедините или уберите файлы до details.limitFiles; предел одинаков на всех тарифах |
429 MINT_QUOTA_EXCEEDED | Суточный бюджет загрузок исчерпан. details.remainingTickets = 0 — повтор только после Retry-After (полночь UTC); тикеты остались — объявите файлы в сумме не больше details.remainingBytes |
503 MINT_QUOTA_UNAVAILABLE | Бюджет не удалось проверить, ticket не выдан; повторите тот же запрос после Retry-After |
422 IDEMPOTENCY_KEY_MISMATCH | Не меняйте body/path/query под уже использованным generic key |
413 PAYLOAD_TOO_LARGE, ITEM_COUNT_EXCEEDED | Уменьшите запрос согласно реальному бюджету операции |
429 RATE_LIMIT_EXCEEDED, RATE_LIMIT_UNAVAILABLE | Соблюдайте Retry-After, если выдан; отказ не обходят |
500 INTERNAL_ERROR | Сохраните traceId; не объявляйте deploy или secret write успешным |
Успешные Hub REST-ответы несут context headers X-Client-Slug и X-Bot-Username, если доступны, а также три X-RateLimit-* headers. Точные DTO, все операции и ошибки — в доменах, сайтах, витрине, формах и общих ошибках. Результат настройки — доступный по своему разрешённому адресу сайт и форма, чьи ответы сразу видны вашему бизнесу; подтверждайте это реальным состоянием своей публикации, а не примером из документации.