Перейти к содержанию

Изменения API

Здесь только то, что видно клиенту: поля запросов и ответов, коды, лимиты, заметное поведение. Внутренние правки сюда не попадают. Машинный контракт всегда актуален на странице API Reference, эта страница отвечает на другой вопрос: что изменилось с прошлого раза и что с этим делать в приложении.

Ломающие изменения помечены ⚠️.

2026-10-18 - петля сетки всегда пережата

  • preview.animation больше не бывает тем же файлом, что preview.video: это всегда петля 480 по короткой стороне, по умолчанию весь клип в своём темпе. Поля и их форма не меняются, в сетке просто играет файл в несколько раз легче. Подробнее.

2026-10-15 - отзыв входа через Apple

  • POST /v1/auth/apple принимает необязательное authorization_code из того же ответа Sign in with Apple, что и id_token. Передавай его при каждом входе: при удалении аккаунта сервер отзывает по нему доступ приложения к Apple ID, как требует App Store. Без него вход работает как раньше. Подробнее.

2026-10-14 - фото по персонажам

Эффект говорит, сколько людей он берёт и сколько фото каждого, а генерация принимает фото по людям. Сейчас все эффекты каталога берут по одному фото на человека, и старые сборки с ними работают как раньше.

  • У эффекта новое поле characters: список персонажей, у каждого min_photos и max_photos. Первый персонаж обязателен, у необязательного min_photos = 0. Несколько фото одного персонажа - ракурсы одного лица. min_inputs / max_inputs остаются и равны суммам по персонажам.
  • POST /v1/generations принимает фото полем characters: [[upload_id, …], …], по списку на персонажа в порядке эффекта. Старое upload_id + additional_uploads тоже принимается, а additional_uploads теперь до 15 элементов вместо 8.
  • ⚠️ Плоский список на эффект, где из него не понять, чьё фото чьё (у персонажа может быть разное число фото), отклоняется 422 с новым кодом characters_required. Такой эффект запускается только полем characters. В каталоге таких эффектов пока нет, они появятся с видео-моделями, которые берут несколько ракурсов одного человека.
  • Одно и то же upload_id дважды в поле characters или в двух полях-фото студии - 422 validation_error: каждое фото - своя загрузка. Старый плоский список повтор по-прежнему принимает.
  • Видео-эффекты на Grok теперь учитывают в цене входные картинки, как модель и списывает: cost таких эффектов немного вырастет после пересохранения.

Подробно - Каталог, Генерации, Ошибки.

2026-10-12 - поиск по каталогу находит эффекты по теме

Контракт GET /v1/effects?search= не меняется. На сервере появился поиск по смыслу: запрос находит эффект и без общих слов («шпион» → «Секретный агент»). Он включается на каждом окружении отдельно и по умолчанию выключен.

  • Эффекты, найденные по теме, стоят после тех, что запрос называет прямо, их не больше десяти.
  • По теме находится только то, что заметно ближе к запросу, чем остальной каталог. Запрос, которому ничего не подходит, по-прежнему возвращает пустой список.
  • Заметной задержки к ответу поиск по теме не добавляет.
  • Запрос короче трёх символов ищется только по словам.

Подробно - Каталог.

2026-10-12 - поиск по каталогу ищет слова, а не подстроку

Контракт GET /v1/effects?search= не меняется: те же параметры, тот же ответ. Меняется то, что находится и в каком порядке.

  • Раньше search искал подстроку в названии и slug. Теперь он ищет слова запроса в названии, тегах, категориях и описании, понимает формы слова («супергероя» → «Супергерой»), опечатку в одну-две буквы и недописанное слово. Кусок из середины слова больше не находит: «герой» не даст «Супергерой» по названию.
  • Выдача с search упорядочена по близости к запросу: название, затем теги и категории, затем описание. Ранжирование внутри категории при поиске не действует.
  • Строку можно слать как есть, в том числе распознанную с голоса: регистр, пунктуацию и слова просьбы («покажи», «найди мне») сервер убирает.
  • Потолок длины search поднят со 100 до 300 символов, длиннее по-прежнему 422. Расшифровку длиннее 300 символов приложение обрезает по границе слова.

Подробно - Каталог.

2026-10-12 - эффект лежит в нескольких категориях

Контракт только расширяется, старые сборки продолжают работать.

  • У эффекта новое поле category_ids: все его категории списком. category_id остаётся и равен первой из них, включённые категории идут в списке первыми. В новом коде читай список.
  • GET /v1/effects?category= находит эффект по любой из его категорий, по id или slug. В плоском списке без category эффект по-прежнему приходит один раз.
  • effect_count в GET /v1/categories считает эффект в каждой его категории, поэтому сумма по категориям может быть больше total плоского списка.
  • В списки попадает только эффект, у которого есть хотя бы одна включённая категория. Раньше эффект совсем без категории оставался в плоском списке, в поиске и на странице автора, теперь его там нет. По прямому id или slug он открывается и запускается как раньше.
  • Порядок внутри категории учитывает ручную расстановку оператора: часть эффектов он может поставить наверх или вниз ленты. Формат ответа не меняется, список по-прежнему нужно показывать в том порядке, в котором он пришёл.

Подробно - Каталог.

2026-10-12 - голосовой ввод: распознавание речи

Новая ручка POST /v1/speech/transcriptions: приложение шлёт короткую запись голоса файлом (multipart/form-data, поле audio), сервер отвечает её текстом:

{ "text": "Неоновый город.", "language": "ru" }
  • Ручка общая для любого голосового ввода. Найти эффект голосом - это два запроса: распознать, затем передать текст в обычный GET /v1/effects?search=.
  • text приходит как есть, с пунктуацией модели и без ограничения длины.
  • Нужен токен зарегистрированного пользователя: гостю 403 registration_required.
  • Запись: m4a/mp4, mp3, wav или webm, не длиннее 15 секунд и не больше 512 КиБ.
  • Речь не распознана - ответ 200 с пустым text.
  • Новый код ошибки 415 unsupported_media_type: формат записи не поддерживается. Запись сверх предела - 413 payload_too_large, распознавание недоступно - 503 provider_unavailable, слишком частые запросы - 429 rate_limited с Retry-After.

Существующие ручки не меняются. Подробно - Голосовой ввод.

2026-10-12 - цена своего эффекта известна сразу

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

Отказ 422 (validation_error) на POST /v1/generations в первые секунды-минуты processing остаётся: пока система не собрала эффект, он не запускается и ничего не списывает. Подробно - Свои эффекты, «Прогнать свой эффект до публикации».

2026-10-11 - реферальный бонус не платится за свой второй аккаунт

Контракт не меняется. Если приглашённый и пригласивший когда-либо входили с одного устройства (device_id в регистрации и входе), регистрация по коду проходит и привязка пишется, но бонус не получает ни одна сторона: в GET /v1/referral растёт invited, а credited и earned остаются прежними. Подробно - Реферальная программа.

2026-10-10 - свой шаблон без цены не запускается

POST /v1/generations по собственному шаблону, у которого ещё нет цены (cost: 0 в первые секунды-минуты processing), теперь отвечает 422 вместо бесплатного запуска. Как только система назначила цену, запуск работает как раньше и списывает её. В приложении кнопку «попробовать» стоит показывать при cost > 0.

2026-10-10 - доля автора приходит в кошелёк после холда

Новые поля, старые не менялись. Меняется момент зачисления.

  • Доля автора за чужую генерацию сначала ждёт холд (сейчас 14 дней) и только потом попадает в кошелёк. Если покупатель за это время вернёт деньги, все его ожидающие доли снимаются и в кошелёк не приходят.
  • pending в GET /v1/user-effects/account: часть earned, которая ещё на холде.
  • settles_at у строк GET /v1/user-effects/earnings: когда начисление придёт в кошелёк, null - уже пришло или снято.
  • effect_earned в истории кредитов и push о начислении приходят в момент зачисления, а не генерации. earned в счёте и у эффекта растёт сразу, как и раньше.
  • Что стоит сделать в приложении: показать pending как «ожидает зачисления» рядом с балансом автора, чтобы рост earned без роста кошелька не выглядел ошибкой.

Подробно - Свои эффекты, «Кабинет автора».

2026-10-10 - новый код отказа original_unavailable

Правка эффекта больше не публикуется, если, пока она ждала проверки, сам эффект ушёл из каталога: его отклонили, сняли или он ушёл в архив. Такая правка получает rejected с reject_reason: "original_unavailable" и готовым reject_reason_text. Паузу после отказа код не включает. Сборки, которые показывают reject_reason_text, ничего менять не должны; если приложение ветвится по коду, неизвестный код стоит вести как other.

2026-10-10 - login закрывается для новых устройств, если пароль подбирают

Полей и кодов не прибавилось. Если к одному аккаунту за час пришло много неверных паролей с разных адресов, POST /v1/auth/login до конца часа принимает пароль только с устройства, с которого в этот аккаунт уже входили, остальным отвечает 429 rate_limited.

  • Что стоит сделать в приложении: передавать в login тот же device_id, что и в остальных запросах. Сборка без него в такой момент войти не сможет.

Подробно - Аутентификация, «Лимиты».

2026-10-10 - push-токен привязан к установке: device_id в POST /v1/devices

Новое необязательное поле. Сборки, которые его не шлют, работают как раньше.

  • device_id в теле POST /v1/devices - тот же идентификатор установки, что уходит при входе. Токен, зарегистрированный с ним, переходит на другой аккаунт только с той же установки: запрос с другим device_id или без него получает 403 (forbidden), а токен остаётся у прежнего владельца.
  • Что стоит сделать в приложении: слать device_id при каждой регистрации токена. Без него токен можно перехватить, зная его значение.

Подробно - Push-устройства.

2026-10-09 - повтор запроса без второго списания: Idempotency-Key

Новый необязательный заголовок. Сборки, которые его не шлют, работают как раньше.

  • Idempotency-Key принимают POST /v1/generations, POST /v1/studio/generations, POST /v1/payments и POST /v1/subscriptions. Повтор с тем же ключом и тем же телом возвращает объект, созданный в первый раз: ту же генерацию без второго списания, тот же платёж с той же payment_url, ту же подписку вместо 409 already_subscribed.
  • Ключ создаёт приложение, один на нажатие: от 16 до 64 символов из A-Z a-z 0-9 _ -, удобнее всего UUID. Автоматические повторы одного нажатия идут с тем же ключом.
  • Новые коды ошибок, оба 409: idempotency_key_reused (ключ уже приходил с другим телом, это ошибка в приложении) и request_in_progress (только оплата и подписка: первый запрос ещё ждёт ответа платёжного центра, повторить через 1-2 секунды с тем же ключом). Если общий обработчик 409 у тебя показывает «уже подписан», разведи его по code.
  • Что стоит сделать в приложении: слать ключ на всех четырёх ручках и после обрыва связи или таймаута повторять запрос с ним же, а не начинать заново.

Подробно - Ошибки, «Повтор запроса», примеры в Генерации и Оплате.

2026-10-09 - у генерации в processing появился срок

  • Запуск, который провайдер не закончил за час с отправки, завершается failed с error_code = provider_timeout, кредиты возвращаются. Раньше такой запуск мог оставаться в processing без срока. Код ошибки прежний, приложению менять нечего.

2026-10-08 - у offset в списках появилась верхняя граница

  • ⚠️ offset принимает значения от 0 до 10000 во всех постраничных списках: /v1/effects, /v1/authors/{author_id}, /v1/generations, /v1/credits/transactions, /v1/user-effects, /v1/user-effects/earnings. Больше - 422 validation_error. Раньше граница была только у limit.
  • total по-прежнему считает все строки, а получить можно первые 10000 + limit. Листание заканчивай на offset = 10000, а не по total. Пока списки короче, приложению менять нечего.

2026-10-05 - точка фокуса постера и клипа

Новые поля, старые сборки их не читают и работают как раньше.

  • preview.focus у эффекта: {"x": 0.63, "y": 0.34}, точка постера в долях ширины и высоты от левого верхнего угла. Когда плитка другой формы, чем постер (карточка 3:4, квадратное промо), окно обрезки ставится центром на эту точку, а не на середину кадра, и лицо остаётся в плитке. null - точки нет, режь по центру.
  • preview.video_focus - то же для клипа, одна точка на preview.video и preview.animation. Отдельная от постера: клип часто другой формы.

Формула выравнивания - Каталог, поле preview.

2026-10-05 - подписка: кредиты плана сгорают, отмена с причиной

Раздела подписок в приложении ещё нет, поэтому сломать нечего, но экран делать уже по этому контракту.

  • Кредиты плана живут один период. При продлении остаток сгорает и приходят кредиты нового периода; когда подписка закрывается, остаток сгорает совсем. Пакеты, приветственные и заработанные кредиты не сгорают. Раньше не сгорало ничего.
  • GET /v1/credits/balance отдаёт два новых поля: plan_credits (какая часть balance сгорит) и plan_expires_at (когда; null без подписки). balance значит то же, что и раньше. В истории появился kind: "plan_expired" - запись о сгорании.
  • ⚠️ POST /v1/subscriptions/{id}/cancel требует тело: reason из закрытого списка и необязательный comment. Без reason - 422.
  • Один план на аккаунт. Другой план оформляется без отмены текущего и заменяет его в момент оплаты, остаток прежнего сгорает.
  • Напоминания push за 3 дня и за 1 день до конца периода: data.status = "period_ending", data.days_left = "3" или "1".

Подробно - Подписки и Кредиты.

2026-10-04 - студия: цена на каждый запуск, потолок длины ответа

Поля и формат ответа прежние.

  • Цена считается на каждый запуск. Раньше её фиксировал первый запуск с данными ответами: запуск с двумя фото после запуска с одним списывал цену одного, а в обратном порядке expected_cost отвечал 422 «Цена изменилась» при той же оценке. Теперь списание всегда равно оценке для приложенного числа фото.
  • Текстовый ответ формы - до 1 000 символов, запрос после подстановки ответов - до 10 000. Длиннее - 422 validation_error. Ограничь поле ввода в приложении.

Подробно - Студия.

2026-10-04 - подписка: только карта, условия фиксируются при оформлении

Формат ответов прежний, меняются ответы на оформление (POST /v1/subscriptions):

  • method: "sbp" теперь отвечает 422 validation_error. Раньше такая подписка оформлялась, отрабатывала первый период и молча не продлевалась: продление идёт по токену карты.
  • Появились 403 forbidden (план продаётся ограниченное число раз на аккаунт) и 429 rate_limited (слишком много незавершённых оплат), те же, что у разового пакета.

Продление списывает сумму и начисляет кредиты, зафиксированные при оформлении (amount_minor и credits подписки), а не текущую цену плана. Подписка, которую нечем продлить, переходит в expired, когда кончается оплаченный период. Подробно - Подписки.

2026-10-04 - failed до отправки модели приходит в стрим и пушем

Поля и формат ответа прежние. Если генерация падала ещё до отправки модели (негодное фото, отказ провайдера принять задачу), кредиты возвращались, но ни события в SSE-стриме, ни пуша не было: стрим молчал до конца своего срока жизни, и узнать об ошибке можно было только запросом GET /v1/generations/{id}. Теперь терминальный статус приходит в стрим и пушем при любом исходе. В приложении менять ничего не нужно. Подробно - Генерация.

2026-09-30 - расширение результата по формату

Поля и формат ответа прежние. Картинка раньше лежала под .jpg, даже когда внутри PNG или WebP, и приложение, которое сохраняло файл по имени из ссылки, получало PNG с расширением .jpg. Теперь расширение в result_url то же, что у файла, который отдала модель, и совпадает с форматом. Результаты, готовые до этого дня, остаются под .jpg, так что формат по-прежнему надёжнее брать из Content-Type. Подробно - Генерация.

2026-09-29 - петля сетки выбирается по эффекту

Поля и формат ответа прежние. preview.animation раньше всегда была первыми 3 секундами клипа, и у превью «до/после» в сетку не попадало «после». Теперь контент выбирает петлю для каждого эффекта: первые 3 секунды, весь клип за 3 секунды, весь клип в своём темпе или сам клип карточки. Петля может быть длиннее 3 секунд и совпадать с preview.video. Играй её по-прежнему без звука и по кругу, на длину не рассчитывай. Подробно - Каталог.

2026-09-28 - свой промпт в студии

Новые необязательные поля, старые не меняются: сборка, которая их не читает и не шлёт, работает как работала.

prompt в категории студии (GET /v1/studio/categories) - промпт задачи, если она разрешает его править, иначе null. Показывай его заполненным в поле ввода.

prompt при запуске (POST /v1/studio/generations) - текст пользователя вместо промпта задачи, до 10 000 символов. Пустой или не присланный - запуск по промпту задачи. Подстановки полей формы ({product_name}) в нём работают. В задачу с prompt: null его присылать нельзя, придёт 422. Цена от текста не зависит. Подробно - Студия.

2026-09-28 - результат с прозрачным фоном

Поля и формат ответа прежние. Эффект вроде «убрать фон» теперь возвращает PNG с альфа-каналом, а не картинку на подложке. Показывай его без подложки, «Сохранить» и «Поделиться» отдавай тем же PNG, иначе фон закрасится. Формат бери из Content-Type ответа: расширение в result_url у картинки всегда .jpg. Превью для галереи (preview.thumb, WebP) прозрачность тоже сохраняет. Подробно - Генерация.

2026-09-24 - клип и петля не больше 60 fps

Поля и формат ответа прежние. Клип (preview_video_url, он же preview.video) и петля для сетки (preview.animation) теперь не больше 60 кадров в секунду: телефонный экспорт в 120 или 240 fps перекодируется в 60, ролик в 60 fps и медленнее сохраняет свою частоту. Пропорция по-прежнему как у загруженного ролика.

2026-09-23 - превью результатов для галереи

Новое необязательное поле, старые не меняются: сборка, которая его не читает, работает как работала.

preview в генерации - уменьшенная копия результата для галереи и истории: WebP 480 по короткой стороне (thumb), у видео ещё poster (кадр на полсекунды от начала, тот же файл), размер оригинала (width, height) и blurhash. Приходит в GET /v1/generations и GET /v1/generations/{id}. Копия весит десятки килобайт вместо мегабайт у result_url, а у видео наконец есть кадр для плитки.

Ссылки подписанные и живут столько же, сколько result_url: результаты личные. preview = null у старых генераций и пока генерация не готова: тогда показывай result_url, как раньше. Копия готова до succeeded, так что деталь, запрошенная по SSE или push, уже её несёт. Подробно - Генерация.

2026-09-22 - теги для фильтров каталога

Новый эндпоинт, существующие не меняются.

GET /v1/tags - чипы фильтра: slug, name на языке locale и effect_count. slug уходит в GET /v1/effects?tag=<slug>, name показывается пользователю: в tags эффекта лежат ключи латиницей, а не подписи. В ответе только теги, которые стоят хотя бы на одном доступном эффекте, самые частые первыми. Подробно - Каталог.

2026-09-22 - превью каталога в размер тайла

Новое необязательное поле, старые не меняются: сборка, которая его не читает, работает как работала.

preview в эффекте - постер в трёх размерах WebP (thumb / card / full, по короткой стороне 480 / 720 / 1080), размер оригинала (width, height) и blurhash для заглушки. Приходит везде, где приходит эффект: GET /v1/effects, GET /v1/effects/{id}, профиль автора, а также в элементах GET /v1/user-effects. Оригинал в preview_url весит мегабайты, копия для сетки - десятки килобайт. URL неизменяемые, кэшировать можно навсегда.

preview = null, пока копии не готовы: тогда показывай preview_url, как раньше. Подробно - Каталог.

Петля для сетки - видео, а не WebP. preview.animation - MP4 480 на 3 секунды без звука, около 200 КБ: анимированный WebP того же размера весил 0,5-1 МБ. Играть без звука и по кругу. preview_animation_url для старых сборок остаётся WebP.

Клип превью легче. preview_video_url теперь H.264 720 по короткой стороне, без звука, с индексом в начале файла: плеер стартует до полной докачки. Адрес у каждого эффекта сменится один раз, поле и формат ответа прежние.

result_url живёт час вместо 10 минут. Галерея результатов больше не сереет посреди сессии. Истёкшая ссылка по-прежнему лечится повторным запросом генерации.

2026-09-21 - студия: генерация под свою задачу

Новые ручки, ничего существующего не меняют: клиент, который их не вызывает, продолжает работать как работал. Но это первое место, где при генерации передаются не только фотографии - ответы формы и выбранные настройки, - поэтому смотреть на неё как на «ещё один эндпоинт каталога» не стоит. Полное описание - Студия.

GET /v1/studio/categories - список задач («убрать фон», позже другие) с описанием формы: какие поля спросить, какие обязательны, какие настройки модели эта задача разрешает менять. Экран строится из этого ответа: состав задач и форм меняется в админке без релиза приложения. Набор типов полей (photo, text, enum, integer, boolean) фиксирован - новый тип потребует релиза, и мы предупредим заранее.

POST /v1/studio/generations - запуск. Ответы формы идут в values (ключи - это key из fields), выбранные настройки в params; поле photo несёт идентификатор обычной загрузки. Промпт собирается на сервере по шаблону задачи. Ответ такой же, как у обычной генерации - 202 {id, status, cost}, дальше статус, SSE и push без изменений.

POST /v1/studio/generations/estimate - цена в кредитах до запуска, считается тем же движком, что и списание. Зависит и от настроек, и от числа фотографий (photos).

expected_cost в запуске (необязательный) - цена, которую вы показали пользователю. Если к моменту запуска она выросла, придёт 422 с новой ценой и без списания; если упала - запуск идёт по меньшей. Не прислали - спишется актуальная.

Результат студийного запуска приватный: он не появляется в каталоге и виден только автору, в обычной истории генераций. Публикация в каталог - по-прежнему заявкой автора.

Гостю студию видно, но запускать нельзя. GET /v1/studio/categories и оценка отвечают и гостевому аккаунту - экран студии можно показать целиком до регистрации, вместе с ценой. POST /v1/studio/generations гостю отвечает 403 registration_required: ведите на регистрацию, обновлять токен бесполезно. После регистрации аккаунт тот же, заполненная форма и кредиты остаются.

Чего пока нет: задачи, которой не нужна ни одна фотография («создать картинку с нуля по описанию»). Путь генерации стартует от загрузки пользователя, и такая форма ответит 422. Планируете такой экран - напишите нам заранее.

2026-09-21 - автор сам убирает свой шаблон из каталога

Отменяет ограничение из записи ниже: там снимать опубликованный шаблон мог только оператор. Ломающих изменений нет, ручка новая.

POST /v1/user-effects/{id}/unpublish - зеркало …/publish. Переводит свой опубликованный шаблон в private: из каталога он пропадает, у автора остаётся - тот им генерирует и может отправить на публикацию снова. Уже идущие генерации не прерываются.

  • 422 - шаблон и так не в каталоге; кнопку показывайте только на status: "approved".
  • 404 - чужая заявка, как и у остальных авторских действий.

Почему передумали: «карточкой уже пользуются люди» верно для эффекта, который завели мы, и неверно для авторского. Человек сделал работу - ему и решать, висеть ей в витрине или нет; в любом маркетплейсе продавец снимает свой товар сам.

Что осталось за оператором: снятие ЧУЖОГО шаблона - по жалобе и у кураторских эффектов, где автора нет. Различить можно по push'у: своё снятие приходит только стримом, операторское - ещё и push'ом «Эффект убран из каталога».

Подробности: Свои эффекты.

2026-09-21 - свой шаблон можно оставить себе

Новое поле запроса необязательно, и POST /v1/user-effects без него ведёт себя ровно как раньше - заявка уходит в модерацию. Но в поле status появилось новое ЗНАЧЕНИЕ, и оно терминальное. Работа тут есть у всех, а не только у тех, кто возьмёт новое поле: новый статус прилетит и клиенту, который ничего не менял, - достаточно, чтобы оператор снял с витрины уже опубликованный шаблон его автора.

publish в POST /v1/user-effects (по умолчанию true). Передайте false, если человек делает шаблон для себя: система так же соберёт и оттестирует его, но на модерацию он не уйдёт. В каталоге такого шаблона нет - ни списком, ни по прямой ссылке, GET /v1/effects/{id} отдаёт 404 даже автору, - а сам автор им генерирует как обычно.

Новый статус private - шаблон вне каталога, у автора. Приходит в двух разных ситуациях, и это важно различать:

  • собран и оставлен себе (заявка с publish: false) - только стримом, push'а нет: это конец сборки, а не решение по заявке;
  • оператор вернул опубликованный шаблон автору - и стримом, и push'ом «Эффект убран из каталога». Этот случай приходит без участия клиента: он мог никогда не присылать publish: false.

Что сделать в приложении: обработать private как терминальный и закрывать экран ожидания по событию стрима, а не по приходу push'а - в первом случае push'а не будет вовсе, и экран, который его ждёт, зависнет навсегда (предупреждение целиком - в Свои эффекты). Приватные шаблоны не занимают max_pending: pending_now в /config считает только то, что реально ждёт модератора.

Новое поле visibility рядом со status - private, moderation или published. Отвечает на вопрос «кому эффект виден», без вердикта модерации. Полезное следствие: отклонённая заявка приходит как status: "rejected", visibility: "private", и запускать её всё равно нельзя - гейтите кнопку «попробовать» по status, а не по visibility.

POST /v1/user-effects/{id}/publish - отправить свой приватный шаблон в каталог тем же путём, что и обычную заявку. Кнопку показывайте только на status: "private": на строящемся, уже отправленном, опубликованном или отклонённом ответ 422.

Обратной ручки в этой версии нет: снять опубликованный эффект может только оператор. Это решение отменено в тот же день - см. запись «автор сам убирает свой шаблон из каталога» выше. Автору по-прежнему доступен DELETE (архив).

Подробности: Свои эффекты.

2026-09-21 - скидочное предложение после отказа от paywall

Новый раздел /v1/offers. Ломающих изменений нет, но одно изменение поведения витрины описано в конце записи.

POST /v1/offers/paywall-dismissed - клиент сообщает, что пользователь закрыл основной paywall. В ответ приходит предложение со скидкой или null (предложения нет - показывайте обычный paywall, это не ошибка):

{ "id": "…", "code": "first_paywall_discount", "expires_in": 600, "is_dismissed": false,
  "product": { "id": "…", "title": "30 дней", "price_minor": 29900, "credits": 1000 } }

Окно считает сервер. expires_in приходит числом секунд, а не временем истечения, так что часы устройства ни на что не влияют. Повторный вызов при живом предложении возвращает то же самое предложение с уже уменьшившимся остатком: перезапуск приложения окно не продлевает.

GET /v1/offers/current - то же предложение без побочных эффектов, для баннера и возврата на экран. POST /v1/offers/{id}/dismiss отмечает, что экран закрыт, и отвечает 204; скидка при этом НЕ сгорает - до конца окна она остаётся в /current с is_dismissed: true и её по-прежнему можно оплатить.

Оплата - обычным POST /v1/payments с product_id из product.id, отдельной ручки нет. После истечения окна (или если предложение уже оплачено, или не выдавалось этому пользователю) тот же запрос отвечает 403 forbidden.

Гостям предложения не выдаются: обе ручки отвечают 403 registration_required.

Изменение поведения: товары, которые продаются только по предложению, больше не попадают в GET /v1/payments/products и GET /v1/subscriptions/plans. Витрина показывает обычный прайс; скидочная позиция приходит только внутри самого предложения.

Подробности и требование показывать цену продления на экране: Спецпредложения.

2026-09-21 - гостевой аккаунт до регистрации

Пользователь генерирует, не заводя аккаунт. Ломающих изменений нет; в user появилось одно новое поле.

POST /v1/auth/anonymous принимает обязательный device_id и необязательный ref_code, отвечает 200 и обычным TokenResponse:

{ "device_id": "<стабильный id устройства>", "ref_code": "…" }

Первый вызов создаёт гостя и начисляет стартовые кредиты (сейчас 100 - бери число из user.balance, не зашивай). Повтор с тем же device_id возвращает тот же аккаунт с тем же балансом и новую пару токенов. Если устройство уже принадлежит полноценному аккаунту - 403 forbidden. Стартовые кредиты даются один раз на устройство за всю его жизнь: переустановка приложения и DELETE /v1/me их не возвращают.

device_id в этом потоке - ключ от аккаунта с чужими фотографиями. Он должен быть случайным идентификатором, который приложение сгенерировало само и держит в Keychain/Keystore.

В user появилось поле is_anonymous (и в /v1/me, и в TokenResponse): true у гостя, false у зарегистрированного. У гостя email и name пустые, providers - пустой список.

Апгрейд гостя. register, google, apple, vk и yandex теперь принимают необязательный Authorization с токеном гостя. Тогда новый аккаунт не создаётся: тот же user.id становится полноценным, баланс, генерации и загрузки остаются, приветственные кредиты начисляются сверх гостевых, реферальные бонусы (если гость приходил по коду) выплачиваются в этот момент. Занятая почта по-прежнему 409 email_taken, и гость при этом остаётся рабочим гостем. Вход в уже существующий аккаунт отдаёт токены того аккаунта, а баланс и работы гостя туда не переносятся - предупреди пользователя до входа.

Новый код ошибки registration_required (HTTP 403) - раздел требует настоящего аккаунта. Отвечают им платежи и подписки (кроме витрин), весь /v1/user-effects и POST /v1/auth/change-password. Витрины GET /v1/payments/products и GET /v1/subscriptions/plans гостю открыты: цены нужно видеть до того, как захочется купить. Важно: не заводи этот код в обработчик 401 → refresh → повтор. Токен гостя валиден, обновление вернёт такую же пару, и клиент зациклится.

Брошенный гость (нет входов больше 30 дней) удаляется вместе со своими фотографиями. Отсчёт от последнего входа, так что активный гость не удаляется. Подробности: Аутентификация.

2026-09-17 - вход через VK ID и Яндекс ID

Два новых способа входа. Одно ломающее изменение: смена пароля на аккаунте без почты (в конце записи).

POST /v1/auth/vk принимает access_token от VK ID SDK. Один роут на все три кнопки VK ID: ВКонтакте, Одноклассники, Mail. Какой кнопкой вошёл пользователь, передаётся в необязательном oauth_provider (vk / ok / mail, по умолчанию vk):

{ "access_token": "<vk_access_token>", "oauth_provider": "ok", "device_id": "…", "ref_code": "…" }

POST /v1/auth/yandex принимает access_token от Яндекс LoginSDK:

{ "access_token": "<yandex_oauth_token>", "device_id": "…", "ref_code": "…" }

Оба отвечают как google и apple: 200 и TokenResponse, в том числе при создании аккаунта. Плохой токен (включая токен Яндекса, выданный другому приложению) - 401 invalid_credentials. Провайдер не настроен на сервере или не ответил - 503 provider_unavailable. Пустой или недопустимый по формату access_token (не печатные ASCII-символы, пробелы, длиннее 4096) или неизвестный oauth_provider - 422. Пока на сервере не заданы id приложений VK и Яндекса, оба роута отвечают 503.

Привязка к существующему аккаунту по почте: Яндекс привязывается, как Google. VK ID пока нет: VK не сообщает, подтверждена ли почта, поэтому вход через VK создаёт отдельный аккаунт без почты. Подробности: Аутентификация.

В user появилось поле providers (и в /v1/me, и в TokenResponse): способы входа в аккаунт. Значения email, google, apple, vk, ok, mail, yandex, email первым, если в аккаунт можно войти по почте и паролю:

"providers": ["email", "yandex"]

⚠️ POST /v1/auth/change-password отвечает 403 forbidden аккаунту без почты (email: null), например созданному входом через VK ID. Вход по паролю ищет аккаунт по почте, так что заданный без неё пароль никуда не пускал. Отказ ничего не меняет: пароль не ставится, сессии не гасятся. Показывай «задать пароль» только при непустом user.email.

2026-09-07 — кредит стал одним числом

Раньше баланс делился на три «бакета» по происхождению кредитов (paid / bonus / earned), и одно списание могло превратиться в несколько записей журнала. Деление обслуживало единственное правило (автору эффекта капает процент только с оплаченных генераций), а правило теперь проверяется по истории платежей плательщика. Кошелёк снова одно число.

⚠️ GET /v1/credits/balance больше не отдаёт paid, bonus, earned:

{ "balance": 1120 }

⚠️ GET /v1/credits/transactions: у записи больше нет поля bucket. Заодно одна операция теперь всегда одна запись: списание под генерацию это одна строка reserve, возврат одна строка refund с тем же reference. Прежняя рекомендация группировать историю по reference больше не нужна, хотя ничего и не ломает.

⚠️ GET /v1/user-effects/account больше не отдаёт available (сколько заработанного ещё лежит на балансе). Такого числа больше нет: кошелёк не помнит, откуда пришёл каждый кредит. Текущий остаток берётся из общего баланса, начислено и отозвано за всё время остаётся в earned и revoked.

Значения kind в журнале не изменились. Подробности: Кредиты и Свои эффекты.

Лимиты длины на полях запросов

Поля, у которых раньше не было верхней границы, теперь её имеют. Слишком длинное значение отвечает 422, а не 500:

Поле Где Лимит
device_id register, login, oauth, refresh, change-password 255
name register 120
ref_code register, oauth 32
category GET /v1/effects (query) 64

Реальные значения (UUID устройства, имя пользователя, реферальный код из 8 символов) проходят с запасом, менять в клиенте нечего.

Заметное поведение

  • Аватар пересжимается на сервере. Загруженный файл перекодируется в JPEG со срезанными EXIF (в том числе геометкой съёмки): по тому же ключу объекта лежит уже наш файл, а не оригинал с телефона.
  • Отклонённая оплата занимает попытку. У пакетов с ограничением «один на аккаунт» неудачная попытка теперь учитывается наравне с созданной: плательщик может вернуться на страницу ПЦ и оплатить её же, поэтому вторая параллельная покупка того же пакета не разрешается.
  • Подписка не залипает в pending. Если первый платёж по подписке так и не был оплачен и его заказ уже протух, слот плана освобождается, и повторный POST /v1/subscriptions больше не отвечает 409 already_subscribed.
  • Отказ процессинга завершает подписку честно. Документированный отказ ПЦ (рекуррентные платежи или токены не включены, токен карты умер) обрабатывается как неуспешное списание: работает лестница повторов, и подписка в итоге истекает, а не остаётся active без единого списания.