Как выдавать API-ключи капчи вашим клиентам

Если вы держите CapSkip для других людей, API-ключи капчи можно выдавать прямо в момент оплаты, из вашего же биллингового webhook. Remote Key Management открывает POST-эндпоинты, которые добавляют, перечисляют, удаляют, ограничивают и считают ключи на работающем экземпляре, а новый ключ действует уже со следующего запроса на решение капчи. Ни перезапуска, ни ручного шага: между прошедшим платежом и клиентом, который начал решать капчи, больше ничего не стоит. Каждый ключ вдобавок несёт собственный лимит кредитов по каждому типу капчи, так что тарифный уровень превращается в число, которое считает сам решатель, а не в правило, за которым должен следить ваш биллинг. В статье разбираем, как выдать ключ при подписке, как задавать и пополнять эти лимиты, как считать расход по каждому клиенту, как отзывать ключ при отмене и какую сетевую ошибку такая схема провоцирует.
Что понадобится
- CapSkip работает на машине с Windows, которой распоряжаетесь вы. Эта машина и есть ваш сервис распознавания, и она единственная, где приложение вообще нужно установить. Вашим клиентам ставить не нужно ничего.
- Режим Server, чтобы клиенты действительно могли до неё достучаться. Режим Local слушает петлевой адрес и обслуживает только само устройство, а режим Server слушает ваш внутренний или публичный IP, так что подключиться могут и другие машины. Оба режима описаны в разделе Настройки подключения, и прежде чем давать кому-то адрес, стоит обзавестись статическим публичным IP.
- Один заход в окно CapSkip, чтобы сгенерировать админский token. Это единственный шаг, который происходит в интерфейсе, и повторять его не придётся.
- Место, откуда вы будете делать эти вызовы: обработчик биллингового webhook, бэкенд вашей панели или терминал, пока вы всё это проверяете.
Прежде чем перейти к коду, стоит сказать прямо, потому что именно на этом вся схема и держится. Ключи, которые вы раздаёте, вы чеканите сами. Это не кредиты, купленные у поставщика и перепроданные дальше, и чеканка одного ключа не стоит вам ничего. CapSkip работает на вашем железе, поэтому сотня клиентских ключей стоит ровно столько же, сколько один.
Что ключ теперь может нести, так это собственный лимит кредитов, причём отдельный для каждого типа капчи. Из простой метки он превращается в тарифный уровень, который решатель соблюдает сам: «Старт, 10 000 решений reCAPTCHA» становится числом, которое CapSkip отсчитывает, а не тем, за чем приходится следить вам. Расчётных корзин восемь, и они намеренно крупнее, чем типы задач у самого решателя:
- Корзина ImageToText покрывает решение картинок и OCR.
- Корзина Recaptcha объединяет v2, v3 и оба варианта Enterprise.
- Корзина Turnstile покрывает Cloudflare Turnstile.
- Корзина GeeTest покрывает GeeTest.
- Корзина Altcha покрывает ALTCHA.
- Корзина CaptchaFox покрывает CaptchaFox.
- Корзина Capy покрывает Capy Puzzle.
- Корзина FriendlyCaptcha покрывает Friendly Captcha.
Корзина без заданного лимита безлимитна, и ровно так вёл себя каждый ключ до появления этой возможности. Поэтому само по себе обновление CapSkip не меняет ничего: каждый уже выданный ключ остаётся безлимитным во всех восьми корзинах, пока вы не зададите лимит, а вызов добавления, который про лимиты ничего не говорит, по-прежнему создаёт безлимитный ключ.
Шаг 1: включите Remote Key Management
Переключатель находится в Settings, в разделе API access, затем Remote Key Management. Включите его и нажмите кнопку Generate.
Token показывается один раз и хранится только в виде хеша с солью, поэтому прочитать его потом уже негде. Потеряли: генерируйте новый, старый перестанет работать немедленно. Чтобы отключить admin API, не выпуская новый token, нажмите Revoke: сохранённый token будет удалён. Держите token там же, где ваш биллинг держит остальные секреты, а не в коде приложения.
Эндпоинты живут на том же хосте и порту, который уже отвечает на ваши запросы распознавания. Если клиенты обращаются к порту 8080, там же находится и админский API. Это удобно, и ровно с этим же надо быть осторожным: об этом отдельный раздел ниже.
Пока функция выключена, любой админский путь отдаёт голый 404 без тела ответа. Ровно то же самое приходит в ответ на несуществующий путь, и сделано это намеренно: тот, кто сканирует порт, не отличит экземпляр с выключенной функцией от экземпляра, который о ней вообще не знает.
Шаг 2: выдайте ключ, когда клиент оформил подписку
Каждый эндпоинт работает по POST, принимает тело в JSON и требует заголовок Authorization с token в виде учётных данных Bearer. Вызову добавления нужно имя. Передайте только имя, а значение ключа сгенерирует CapSkip: здесь это именно то, что нужно, ведь клиент не должен выбирать себе учётные данные сам. Добавьте объект grants, и ключ появится сразу с тарифными лимитами этого клиента.
# No install needed. Name the key after the customer, not after the plan.
curl -X POST http://YOUR_SERVER:8080/admin/keys/add \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"cust_10482","grants":{"Recaptcha":10000,"ImageToText":50000}}'{
"errorId": 0,
"key": {
"name": "cust_10482",
"key": "a1b2c3d4e5f6...",
"grants": {
"ImageToText": 50000,
"Recaptcha": 10000,
"Turnstile": null,
"GeeTest": null, "Altcha": null, "CaptchaFox": null, "Capy": null, "FriendlyCaptcha": null
}
}
}В ответе перечислены все восемь корзин, а не только те две, которые вы задали, так что повторное чтение ради подтверждения вам не понадобится. Называйте ключ по чему-то устойчивому в вашей же системе: по идентификатору клиента или подписки. Имена уникальны, и правило проверяется при добавлении, поэтому имя становится надёжной ручкой для всех последующих операций. А вот названия по тарифу или по дате как раз и превращают день отмены в мучение.
Объект grants подчиняется четырём правилам, и спасает вас именно последнее:
- Не передавайте его вовсе, и ключ будет безлимитным везде. Это ровно прежнее поведение, поэтому интеграции, написанные до появления квот, продолжают работать нетронутыми.
- Пропустите одну корзину или передайте её как JSON null, и она будет безлимитной. Передайте ноль, и она, наоборот, стартует уже исчерпанной.
- Имена корзин нечувствительны к регистру, а вот credits должно быть целым числом JSON от нуля и выше, без кавычек: ни строкой в кавычках, ни дробным.
- Сначала проверяется весь запрос целиком, и только потом создаётся ключ. Неизвестное имя корзины или плохое число вернут 400 и не создадут ничего, поэтому отклонённый запрос физически не может оставить после себя наполовину ограниченный ключ.
Задавайте лимиты при создании, а не создавайте ключ, чтобы потом его ограничить. Второй путь оставляет окно, пусть и короткое, в котором совершенно новый клиентский ключ живёт вообще без лимитов.
Это же правило уникальности служит вам защитой от повторов. Платёжные сервисы пересылают webhook повторно, и повторное событие о подписке вернёт 409 и ERROR_KEY_EXISTS вместо того, чтобы тихо завести второй ключ тому же клиенту. Считайте такой 409 успехом в обработчике, и проблема повторов исчезнет.
Значение возвращается ровно один раз, в этом самом ответе. Покажите его клиенту или сохраните туда, откуда читает ваша панель: получение списка ключей потом относится к админским операциям, и запускать его ради восстановления одних учётных данных вам точно не захочется.
Шаг 3: меняйте лимиты при апгрейде или продлении
Эндпоинт grant записывает лимит одной корзины на одном ключе. Ключ выбирается по имени или по значению, ровно одним из двух, точно так же, как при удалении.
# Upgrade: replace the limit outright.
curl -X POST http://YOUR_SERVER:8080/admin/keys/grant \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"cust_10482","bucket":"Recaptcha","credits":25000,"mode":"set"}'
# Top-up: add to whatever is left.
# -d '{"name":"cust_10482","bucket":"Recaptcha","credits":10000,"mode":"add"}'Два режима несимметричны. Режим set заменяет число целиком, а значение credits, равное null или вовсе не переданное, в режиме set сбрасывает лимит обратно в безлимит. Это единственный способ снять лимит, когда он уже есть. Стоит заметить, что значение credits, равное нулю, означает противоположное null: оно исчерпывает корзину, а не снимает ограничение. Режим add складывает текущее значение с вашим числом, считая безлимит нулём, и потому устанавливает лимит там, где его не было. Он требует значение credits больше нуля.
Три формы запроса отклоняются, а не выполняются, и каждая по одной причине: иначе сбой остался бы незаметным.
- Прибавление нуля. Ночной скрипт пополнения, считающий разницу между целью и текущим значением, выдаёт ноль для каждого клиента, который уже на цели. Против безлимитной корзины это установило бы лимит ровно в ноль и остановило бы решение по этому ключу. Поэтому, когда вы имеете в виду ноль, берите режим set.
- Отрицательные числа, в любом режиме. Прибавление минус 500 читается как списание с ограниченной корзины, но тот же самый запрос против безлимитной опустится до нуля и тихо превратит её в исчерпанную. Вызывающая сторона не может по своему запросу предсказать, что получит, поэтому не разрешено ни то, ни другое.
- Числа в кавычках. Передача значения credits строкой в кавычках означает «ограничить этот ключ таким числом», но воспринятая как отсутствующее значение она означала бы ровно обратное, снятие лимита, и всё равно вернула бы успех. Кавычки вокруг чисел именно то, что случайно делает shell-скрипт, поэтому передавайте credits без них.
Одно нужно спланировать заранее: выдача представляет собой баланс кредитов, а не месячную норму. Внутри CapSkip ничто не живёт по календарю. Для помесячного тарифа с оплатой по расходу вызывайте эндпоинт grant в режиме set с нормой тарифа в начале каждого расчётного периода, из webhook продления. Для тарифа, где неиспользованное переносится, берите режим add с нормой периода.
Шаг 4: считайте расход каждого клиента
Эндпоинт usage принимает пустой объект и возвращает расход по каждому ключу и каждой корзине сразу для всех ключей.
# Every key, every bucket, in one call.
curl -X POST http://YOUR_SERVER:8080/admin/keys/usage \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" -d '{}'{
"errorId": 0,
"keys": [
{
"name": "cust_10482",
"key": "a1b2c3d4e5f6...",
"buckets": [
{
"bucket": "Recaptcha",
"granted": 10000,
"remaining": 9581,
"used": 419,
"failed": 12,
"inFlight": 3,
"lastUsed": "2026-08-26T09:14:02.1234567Z"
}
]
}
]
}| Поле | Значение |
|---|---|
| "granted" | Лимит, либо null, если корзина безлимитна. |
| "remaining" | Остаток кредитов, либо null, когда корзина безлимитна. |
| "used" | Успешные решения. Только они и тратят кредиты. |
| "failed" | Неудачные попытки: считаются для диагностики, но никогда не тарифицируются. |
| "inFlight" | Задачи, зарезервированные прямо сейчас. Из остатка они уже вычтены и возвращаются, если задача провалится. |
| "lastUsed" | Время последнего успешного решения в ISO-8601 UTC, либо null, если ключ ни разу не использовали. |
Счётчики накапливаются за всю жизнь ключа и переживают перезапуски. Один вызов возвращает все ключи сразу, поэтому опрашивайте его по расписанию и кормите им панель, а не ходите за каждым клиентом отдельно. Поле lastUsed заодно показывает, какие ключи простаивают и их стоит забрать обратно.
Шаг 5: отзовите ключ при отмене подписки
Удаление принимает ровно один признак: либо значение ключа, либо имя. Раз вы называли ключи по клиентам, берите имя:
# By name, which is unambiguous because names are unique.
curl -X POST http://YOUR_SERVER:8080/admin/keys/delete \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" -d '{"name":"cust_10482"}'
# By value works too, if that is what your records hold:
# -d '{"key":"the-old-value"}'Отзыв вступает в силу со следующего запроса распознавания, так что доступ пропадает в момент возврата вызова. С этой секунды вызовы клиента будут завершаться ошибкой ERROR_KEY_DOES_NOT_EXIST, и это достаточно внятный сигнал: их интеграция может показать его как истёкшую подписку, а не как аварию на вашей стороне.
Момент отзыва выбирайте осознанно. Удаление по событию отмены обрывает доступ сразу, хотя клиент обычно оплатил до конца периода. Удаление по событию окончания периода и есть то, чего в большинстве случаев хотят на самом деле. Неудачное списание представляет собой третий случай: небольшая отсрочка перед отзывом оборачивается куда меньшим числом обращений в поддержку, чем ключ, исчезнувший при повторной попытке списания.
Шаг 6: сверяйте список с вашей биллинговой системой
Вызов со списком принимает пустой объект и возвращает каждый ключ, который экземпляр принимает прямо сейчас.
# The audit: exactly who can solve right now.
curl -X POST http://YOUR_SERVER:8080/admin/keys/list \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" -d '{}'{
"errorId": 0,
"keys": [
{ "name": "cust_10482", "key": "..." },
{ "name": "cust_10515", "key": "..." }
]
}Значение errorId, равное 0, означает успех, и это та же конвенция, что и у эндпоинтов распознавания. Запускайте это по расписанию и сверяйте имена с вашими активными подписками. Ключ без подписки означает, что кто-то до сих пор решает капчи бесплатно, а подписка без ключа означает клиента, чей webhook на выдачу потерялся и который вот-вот напишет в поддержку. Обе ситуации молчат, пока вы не посмотрите, и этот вызов остаётся единственным местом, где живёт правда.
Что видит клиент, когда лимит исчерпан
Ключ, у которого в корзине не осталось кредитов, получает от эндпоинтов решения ERROR_ZERO_BALANCE, и только для задач этой корзины. Остальные корзины того же ключа продолжают работать. Ошибка приходит из API решения, а не из этого админского API, и выглядит одинаково во всех режимах совместимости: 2captcha, Anti-Captcha, CapSolver и DeathByCaptcha. Так что обработка исчерпанного баланса 2captcha, которая у клиента уже написана, покрывает и этот случай без единой правки с его стороны.
Проверка баланса тоже ведёт себя разумно. Клиент, опрашивающий getBalance или действие баланса в res.php, видит свой реальный остаток кредитов, просуммированный по всем ограниченным корзинам, поэтому число убывает по мере работы. Ключ, у которого нигде нет лимитов, по-прежнему отдаёт прежнее условное значение 999999.99, а значит клиенты, которые перед отправкой проверяют баланс на вменяемость, работают ровно как раньше.
Об одном слепом пятне стоит знать. В формате передачи есть место лишь под одно число, поэтому ключ с 50 000 кредитов на картинки и нулём на reCAPTCHA всё равно отдаёт вполне здоровый баланс. Всему, чему нужна разбивка по типам, придётся читать эндпоинт usage, а он доступен только админу.
Как встроить это в webhook подписки
Всё вместе укладывается в таблицу тарифов и четыре обработчика:
# pip install requests
import requests
BASE = "http://YOUR_SERVER:8080"
AUTH = {"Authorization": "Bearer YOUR_TOKEN"}
BUCKETS = ("ImageToText", "Recaptcha", "Turnstile", "GeeTest", "Altcha", "CaptchaFox", "Capy", "FriendlyCaptcha")
# None means unlimited; an omitted bucket is unlimited too.
PLANS = {
"starter": {"ImageToText": 50_000, "Recaptcha": 10_000},
"pro": {"ImageToText": 500_000, "Recaptcha": 100_000,
"Turnstile": 25_000},
"unlimited": {},
}
def admin(path, body):
r = requests.post(f"{BASE}/admin/keys/{path}", json=body,
headers=AUTH, timeout=10)
if r.status_code == 409:
return None # already exists: a retried webhook
r.raise_for_status()
return r.json()
def on_subscription_active(customer_id, plan):
body = {"name": f"cust_{customer_id}"}
if PLANS[plan]:
body["grants"] = PLANS[plan]
created = admin("add", body)
return created["key"]["key"] if created else None
def apply_plan(customer_id, plan):
"""Upgrade, downgrade, or start-of-cycle reset."""
limits = PLANS[plan]
for bucket in BUCKETS:
admin("grant", {"name": f"cust_{customer_id}", "bucket": bucket,
"mode": "set", "credits": limits.get(bucket)})
def on_subscription_ended(customer_id):
admin("delete", {"name": f"cust_{customer_id}"})
def usage_report():
return admin("usage", {})["keys"]Обратите внимание, что apply_plan записывает каждую корзину, а не правит только изменившиеся. Так задумано: при понижении тарифа он снимает лимиты, которые были у старого плана и которых нет у нового, ведь отсутствующее значение credits в режиме set означает безлимит. Для тарифа с переносом переключите его в режим add и пропускайте корзины с лимитом None, потому что режим add отвергает отсутствующее или нулевое значение.
К значению None от повторного события о подписке отнеситесь внимательно. Оно означает, что ключ есть, но его значение вам больше не увидеть, поэтому если в первый раз вы его не сохранили, восстанавливаться надо удалением и повторным созданием, а не получением списка. Сохраните значение сразу при первом получении, и ситуация просто не возникнет.
Не оставляйте админский API на порту, доступном клиентам
Для такой схемы это самый важный раздел, потому что удобство одного хоста и порта работает в обе стороны. Вашим клиентам нужен доступ к эндпоинтам распознавания. Админские эндпоинты находятся на том же порту, защищены только Bearer token, а проверки транспорта нет вовсе, так что на нестандартном порту этот token идёт открытым HTTP. Любой, кто способен наблюдать трафик, прочитает его, а любой, у кого он окажется, выпишет себе ключ.
Поставьте перед экземпляром обратный прокси и разведите две аудитории:
- В интернет публикуйте только пути распознавания, по HTTPS с терминацией на прокси, а на всё, что лежит под админским путём, отдавайте 404.
- К админским эндпоинтам ходите со своего бэкенда по закрытому маршруту: через петлевой адрес, если биллинг работает на той же машине с Windows, иначе через частную сеть, VPN или SSH-туннель.
Правило простое: админский token не должен покидать вашу инфраструктуру, а порт, с которым он разговаривает, не должен быть тем, до которого дотягивается клиент. Никогда не выставляйте админский путь в открытый интернет, независимо от того, включена функция или нет.
Стоит знать ещё одну границу, ведь клиентская панель как раз и есть первое, что здесь хочется построить. Браузеры вызвать эти эндпоинты не смогут, потому что админские ответы по замыслу не несут заголовков CORS . Ваша панель обязана ходить через ваш собственный бэкенд, где token и должен лежать.
Куда именно записываются ключи
Ключи попадают туда, где ваши ключи уже лежат. Direct Input и From File представляют собой два отдельных списка, и API читает и пишет тот из них, который сейчас выбран. Переключение источника в окне настроек меняет и то, что видят эндпоинты, поэтому убедитесь, в каком вы режиме, прежде чем недоумевать, почему только что выданного ключа нет в списке.
У файлового режима есть одна особенность, о которой лучше знать заранее: первая же запись переписывает текстовый файл ключей в формате JSON. Ничего не теряется, но формат меняется навсегда, поэтому сделайте резервную копию, если файл читает что-то ещё. А если файл вообще не выбран, записи некуда лечь, и вы получите ERROR_ADMIN_STORE_FAILURE с кодом 500. Практически всегда 500 здесь означает именно это.
Списки, созданные до появления этой функции, всё ещё могут содержать повторяющиеся имена, потому что уникальность проверяется только при добавлении. Удаление по имени в таком случае вернёт ERROR_KEY_NAME_AMBIGUOUS и ничего не изменит. Удалите по значению, и неоднозначность исчезнет.
Кавычки в оболочках Windows, если вы проверяете прямо на этой машине
CapSkip представляет собой приложение для Windows, поэтому машина, с которой вы всё проверяете, тоже обычно под Windows. Командная строка не считает одинарную кавычку кавычкой. Скопированная команда отправляет сами кавычки как часть тела JSON, парсер их отвергает, и вы получаете ERROR_ADMIN_BAD_REQUEST на команде, которая выглядит совершенно правильной.
| Оболочка | Пустое тело | С полями |
|---|---|---|
| Командная строка | -d "{}" | -d "{\"name\":\"prod\"}" |
| PowerShell, через curl.exe | -d '{}' | -d '{\"name\":\"prod\"}' |
| Git Bash, macOS, Linux | -d '{}' | -d '{"name":"prod"}' |
В PowerShell вызывайте curl.exe полным именем. Обычный curl там представляет собой псевдоним для Invoke-WebRequest, у которого совсем другие аргументы, и ошибка будет никак не связана с этим API.
Коды ошибок
| Статус | Код | Причина |
|---|---|---|
| 404 | простой текст | Функция выключена либо путь неизвестен. Специально сделано неразличимым. |
| 401 | ERROR_ADMIN_UNAUTHORIZED | Token отсутствует, повреждён или неверен. |
| 405 | ERROR_ADMIN_METHOD_NOT_ALLOWED | Отправлен не POST. |
| 400 | ERROR_ADMIN_BAD_REQUEST | Некорректное тело: добавление без имени, удаление с обоими признаками или без единого, неизвестное имя корзины, значение credits или grants, не являющееся целым неотрицательным числом без кавычек, либо выдача в режиме add без значения credits больше нуля. Ничего не создаётся и не изменяется. |
| 409 | ERROR_KEY_EXISTS | Такое имя или такое значение уже используется. В обработчике подписки это обычно повторно присланный webhook. |
| 409 | ERROR_KEY_NAME_AMBIGUOUS | Удаление или выдача по имени совпали с несколькими ключами. Выбирайте по значению ключа. |
| 404 | ERROR_KEY_NOT_FOUND | Удаление или выдача ни с чем не совпали. Обычно это уже обработанная отмена. |
| 500 | ERROR_ADMIN_STORE_FAILURE | Не удалось сохранить. Обычно это файловый режим без выбранного файла. |
Ещё один код уместен здесь, хотя приходит он с другой стороны продукта. ERROR_ZERO_BALANCE возвращают эндпоинты решения, а не этот админский API, и происходит это, когда у ключа кончились кредиты в той корзине, к которой относится задача. Если клиент сообщил о нём, ответом будет пополнение, а не поломка в выдаче ключей.
Эти коды соседствуют с кодами ошибок распознавания, а не заменяют их, и полный набор приведён в разделе Документация по API CapSkip.
FAQ
Как быстро новый ключ реально начинает работать?
Со следующего запроса распознавания. Ничего не кэшируется и ничего не требует перезагрузки, поэтому клиент, получивший ключ на странице оплаты, воспользуется им в ту же минуту. Отзыв в обратную сторону такой же мгновенный, и именно поэтому момент срабатывания вашего обработчика отмены остаётся вопросом политики, а не техники.
Стоит ли денег каждый следующий клиентский ключ?
По-прежнему ничего. Чеканка ключа бесплатна, и никакого счётчика поставщика за ней нет. Изменилось то, что ключ теперь несёт ваши собственные лимиты кредитов и потому может представлять тарифный уровень: учёт ваш, и ценообразование тоже ваше. Компонент SDK для распознавания капчи использует тот ключ, который вы ему передали, так что схему разделения вы придумываете сами.
Могу ли я через этот API ограничивать частоту запросов клиента?
Нет, и квоты этого не заменяют. Квота представляет собой бюджет, а не потолок запросов в секунду: она ограничивает, сколько клиент получит всего, а не то, насколько сильно он ударит по эндпоинту в отдельно взятую секунду. Ограничение частоты по-прежнему живёт на обратном прокси, который вы и так ставите перед экземпляром, и опирается на API-ключ, который прислал клиент.
Обнуляются ли лимиты каждый месяц?
Нет. Выдача представляет собой баланс, который убывает, пока вы его не измените, и внутри CapSkip ничто не живёт по календарю. Вызывайте эндпоинт grant в режиме set в начале каждого расчётного периода, чтобы вернуть норму тарифа, либо в режиме add, если неиспользованные кредиты должны переноситься.
Тратят ли кредиты неудачные решения?
Нет. Кредит списывается только за успешное решение. Неудачи считаются в отчёте о расходе отдельно, так что вы их всё равно видите, а задача, провалившаяся уже в работе, возвращает свой резерв. Значит, клиент, которого трудный сайт гоняет по кругу, не выжигает молча свой лимит на попытках, не вернувших ничего.
Что станет с моими ключами после обновления CapSkip?
Ничего. Каждый уже выданный ключ безлимитен во всех восьми корзинах, пока вы не зададите ему лимит, а вызовы добавления, которые про grants ничего не говорят, создают безлимитные ключи точно как раньше. Возможность бездействует, пока оператор сам её не включит, поэтому нет ни шага миграции, ни окна, в котором клиенты теряют доступ.
Могут ли клиенты видеть свой остаток кредитов?
Через проверку баланса могут. Как только лимиты заданы, она отдаёт их настоящий остаток, а не прежнее условное значение, поэтому число убывает по мере работы. Разбивка по корзинам совсем другое дело: ей нужен админский token, и она возвращает сразу все ключи, поэтому показывайте её из своей собственной панели, а не открывайте клиентам сам эндпоинт.
Я потерял админский token. Что теперь?
Сгенерируйте новый в той же панели настроек. Старый token перестаёт работать немедленно, поэтому никакой уборки не требуется и нет окна, в котором действуют оба. Клиентов это не затрагивает: их ключи остаются нетронутыми, и они продолжают решать капчи. Ломается только ваша собственная выдача ключей, пока вы не обновите секрет, который читает обработчик webhook.
Самая короткая версия
Remote Key Management включается один раз, а token храните вместе с остальными биллинговыми секретами. Когда подписка становится активной, добавляйте ключ с именем клиента и сразу с grants его тарифа, а повторный 409 считайте успехом. Сброс и пополнение лимитов делайте из webhook продления, ведь ничто здесь не живёт по календарю. Опрашивайте эндпоинт usage по расписанию: он нужен и для сверки счетов, и для поиска простаивающих ключей. По окончании подписки удаляйте по имени и сверяйте список ключей с активными подписчиками, потому что только там живёт настоящий ответ. Затем поставьте впереди прокси и уберите админский путь с порта, который видят клиенты. Сделайте это, и обход капчи превратится в то, что вы можете перепродавать на собственном железе, считая по своим собственным числам, а скорость выдачи будет равна скорости, с которой ваша касса отправляет webhook.
