Пейволл и тарифы
Пейволл — окно оплаты, которое чат показывает, когда у посетителя кончились бесплатные кредиты или сообщения. Условия показа общие для ассистента, а цены, валюта и тексты задаются отдельно на каждый язык.
Как устроен пейволл
Настройки делятся на два уровня.
| Уровень | Поля | Где хранится |
|---|---|---|
| Ассистент | billing_mode, default_credits, paywall_trigger, messages_limit, paywall_limit_action, paywall_cliffhanger_instruction, paywall_auto_google_login | Одно значение на все языки. |
| Бандл языка | currency, pricing_plans, paywall_features, paywall_texts, payment_description | Отдельно на каждый код языка. |
Язык бандла задаётся параметром строки запроса ?locale=, при записи — ещё и полем locale в теле. Если заданы оба, главнее строка запроса. Без них — язык ассистента по умолчанию.
Бандлы пейволла и языки сайта — два разных списка (paywall_locales и locales в GET /locales, см. Языки и переводы). Бандл может существовать для языка, которого нет на сайте: так у русскоязычного ассистента заводят долларовые цены для оплаты иностранной картой.
Какой бандл получит посетитель: бандл его языка → бандл языка ассистента → первый бандл из языков ассистента → старые плоские поля ассистента.
Настройки ассистента
| Свойство | Тип | Описание |
|---|---|---|
paywall_trigger | string | Когда показывать пейволл: never, credits (кончились кредиты), messages (исчерпан лимит сообщений). По умолчанию never. |
messages_limit | int | Лимит сообщений для messages, 1–1000, по умолчанию 3. |
default_credits | int | Кредиты новому посетителю при первом входе, 0–1 000 000, по умолчанию 100. |
paywall_limit_action | string | Что делать на лимите: paywall (по умолчанию) — открыть окно оплаты; cliffhanger — модель обрывает ответ на самом интересном месте одной строкой и мягко говорит, что бесплатные кредиты кончились, а пейволл открывается по кнопке. Для ролевых ассистентов. |
paywall_cliffhanger_instruction | string или null | Стиль обрыва для cliffhanger своими словами, до 2000 знаков. Дописывается к служебной инструкции модели. Пустая строка — null. |
paywall_auto_google_login | bool | true (по умолчанию) — гость кликает по тарифу, сразу открывается окно Google, после входа — оплата. false — внутри пейволла показывается выбор способа входа (Google, Telegram, почта), после входа оплата продолжается тем же тарифом. В Telegram Mini App вход всегда проходит через бота ассистента. Действует и на свой HTML-пейволл. |
billing_mode | string | tokens или subscription. Историческое поле: хранится, но на работу чата не влияет. Разовая это покупка или подписка, решает interval тарифа. |
Бандл языка
| Свойство | Тип | Описание |
|---|---|---|
currency | string | RUB или USD. Валюта тарифов этого языка; от неё зависит касса (см. «Кассы»). |
pricing_plans | array | Тарифы, до 10. Заменяется целиком. |
paywall_features | array | Преимущества списком над тарифами, до 10. Заменяется целиком. |
paywall_texts | object | Тексты окна. Сливается с сохранёнными по ключам верхнего уровня: можно прислать одно поле. |
payment_description | string | Описание платежа в чеке кассы, до 255 знаков. |
Тариф
| Свойство | Тип | Описание |
|---|---|---|
amount | number | Цена в валюте бандла, обязательна, от 0: 3 — это $3, 1900 — это 1900 ₽. |
credits | int | Сколько кредитов даёт тариф, обязательно, от 0. Через это число задаётся маржа. |
interval | string | day, week, month, year — подписка; one_time — разовая покупка. |
name | string | Название, до 50 знаков. |
description | string | Подзаголовок тарифа, до 200 знаков. |
button_text | string | Текст кнопки этого тарифа, до 50 знаков. Пусто — общий paywall_texts.button_text. |
label | string | Бейдж («Выгодно»), до 30 знаков. Синоним при записи — badge. Пусто и задана old_price — бейдж считается сам как «-N%». |
old_price | string | Зачёркнутая цена. Принимает число или строку, хранится строкой. Синоним при записи — old_amount. |
features | array | Пункты внутри карточки тарифа, до 10 строк по 120 знаков. |
is_default | bool | Тариф, выбранный при открытии окна. Ставьте ровно одному. |
is_top_up | bool | Тариф докупки кредитов, см. «Докупка кредитов». В общем списке тарифов не показывается. |
paddle_price_id, paddle_fingerprint | string | Служебные, присылать не нужно: проставляются при синхронизации с Paddle и переносятся между сохранениями по совпадению amount и interval. |
После сохранения тарифов сервер синхронизирует их с Paddle: один продукт на ассистента и одна цена на каждую уникальную тройку «сумма, валюта, период», общая для всех языков. При смене суммы или периода старая цена архивируется. Сбой синхронизации сохранение не отменяет.
Преимущество
| Свойство | Тип | Описание |
|---|---|---|
title | string | До 100 знаков. |
description | string | До 200 знаков. |
icon | string | Имя иконки Lucide (sparkles, globe), до 50 знаков. |
icon_bg | string | Класс фона иконки Tailwind (bg-amber-500), до 50 знаков. |
Тексты
| Свойство | Тип | Описание |
|---|---|---|
title | string | Заголовок, до 100 знаков. Пусто — текст по умолчанию. |
subtitle_anonymous | string | Подзаголовок, до 200 знаков, показывается всем. {balance} заменяется балансом вошедшего; у платных пользователей баланс ∞, поэтому лучше писать нейтральную фразу. Пусто — подзаголовка нет. |
button_text | string | Общий текст кнопки, до 50 знаков. |
footer | string | Строка под кнопкой, html, до 500 знаков. Пусто — строки нет. |
footer_icon | string | Иконка Lucide перед строкой, по умолчанию shield-check. |
sticker_src | string | Адрес картинки над заголовком, до 500 знаков. "none", null или "" — картинки нет. Загрузка — POST /assistants/{assistantId}/upload/image, см. Загрузка файлов. |
recurring_consent | object или null | Режим согласий на автосписания, см. ниже. |
Поле subtitle_authorized встречается в старых бандлах: оно хранится, но пейволл его не выводит.
Методы пейволла
Получить пейволл
Настройки ассистента плюс бандл, подобранный для языка.
GET /assistants/{assistantId}/paywall
| Параметр | Тип | Обяз. | Описание |
|---|---|---|---|
assistantId | int | да | Путь. |
locale | string | нет | Строка запроса. Язык бандла; по умолчанию язык ассистента. |
curl -s "$BASE/assistants/$ASSISTANT/paywall?locale=en" \
-H "Authorization: Bearer $KEY"
{
"success": true,
"billing_mode": "tokens",
"default_credits": 5,
"paywall_trigger": "messages",
"messages_limit": 5,
"paywall_limit_action": "paywall",
"paywall_auto_google_login": true,
"paywall_cliffhanger_instruction": null,
"locales": ["ru", "en", "kk", "…"],
"default_locale": "ru",
"locale": "en",
"currency": "USD",
"pricing_plans": [
{
"name": "Weekly",
"amount": 6,
"credits": 5000,
"interval": "week",
"old_price": "9",
"button_text": null,
"description": "Full access for 7 days",
"paddle_price_id": "pri_01XXXXXXXXXXXXXXXXXXXXXXXX",
"paddle_fingerprint": "4783bee4…"
},
{
"name": "Monthly",
"label": "Best value",
"amount": 14,
"credits": 11667,
"interval": "month",
"old_price": "24",
"is_default": true,
"description": "Full access for 30 days",
"…": "…"
}
],
"paywall_features": [
{"icon": "sparkles", "title": "All premium AI models", "icon_bg": "bg-amber-500", "description": "Claude, Gemini, GPT, Grok, DeepSeek"}
],
"paywall_texts": {
"title": "Full access",
"subtitle_anonymous": "One subscription for every AI model — any task, documents, live web search.",
"button_text": "Continue",
"footer": "<b>Secure payment</b> · 100% money-back guarantee if the service does not suit you",
"footer_icon": "shield-check",
"sticker_src": null
},
"payment_description": "Framesuite AI subscription"
}
locale в ответе — язык бандла, который реально подставился. Запрос ?locale=ar у ассистента без арабского бандла вернёт "locale": "ru" и рублёвые цены. locales — языки сайта, не список бандлов.
| HTTP | Когда |
|---|---|
| 404 | Ассистента нет или нет доступа. |
Изменить пейволл
Пишет присланные поля ассистента и бандл одного языка. Остальные языки не трогает. Если прислано хоть одно поле бандла, бандл собирается заново: присланные поля заменяют сохранённые (paywall_texts — сливается по ключам), не присланные остаются.
PUT /assistants/{assistantId}/paywall
| Параметр | Тип | Обяз. | Описание |
|---|---|---|---|
assistantId | int | да | Путь. |
locale | string | нет | Строка запроса или тело (строка запроса главнее). Язык бандла. Должен быть языком сайта или уже существующим бандлом. |
strict | bool | нет | Строка запроса или тело. true — незнакомые поля дают 422. |
| поля ассистента | нет | Тело. См. «Настройки ассистента». | |
| поля бандла | нет | Тело. См. «Бандл языка». |
curl -s -X PUT "$BASE/assistants/$ASSISTANT/paywall?locale=ru" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"paywall_trigger": "messages",
"messages_limit": 5,
"currency": "RUB",
"pricing_plans": [
{"name": "На неделю", "amount": 490, "credits": 5428, "interval": "week", "old_price": 690, "description": "Полный доступ на 7 дней"},
{"name": "На месяц", "amount": 1900, "credits": 20000, "interval": "month", "label": "Выгодно", "is_default": true}
],
"paywall_texts": {"title": "Полный доступ", "button_text": "Продолжить"}
}'
{
"success": true,
"applied": ["paywall_trigger", "messages_limit", "paywall_locales", "pricing_plans"],
"ignored": [],
"locale": "ru"
}
Поля ассистента billing_mode, default_credits, paywall_trigger, messages_limit, paywall_limit_action со значением null не меняются и в applied не попадают. Пустой currency ("" или null) оставляет валюту бандла прежней.
applied — что записано: paywall_locales означает бандл, pricing_plans — плоскую копию тарифов (её держат в синхроне с бандлом языка по умолчанию для кабинета). ignored — незнакомые поля тела; при них в ответе ещё warning.
| HTTP | Когда |
|---|---|
| 404 | Ассистента нет или нет доступа. |
| 422 | Язык не язык сайта и не бандл: {"success": false, "error": "locale_not_active", "message": "Locale 'xx' is neither a site language nor a paywall bundle. Add it via POST /assistants/53/locales (use site_language=false for a paywall-only bundle)."}. |
| 422 | Ошибка проверки: {"success": false, "errors": {"paywall_trigger": ["The selected paywall trigger is invalid."], "pricing_plans.0.amount": ["The pricing_plans.0.amount field is required when pricing plans is present."]}}. |
| 422 | strict=true и незнакомые поля: {"success": false, "error": "unknown_fields", "message": "Unknown fields: foo", "ignored": ["foo"]}. |
Сценарии
Предвыбранный тариф
Тариф с is_default: true выбран при открытии окна и подсвечен. Флаг живёт в бандле, поэтому ставится в каждом языке вместе с его тарифами.
Докупка кредитов
Если у человека активная подписка, а кредиты кончились до конца оплаченного периода, ему предлагают докупить пакет. Докупка — разовая покупка: кредиты прибавляются к балансу. При продлении подписки баланс выставляется заново, и остаток вместе с докупленным сгорает.
curl -s -X PUT "$BASE/assistants/$ASSISTANT/paywall?locale=ru" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"currency": "RUB",
"pricing_plans": [
{"name": "На неделю", "amount": 490, "credits": 5428, "interval": "week", "is_default": true},
{"name": "На месяц", "amount": 1900, "credits": 18893, "interval": "month"},
{"name": "Пакет кредитов", "amount": 290, "credits": 3000, "interval": "one_time", "is_top_up": true}
]
}'
Флаг помечает ровно один тариф: если помечено несколько, при сохранении он остаётся только у первого. Не помечен ни один — докупкой служит самый дешёвый тариф, и даже подписочный он в этом случае оплачивается разовым платежом. При paywall_trigger: "never" или без тарифов докупки нет.
В своём HTML-пейволле тариф с is_top_up в нумерацию не входит: {{PRICE_N}}, {{PLANS_COUNT}} и data-checkout-plan="N" считают только остальные тарифы по возрастанию цены. В примере выше {{PRICE_1}} — «На неделю», {{PRICE_2}} — «На месяц». Подробнее — Свои HTML-шаблоны.
Маржа и кредиты
Отдельного поля маржи нет, она зашита в число кредитов тарифа. Каждый ответ списывает ceil(cost_usd × 1000) кредитов, то есть кредит — примерно $0.001 себестоимости модели. Себестоимость кредита в валюте бандла: 0.001 для USD и 0.001 × курс для RUB (курс ЦБ из настроек платформы, при его отсутствии 90).
credits = round(amount / (unit × (1 + margin / 100)))
margin = (amount / (credits × unit) − 1) × 100
При марже 20% тариф $3 даёт 2500 кредитов, $16 — 13 333, 1900 ₽ при курсе 90 — около 17 593. Поле credit_multiplier в /models на списание не влияет.
Цены во всех языках
Общего метода нет: получите список бандлов (paywall_locales в GET /locales) и отправьте PUT /paywall?locale=… по каждому, со своей валютой и пересчитанными кредитами. Для языков с одной валютой тело одинаковое, меняется только ?locale=. pricing_plans всегда присылайте полным списком.
Согласие на автосписания
Для касс, которые требуют явного согласия на рекуррентные платежи. Включается в бандле ключом paywall_texts.recurring_consent:
| Свойство | Тип | Описание |
|---|---|---|
subscription_url | string | Соглашение о подписке. Режим включён, только когда это поле не пустое. |
offer_url | string | Оферта рекуррентных платежей. |
privacy_url | string | Политика обработки персональных данных. |
cancel_url | string | Порядок отмены подписки. Хранится, на пейволле пока не выводится. |
В этом режиме стандартный пейволл пишет на кнопке «Подписаться за 1 490 ₽» и показывает под ней две отмеченные галочки со ссылками; пока обе не отмечены, кнопка неактивна. При оплате подписки согласие (адреса документов, время, IP, браузер) сохраняется в платёже. Для разовых тарифов и для своего HTML-пейволла режима нет.
curl -s -X PUT "$BASE/assistants/$ASSISTANT/paywall?locale=ru" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"paywall_texts": {"recurring_consent": {
"subscription_url": "https://example.com/subscription/",
"offer_url": "https://example.com/recurring/",
"privacy_url": "https://example.com/privacy/",
"cancel_url": "https://example.com/cancel/"
}}}'
recurring_consent заменяется целиком — присылайте все ссылки разом. Выключить — "recurring_consent": null. Содержимое объекта сервер не проверяет.
Оплата иностранной картой у русскоязычного ассистента
Способ «Иностранной картой» появляется, когда у ассистента есть долларовый бандл. Переключатель языка на сайте при этом не нужен.
- Завести бандл без языка сайта:
POST /assistants/{assistantId}/localesс{"locale": "en", "site_language": false, "currency": "USD"}(см. Языки и переводы). - Наполнить его:
PUT /paywall?locale=enс"currency": "USD"и долларовыми тарифами. - Проверить: в
GET /localesанглийский есть вpaywall_locales, но нет вlocales.
Визард такой бандл не показывает и при сохранении не затирает; править его можно только через API.
Кассы
У ассистента два слота касс. Слот rub обслуживает бандлы в рублях, в нём может стоять только аккаунт CloudPayments. Слот usd обслуживает остальные валюты, в нём Payonline или Paddle. Кассу для оплаты выбирает валюта бандла. Пустой слот (null) — общая касса платформы.
Поставить в слот можно только свой аккаунт или тот, что уже стоит. Заводит аккаунты касс администратор платформы.
Получить кассы
GET /assistants/{assistantId}/payment-methods
| Параметр | Тип | Обяз. | Описание |
|---|---|---|---|
assistantId | int | да | Путь. |
curl -s "$BASE/assistants/$ASSISTANT/payment-methods" \
-H "Authorization: Bearer $KEY"
{
"success": true,
"project_id": 53,
"rub": {
"account_id": 1,
"account": {"id": 1, "provider": "cloudpayments", "name": "cloudpayments (ai-flip.ru)", "is_test": false}
},
"usd": {
"account_id": 3,
"account": {"id": 3, "provider": "payonline", "name": "payonline (framesuite)", "is_test": false}
},
"defaults": {
"rub": {"id": 1, "provider": "cloudpayments", "name": "cloudpayments (ai-flip.ru)", "is_test": false},
"usd": {"id": 3, "provider": "payonline", "name": "payonline (framesuite)", "is_test": false}
},
"available_accounts": [
{"id": 1, "provider": "cloudpayments", "name": "cloudpayments (ai-flip.ru)", "is_test": false, "slot": "rub"},
{"id": 3, "provider": "payonline", "name": "payonline (framesuite)", "is_test": false, "slot": "usd"}
]
}
| Поле | Описание |
|---|---|
project_id | Id ассистента; поле называется project_id по историческим причинам. |
rub.account_id, usd.account_id | Что записано в слоте; null — общая касса. |
rub.account, usd.account | Касса, через которую фактически пойдёт оплата. null — касса не настроена. |
defaults | Что стоит за пустым слотом. |
available_accounts | Аккаунты, которые вы вправе поставить, с готовым slot. |
| HTTP | Когда |
|---|---|
| 404 | Ассистента нет или нет доступа. |
| 503 | Кассы на платформе ещё не настроены: {"success": false, "error": "Payment accounts are not migrated yet"}. |
Изменить кассы
Присланный ключ ставит слот, отсутствующий не меняется. Ответ — как у GET.
PUT /assistants/{assistantId}/payment-methods
| Параметр | Тип | Обяз. | Описание |
|---|---|---|---|
assistantId | int | да | Путь. |
rub | int или null | нет | Тело. Id аккаунта CloudPayments или null. |
usd | int или null | нет | Тело. Id аккаунта Payonline или Paddle или null. |
Нужен хотя бы один из ключей.
curl -s -X PUT "$BASE/assistants/$ASSISTANT/payment-methods" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"rub": 4, "usd": null}'
| HTTP | Когда |
|---|---|
| 403 | Аккаунт вам недоступен: {"success": false, "error": "Account #4 cannot be used by this user"}. |
| 404 | Ассистента нет или нет доступа. |
| 422 | Нет ни rub, ни usd: {"success": false, "error": "Nothing to update: pass rub and/or usd"}. |
| 422 | Не число или аккаунта нет: {"success": false, "errors": {"rub": ["The rub field must be an integer."]}}. |
| 422 | Касса не подходит слоту: {"success": false, "error": "Slot 'rub' accepts only cloudpayments, account #3 is payonline"}. |
| 503 | Кассы на платформе не настроены. |
Дальше
- Свои HTML-шаблоны — свой пейволл вместо стандартного.
- Подписчики, кредиты, платежи — кто оплатил и сколько кредитов осталось.
- Языки и переводы — языки сайта и бандлы пейволла.