S Subster

Проверить право доступа

Вы зададите нашему серверу главный вопрос интеграции — оплачен ли доступ у этого покупателя — и разберёте ответ по полям.

Покупатель заплатил на веб-странице, вне магазина приложений, и вернулся в приложение. Открывать ли платное, решаете вы, а достоверные данные об оплате есть только у нашего сервера. В ответ приходит список записей о доступе покупателя: действующих, истёкших и отозванных, у каждой свой уровень и срок. Один разбор этого списка закрывает и первую покупку, и продление, и отмену.

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

Какой из двух запросов ваш

Право доступа отдают два адреса. Считают они одинаково, а отличаются входом и полнотой ответа.

Если у васСпрашивайтеЧто получите
есть свой серверGET /s2s/v1/entitlement?guid=<GUID> с ключом в заголовкесписок записей, ссылку на портал управления подпиской и прогноз выручки
приложение без своего сервераGET /public/entitlement?guid=<GUID>, ключ не нужентот же список записей

Начните со своего сервера, если он есть. Решение, принятое на устройстве, подделывается вместе с устройством, а открытый адрес отвечает любому, кто знает идентификатор. То, что приложение знает о подписке, годится для интерфейса; пускать в платное стоит по ответу, который получил ваш сервер.

Есть и библиотека, и свой сервер. Совмещайте: библиотека опознаёт покупателя и отдаёт приложению идентификатор, приложение передаёт его вашему серверу, а сервер спрашивает право с ключом. Какой путь выбрать в кабинете при таком сочетании — «Обзор», раздел «Какой путь ваш».

Запрос и ответ

curl "https://api.subster.ai/s2s/v1/entitlement?guid=<GUID>" \
  -H "Authorization: Bearer sk_live_…"
ПараметрОбязателенЧто это
guidдаидентификатор покупателя, от 8 до 64 символов
userнетто же значение под вторым именем, оставлено для совместимости со старыми интеграциями — передавать не нужно. Принимает только запрос с ключом. Оба имени с разными значениями дают отказ guid_user_mismatch, ни одного — guid_required

Других параметров адрес не знает: лишний, например метка против кеша, получит отказ 400. Исключение — устаревший externalId у запроса с ключом: он принимается, но на ответ не влияет.

{
  "guid": "abc123def456",
  "user_id": "abc123def456",
  "testMode": false,
  "grants": [
    {
      "level": "premium",
      "status": "active",
      "expires_at": "2026-10-21T00:00:00.000Z",
      "price_id": "price_1Tc…",
      "will_renew": true,
      "livemode": true,
      "amount": 1999,
      "currency": "usd",
      "interval": "month",
      "interval_count": 1,
      "current_period_start": "2026-09-21T00:00:00.000Z",
      "current_period_end": "2026-10-21T00:00:00.000Z",
      "is_trial": false,
      "trial_ends_at": null,
      "expiryPending": false
    }
  ],
  "manage_link": "https://…"
}

Ответ не завёрнут: список grants лежит на верхнем уровне. Часть других наших адресов заворачивает ответ в { success, data }, поэтому форму сверяйте с примером того запроса, который зовёте.

Как решить, пускать ли в платное

Доступ открыт, если среди записей есть хотя бы одна со статусом active:

const paid = data.grants.some(g => g.status === "active");

Статус лежит внутри каждой записи, на верхнем уровне ответа его нет. Код, который читает data.status, получит пустое значение у каждого покупателя, включая оплатившего. Ошибкой это не выглядит: приложение молча никому не откроет платное.

СтатусЧто этоЧто делать
activeоплачено и действуетпускать в платное
active с will_renew: falseдействует, но продления не будетпускать до даты окончания и показать её покупателю
expiredсрок кончилсяне пускать, предложить продлить
revokedдоступ отозванне пускать

Других статусов не бывает. Пробный период отдельным статусом не приходит — он помечен признаком внутри действующей записи. Доступ отзывают за возврат денег или проигранный спор по платежу, сорвавшийся отложенный платёж, истёкший льготный период после неудачного продления, отмену подписки и отключение платёжной системы в проекте. В ответе повод не назван; у отзыва по подписке он приходит в событии entitlement.revoked («Вебхуки»).

Если в организации несколько проектов, в список попадают и записи этого покупателя из соседних проектов. Когда проекты продают разные приложения, пускайте по уровню записи, а не по одному статусу. Покупатель из другой организации — другой случай: записей о нём не будет, запрос с ключом ответит 404.

Поля ответа

Верхний уровень:

ПолеТипКогда естьЧто делать
guidстрокавсегдаидентификатор, который вы передали
user_idстрокатолько с ключомто же значение, что guid
testModeлогическоевсегдаtrue — включён режим проверки: в списке одна выдуманная запись, см. раздел ниже. Проверять достаточно этого поля
grantsмассиввсегда, бывает пустымзаписи о доступе от новой к старой, со всеми статусами
manage_linkстрока или nullтолько с ключомссылка на наш экран портала управления подпиской, живёт 15 минут. null — портал в проекте не готов. Подробно — «Деньги из приложения»

Запись о доступе:

ПолеТипКогда заполненоЧто делать
levelстрокавсегдаоткрыть то, что соответствует уровню. У цены без заданного уровня здесь её идентификатор — level совпадает с price_id
statusстрокавсегдаactive, expired или revoked; пускать только при active
expires_atдата ISO 8601 в UTC или nullnull — срока нет: разовая покупка или срок подписки ещё не вычисленпоказать покупателю «доступ до …»; null читать вместе с expiryPending
price_idстрокавсегдацена, по которой выдан доступ; по ней сопоставляйте запись с событиями и своим каталогом
will_renewлогическоевсегдаfalse — продления не будет. У разовой покупки приходит true, хотя продлевать нечего
livemodeлогическое или nullnull у записей, заведённых до появления поляfalse — оплата прошла в тестовой среде платёжной системы, настоящих денег не было. Режим проверки связки — другое, он помечен testMode
amountцелое или nullnull, если этой цены нет среди цен проекта, которые знает наш серверсумма в минимальных единицах валюты: 1999 у usd — это 19,99, а не 1999
currencyстрока или nullкак у amountкод валюты нижним регистром: usd, eur
intervalстрока или nullкак у amountday, week, month, year; у разовой покупки — one_time. Подписку от разовой покупки отличайте по этому полю, а не по will_renew
interval_countцелое или nullкак у amountмножитель периода: 3 у цены «раз в три месяца»
current_period_startдата или nullnull у разовой покупки, у записи без срока и там же, где amountначало текущего периода
current_period_endдата или nullвсегда равно expires_atвторое имя того же срока для кода, который ждёт пару «начало и конец периода»; берите любое из двух
is_trialлогическоевсегдаtrue — пробный период идёт прямо сейчас
trial_ends_atдата или nullтолько пока is_trial: trueкогда пробный период кончится
expiryPendingлогическоевсегдаtrue — доступ уже есть, а конец периода ещё не подтянулся; это не бессрочный доступ
testModeлогическоетолько на выдуманной записи режима проверки, вместе с testMode: true верхнего уровняотдельно проверять не нужно
projected_revenue_32d, _62d, _184d, _367dцелое или nullтолько с ключом и только у подписоксредняя накопленная выручка с одного покупателя к 32-му, 62-му, 184-му и 367-му дню по статистике всего проекта, в минимальных единицах валюты; одна и та же у всех подписок. null — данных пока мало. К доступу покупателя отношения не имеет
projection_statusстрокатам жеok — посчитаны все четыре срока, insufficient_data — не все

Уровень сверяйте с тем, что настроено. Пары «цена → уровень» задаются в кабинете: «Настройки проекта» → «Подключение приложения» → вкладка «Основное» → ветка «Что открывать в приложении после оплаты». Цена без пары приходит со своим идентификатором вместо уровня, и приложение, которое сравнивает уровень с ожидаемым словом, не откроет ничего. В коде это видно так: level записи равен её price_id. Пары открываются галочкой «У меня несколько разных уровней» — ставьте её и при одном уровне: поле «Название платного доступа в приложении» над ней заблокировано.

Срок подписки появляется не мгновенно. Право выдаётся в тот момент, когда покупатель возвращается со страницы оплаты, а конец периода подтягивается следом. В эти секунды приходит действующая запись без срока и с expiryPending: true — доступ у покупателя уже есть. У разовой покупки срока нет по замыслу, и признак у неё false: по нему эти два случая и различаются.

Почему пустой список приходит и на незнакомый идентификатор

Открытый адрес отвечает на незнакомый идентификатор так же, как на знакомого покупателя без покупок: успехом и пустым списком. Иначе идентификаторы можно было бы перебирать и узнавать чужие покупки. Запрос с ключом отвечает «не найдено» и на незнакомый идентификатор, и на покупателя из другой организации — эти случаи тоже неотличимы.

Пустой список сам по себе не значит, что интеграция сломана. Что смотреть, когда покупатель заплатил, а записей нет, — «Диагностика».

Если у сервера есть только почта

Бывает, что ваш сервер знает почту покупателя, а идентификатор не сохранил. Тогда сначала спросите идентификатор по почте тем же ключом:

curl -X POST "https://api.subster.ai/s2s/v1/identity/resolve-by-email" \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "email": "[email protected]" }'
{ "guid": "abc123def456", "user_id": "abc123def456" }

Мы ищем только подтверждённую почту — ту, что покупатель указал при оплате. Подтверждает её переход по нашей ссылке с кодом, и чаще всего это возврат в приложение после оплаты.

Пока покупатель ни разу не вернулся в приложение по ссылке, рассчитывайте на «не найдено» — тот же отказ, что на незнакомую почту. Опознание при установке из магазина приложений и по отпечатку устройства почту не подтверждает.

Тот же поиск есть на открытом адресе /public/identity/resolve-by-email с подписью секретом — он нужен, только если ключа на сервере нет («Аутентификация»).

Пока включён режим проверки

Режим проверки связки нужен, чтобы пройти весь путь до первой настоящей оплаты. Включается он в кабинете:

  1. «Подключение приложения» → вкладка «Основное» → ветка «Проверить связку до первой оплаты» → «Включить».
  2. «Сохранить настройки» внизу вкладки — без этого режим не включится.

Работает режим на пути «Наша библиотека в приложении». На путях «Собственный сервер» и «Приложение не подключаю» его нет: ветку кабинет не показывает, а при сохранении настроек режим выключается сам. Про пути — «Обзор».

Пока режим включён, ответ приходит с testMode: true и одной выдуманной записью: уровень и идентификатор цены — test, статус действующий, срока и полей цены нет.

Так отвечают только на идентификатор, который проект уже знает: покупатель прошёл воронку или ваш сервер завёл его запросом POST /s2s/v1/users. На выдуманный идентификатор придёт обычный ответ — как проверить, в «Диагностике», раздел «Проверить, не дожидаясь настоящей оплаты».

Настоящие покупки в таком ответе не читаются вовсе. Выключите режим перед выходом на бой — иначе платное открыто каждому, кого знает проект.

Когда спрашивать и можно ли хранить ответ

Спрашивайте первый раз на открытии приложения после возврата с оплаты — право к этому моменту, как правило, уже выдано. Пришёл пустой список — повторите запрос через несколько секунд: изредка платёж подтверждается не на возврате, а уведомлением от платёжной системы следом.

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

Ссылку manage_link не храните: берите её из свежего ответа прямо перед показом кнопки. Как отвечать приложению из сохранённого, пока мы недоступны, — в «Рецептах».

Отказы и ограничения

ОтветКогда приходитЧто делать
400идентификатор не передан, короче 8 или длиннее 64 символов, в адресе лишний параметр; у запроса с ключом сюда же относятся guid_required и guid_user_mismatchисправить запрос: повтор того же запроса ответит тем же
404только у запроса с ключом: идентификатор неизвестен или принадлежит другой организациисчитать покупателя неоплатившим
429частота превышенаповторить позже
401, 403только у запроса с ключом: отказ из-за ключаразбор в «Аутентификации»

Открытый адрес отказывает телом { "statusCode", "message" }, запрос с ключом — { "error": { "type", "code", "message" } }.

Потолок частоты: у открытого адреса 60 запросов в минуту с одного IP-адреса, у запроса с ключом — 120 запросов в минуту на ключ. Как их считают — в «Аутентификации», раздел «Сколько запросов можно».

Дальше