Свои HTML-шаблоны
Вместо стандартного окна оплаты можно показать свой HTML. Цены, тексты и тарифы подставляются в него сервером из настроек пейволла, поэтому один шаблон работает на всех языках и валютах.
Как это работает
Шаблон — HTML-файл (или комплект файлов), который сервер хранит у ассистента. Перед показом сервер подставляет в него переменные вида {{PRICE_1}} из бандла пейволла на языке посетителя, и HTML открывается в изолированном iframe внутри модального окна. Оплату запускает не шаблон, а чат: шаблон только сообщает, какой тариф выбран.
Те же методы обслуживают два слота:
| Слот | Что это |
|---|---|
paywall | Свой пейволл. Эта страница. |
frame | Свой экран агента вместо приветствия. См. Фреймы агента. |
Стандартный пейволл, его тарифы и тексты — на странице Пейволл и тарифы.
Какой пейволл увидит посетитель
Три уровня, как в визарде.
- Фича ассистента
feature_enabled. Выключена — всегда стандартный пейволл, что бы ни было загружено. Первый загруженный шаблон пейволла включает её сам. - Шаблоны. Их может быть много, у каждого свой
enabledи список типов диалогаtypes. - Строка Default — стандартный пейволл как участник подбора: свои
default_enabledиdefault_types.
Тип диалога чату проставляет служебный агент по ходу разговора (roleplay, coding…). Список типов, уже встречавшихся у ассистента, приходит в dialog_types. Тег "*" означает «все остальные типы»; пустой types работает так же.
Подбор для типа T:
- Включённый шаблон, у которого
Tесть вtypes. - Default включён и
Tесть вdefault_types— стандартный пейволл. - Включённый шаблон с тегом
"*". - Стандартный пейволл — всегда, даже если Default выключен.
Среди подходящих шаблонов берётся тот, чей locale совпадает с языком посетителя, затем шаблон без языка, затем первый.
Имя шаблона (name) уходит в событие paywall_shown полем template — так в аналитике видно, какой пейволл показан (null — стандартный). См. События и аналитика.
Свойства шаблона
| Свойство | Тип | Описание |
|---|---|---|
id | int | Id шаблона. |
slot | string | paywall или frame. |
name | string | Короткое имя до 32 знаков. По умолчанию генерируется: буква и две цифры (K49). |
types | array | Типы диалога; "*" — остальные. |
enabled | bool | Участвует ли в подборе. Новый шаблон включён. |
locale | string или null | Язык шаблона; null — для всех языков. |
filename | string | Имя главного HTML-файла. |
files | array | Имена всех загруженных файлов. |
entry | string | Главный файл комплекта. |
size | int | Длина собранного HTML в символах. |
html | string | Собранный HTML. В списке не отдаётся — только в ответах на запись и в легаси-методе. |
updated_at | string | ISO 8601. |
Переменные
Синтаксис {{ИМЯ}}, пробелы внутри скобок допустимы. N — номер тарифа начиная с 1. Неизвестная переменная и тариф, которого нет ({{PRICE_9}} при двух тарифах), дают пустую строку.
Нумерация одна на всё: N в {{PRICE_N}} и остальных переменных тарифа, в data-checkout-plan="N" и в paywall.checkout(N) указывает на один и тот же тариф. В нумерацию входят тарифы бандла без тарифа докупки (is_top_up), отсортированные по возрастанию цены; при равной цене сохраняется порядок из pricing_plans. Тариф докупки продаётся только в режиме докупки, как и в стандартном пейволле, поэтому в своём шаблоне его нет. Кнопка с номером, которому нет тарифа, ничего не оплачивает.
| Переменная | Значение |
|---|---|
{{PRICE_N}} | Цена с валютой и периодом: «1 900 ₽/мес», «$14/mo». Сумма округляется до целого. |
{{PRICE_RAW_N}} | Цена числом без форматирования: 1900, 2.99. |
{{OLD_PRICE_N}} | Зачёркнутая цена с валютой, если задана. |
{{DISCOUNT_N}} | Скидка от старой цены: -34%. |
{{CREDITS_N}} | Кредиты тарифа с разделителями разрядов. |
{{CREDITS_RAW_N}} | Кредиты числом. |
{{PLAN_NAME_N}} | Название тарифа. |
{{PLAN_DESC_N}} | Описание тарифа. |
{{INTERVAL_N}} | Суффикс периода: /дн, /нед, /мес, /год (на английском /day, /wk, /mo, /yr), пусто для разовой покупки. |
{{BADGE_N}} | Бейдж тарифа, а если он пуст — скидка. |
{{CURRENCY}} | Символ валюты: ₽ или $. |
{{CURRENCY_CODE}} | Код валюты: RUB или USD. |
{{TITLE}} | paywall_texts.title. |
{{SUBTITLE}} | paywall_texts.subtitle_anonymous. |
{{BUTTON_TEXT}} | paywall_texts.button_text. |
{{FOOTER}} | paywall_texts.footer. |
{{USER_NAME}} | Имя пользователя, у гостя пусто. |
{{BALANCE}} | Баланс кредитов; у гостя пусто, у владельца ∞. |
{{PLANS_COUNT}} | Сколько тарифов в нумерации, без тарифа докупки. |
Русское форматирование чисел и суффиксов — для бандла ru, для остальных английское.
Мост с чатом
HTML работает в iframe с sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox", без allow-same-origin и без allow-forms: у него нет доступа к кукам, сессии и методам чата. Общение — только через postMessage. Мост чат вставляет в HTML сам.
Декларативно:
| Разметка | Действие |
|---|---|
data-checkout-plan="N" на любом элементе | Клик запускает оплату тарифа N. |
data-close на любом элементе | Клик закрывает окно. Стандартного крестика у своего пейволла нет — рисуйте свой. |
Из JS:
| Вызов | Действие |
|---|---|
paywall.checkout(N) | Оплата тарифа N. |
paywall.close() | Закрыть окно. |
paywall.resize() | Пересчитать размер вручную (обычно не нужно). |
Сообщения из iframe помечены source: 'host': ready и resize (с height и width), checkout (с plan), close. Сумму, валюту и кассу считает сервер по своим тарифам — подменить цену из шаблона нельзя. Вход гостя, выбор способа оплаты и согласия рисует чат поверх шаблона.
Чат отправляет в iframe сообщение {source: 'parent', type: 'locale', locale: 'ru'} — при открытии и при каждой смене языка в шапке.
Размеры и вёрстка
Окно подстраивается под содержимое по высоте (120–4000 px) и по ширине (до 2000 px). Ширину берёт по самому широкому видимому потомку body. Пока не загрузились шрифты и стили, окно скрыто, чтобы не мигала сырая вёрстка; крайний срок — 1,8 секунды.
- Делайте фон прозрачным:
html, body { background: transparent }— видна будет только ваша карточка. - Ширину карточки задавайте сами:
max-width: 760pxна компьютере,100%на телефоне. - Точку перехода на компьютерную вёрстку ставьте ниже ширины карточки: при карточке 760 px —
@media (min-width: 700px), а не768px. Иначе окно, ужавшись до 760, переключит вёрстку в мобильную, и она начнёт мигать. - Внешние файлы в iframe не грузятся: у него нет своего адреса. CSS, шрифты и картинки загружайте в комплекте, сервер встроит их в HTML. Ссылки вида
/legal/…ведут на framesuite.app.
Переводы внутри шаблона
Переменные сервер подставляет уже на языке посетителя. Текст, написанный прямо в HTML, сервер не переводит — это делает сам шаблон по сообщению locale. Цены оставьте переменным, в словарь кладите только текст.
<h1 data-i18n="title">Полный доступ</h1>
<button data-checkout-plan="2" data-i18n="button">Продолжить</button>
<script type="application/json" id="paywall-i18n">
{ "en": { "title": "Full access", "button": "Continue" },
"ru": { "title": "Полный доступ", "button": "Продолжить" } }
</script>
<script>
(function () {
var I18N = JSON.parse(document.getElementById('paywall-i18n').textContent);
function apply(loc) {
loc = String(loc || '').toLowerCase().split('-')[0];
var d = I18N[loc] || I18N.en || {}, base = I18N.en || {};
document.querySelectorAll('[data-i18n]').forEach(function (el) {
var k = el.getAttribute('data-i18n'), v = (k in d) ? d[k] : base[k];
if (v != null) el.textContent = v;
});
}
window.addEventListener('message', function (e) {
if (e.data && e.data.source === 'parent' && e.data.type === 'locale') apply(e.data.locale);
});
})();
</script>
Минимальный шаблон
<style>
html, body { background: transparent; margin: 0 }
.pw { max-width: 420px; margin: 0 auto; padding: 24px; border-radius: 16px; background: #fff; font-family: system-ui; text-align: center }
</style>
<div class="pw">
<h2>{{TITLE}}</h2>
<p>{{SUBTITLE}}</p>
<button data-checkout-plan="1">{{PLAN_NAME_1}} — {{PRICE_1}}</button>
<button data-checkout-plan="2">{{PLAN_NAME_2}} — {{PRICE_2}} <s>{{OLD_PRICE_2}}</s></button>
<p style="opacity:.6;font-size:12px">{{FOOTER}}</p>
<a href="#" data-close>Не сейчас</a>
</div>
Методы
Доступ по общему правилу API: владелец ассистента и пользователь с расшаренным доступом. Особого исключения для администратора площадки нет. Чужой или несуществующий ассистент — 404 {"success": false, "error": "Project not found or access denied"}.
Список шаблонов
Состояние фичи, строка Default, известные типы диалога и шаблоны без HTML.
GET /assistants/{assistantId}/custom-templates
| Параметр | Тип | Обяз. | Описание |
|---|---|---|---|
assistantId | int | да | Путь. |
slot | string | нет | Строка запроса. paywall или frame; без него — шаблоны всех слотов. |
curl -s "$BASE/assistants/$ASSISTANT/custom-templates?slot=paywall" \
-H "Authorization: Bearer $KEY"
{
"success": true,
"feature_enabled": true,
"default_enabled": true,
"default_types": ["*"],
"dialog_types": ["advice", "coding", "roleplay", "…"],
"templates": [
{
"id": 12,
"slot": "paywall",
"locale": null,
"types": ["roleplay"],
"filename": "paywall.html",
"name": "K49",
"enabled": true,
"size": 8123,
"files": ["paywall.html", "style.css"],
"entry": "paywall.html",
"updated_at": "2026-07-09T12:00:00+00:00"
}
]
}
| HTTP | Когда |
|---|---|
| 404 | Ассистента нет или нет доступа. |
Добавить шаблон
Каждый вызов создаёт новый шаблон, прежние не трогаются. Принимается только загрузка файлов (multipart).
POST /assistants/{assistantId}/custom-templates/{slot}/templates
| Параметр | Тип | Обяз. | Описание |
|---|---|---|---|
assistantId | int | да | Путь. |
slot | string | да | Путь. paywall или frame. |
files[] или file | file | да | Тело. Один .html, .zip или комплект файлов, вместе до 5 МБ. |
types[] | string | нет | Тело. Типы диалога; можно строкой через запятую или JSON-массивом. |
name | string | нет | Тело. До 32 знаков; не задано — сгенерируется. |
locale | string | нет | Тело. Язык шаблона. |
Допустимые файлы: html, htm, zip, css, js, png, jpg, jpeg, gif, webp, svg, ico, woff, woff2, ttf, а также json, md, txt (лежат рядом, в HTML не встраиваются). Главный файл ищется так: index.html, index.htm, paywall.html, frame.html, иначе первый .html. Сервер собирает самодостаточный HTML: <link rel="stylesheet"> и <script src> превращаются во встроенные <style> и <script>, картинки и шрифты в src, href и url() — в data-URI. Ссылки на внешние адреса остаются как есть и в iframe не загрузятся.
curl -s -X POST "$BASE/assistants/$ASSISTANT/custom-templates/paywall/templates" \
-H "Authorization: Bearer $KEY" \
-F "files[]=@paywall.html" \
-F "files[]=@paywall.css" \
-F "files[]=@fonts/inter.woff2" \
-F "types[]=roleplay"
{
"success": true,
"template": {
"id": 12,
"slot": "paywall",
"locale": null,
"types": ["roleplay"],
"filename": "paywall.html",
"name": "K49",
"enabled": true,
"size": 8123,
"files": ["paywall.html", "paywall.css", "fonts/inter.woff2"],
"entry": "paywall.html",
"updated_at": "2026-09-25T12:00:00+00:00",
"html": "<style>…</style>…"
}
}
| HTTP | Когда |
|---|---|
| 404 | Ассистента нет или нет доступа. |
| 422 | {"success": false, "error": "Unknown slot"}; {"success": false, "error": "Нет файлов"} (в том числе при JSON-теле); «Недопустимый тип файла: .exe»; «Файлы больше 5 МБ»; «В загрузке нет .html файла»; «Не удалось открыть архив». |
Изменить шаблон
Меняет присланные поля.
PATCH /assistants/{assistantId}/custom-templates/item/{id}
| Параметр | Тип | Обяз. | Описание |
|---|---|---|---|
assistantId | int | да | Путь. |
id | int | да | Путь. Id шаблона. |
types | array | нет | Тело. Заменяет список целиком. |
name | string | нет | Тело. Пустое — сгенерируется новое. |
enabled | bool | нет | Тело. false — шаблон остаётся, но в подборе не участвует. |
curl -s -X PATCH "$BASE/assistants/$ASSISTANT/custom-templates/item/12" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"types": ["roleplay", "*"], "enabled": false}'
Ответ: {"success": true, "template": {…}} — шаблон целиком, с html.
| HTTP | Когда |
|---|---|
| 404 | Ассистента нет; шаблона нет у этого ассистента: {"success": false, "error": "Template not found"}. |
Превью на языке
HTML шаблона с настоящими ценами и текстами бандла указанного языка — тот же рендер, что у посетителя, в роли гостя (USER_NAME и BALANCE пустые).
GET /assistants/{assistantId}/custom-templates/item/{id}/preview
| Параметр | Тип | Обяз. | Описание |
|---|---|---|---|
assistantId | int | да | Путь. |
id | int | да | Путь. |
locale | string | нет | Строка запроса. Язык бандла; по умолчанию язык ассистента. |
curl -s "$BASE/assistants/$ASSISTANT/custom-templates/item/12/preview?locale=en" \
-H "Authorization: Bearer $KEY"
{"success": true, "html": "<div class=\"pw\"><h2>Full access</h2>…<button data-checkout-plan=\"1\">Weekly — $6/wk</button>…"}
| HTTP | Когда |
|---|---|
| 404 | Ассистента или шаблона нет. |
Удалить шаблон
DELETE /assistants/{assistantId}/custom-templates/item/{id}
curl -s -X DELETE "$BASE/assistants/$ASSISTANT/custom-templates/item/12" \
-H "Authorization: Bearer $KEY"
{"success": true, "deleted": 1}
deleted: 0 — такого шаблона у ассистента не было. Фича при удалении последнего шаблона не выключается.
| HTTP | Когда |
|---|---|
| 404 | Ассистента нет или нет доступа. |
Включить или выключить фичу
PATCH /assistants/{assistantId}/custom-templates/{slot}/feature
| Параметр | Тип | Обяз. | Описание |
|---|---|---|---|
assistantId | int | да | Путь. |
slot | string | да | Путь. Пишите paywall: флаг один на ассистента. |
enabled | bool | да | Тело. true, false, 1, 0, "true" или "false". |
curl -s -X PATCH "$BASE/assistants/$ASSISTANT/custom-templates/paywall/feature" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"enabled": true}'
{"success": true, "feature_enabled": true}
Старый путь PATCH …/{slot}/enabled делает то же самое.
| HTTP | Когда |
|---|---|
| 404 | Ассистента нет или нет доступа. |
| 422 | Нет enabled, он null или не булев — флаг не меняется: {"success": false, "message": "The enabled field is required and must be a boolean.", "errors": {"enabled": ["The enabled field is required and must be a boolean."]}}. |
Настроить строку Default
Участие стандартного пейволла в подборе. По умолчанию enabled: true, types: ["*"].
PATCH /assistants/{assistantId}/custom-templates/{slot}/default
| Параметр | Тип | Обяз. | Описание |
|---|---|---|---|
assistantId | int | да | Путь. |
slot | string | да | Путь. Только paywall: строка Default есть лишь у пейволла. |
enabled | bool | нет | Тело. |
types | array | нет | Тело. Типы, которые стандартный пейволл забирает себе; [] — ни одного. |
curl -s -X PATCH "$BASE/assistants/$ASSISTANT/custom-templates/paywall/default" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"enabled": true, "types": ["coding"]}'
{"success": true, "default_enabled": true, "default_types": ["coding"]}
| HTTP | Когда |
|---|---|
| 404 | Ассистента нет или нет доступа. |
| 404 | Незнакомый слот: {"success": false, "error": "Unknown slot"}. |
| 422 | Слот frame: {"success": false, "error": "Default row exists only for the paywall slot"}. |
Старые методы: один шаблон на слот и язык
Остались для совместимости. Работают с одним шаблоном на пару «слот + язык» без id.
| Метод | Что делает |
|---|---|
GET /assistants/{assistantId}/custom-templates/{slot}?locale= | Шаблон слота с этим языком (без locale — шаблон без языка), с html. template: null, если нет. |
PUT /assistants/{assistantId}/custom-templates/{slot} | Создаёт или перезаписывает шаблон «слот + язык». JSON: html (обязателен, до 2 МБ), filename, locale, types, name, enabled. |
POST /assistants/{assistantId}/custom-templates/{slot} | То же файлами (-F file=@…). |
DELETE /assistants/{assistantId}/custom-templates/{slot}?locale= | С ?locale=ru удаляет шаблон этого языка, с пустым ?locale= — шаблон без языка, без параметра — все шаблоны слота на всех языках. Ответ: {"success": true, "deleted": N}. |
Это единственный способ залить HTML строкой JSON. Учтите две особенности: PUT перезаписывает первый найденный шаблон с той же парой «слот + язык», даже если он загружен новым методом, а enabled в теле включает или выключает фичу ассистента, а не сам шаблон.
curl -s -X PUT "$BASE/assistants/$ASSISTANT/custom-templates/paywall" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"html": "<div class=\"pw\"><h2>{{TITLE}}</h2><button data-checkout-plan=\"1\">{{PRICE_1}}</button></div>", "types": ["*"]}'
Ответ: {"success": true, "template": {…}, "feature_enabled": true}.
Дальше
- Фреймы агента — свой HTML вместо приветствия агента.
- Пейволл и тарифы — откуда берутся цены и тексты для переменных.
- События и аналитика — какой шаблон видел посетитель.