Перейти к основному содержимому

API действий с лицензией клиента

Management API предоставляет customer-scoped discovery и durable-выпуск из Subscription, revoke, archive и ротацию ключа для Vendor Console. Каждый route требует customers.read и licenses.manage; Tenant и actor берутся только из authenticated context пользователя или service account.

Discovery и выпуск

GET /api/v1/customers/organizations/{organizationId}/license-issue-candidates?offset=0&limit=50

Discovery возвращает только Trialing или Active Subscriptions, привязанные к точной Published или Retired OfferVersion, принадлежащие path Customer, пока effectiveIssuable остаётся положительным (min остатка SeatLimit тарифа и общего пула). Отсутствие SeatLimit или seat pool — fail closed (не unlimited). Customer должен быть Active. Порядок фиксирован: время создания Subscription по убыванию, затем Subscription ID по возрастанию. offset по умолчанию равен 0; limit по умолчанию равен 50 и принимает 1..200.

Ответ содержит display-поля Product и тарифа, а также immutable identity Subscription/Offer. В нём нет policy references, key material, hashes, provider references и внутренних codes. Discovery advisory: issue повторно читает всех owners и working-seat remaining.

POST /api/v1/customers/organizations/{organizationId}/license-issues
Idempotency-Key: customer-generated-value
Content-Type: application/json

{
"customerSubscriptionId": "0198f35d-7b00-7000-8000-000000000001",
"correlationId": "0198f35d-7b00-7000-8000-000000000002"
}

Product, Customer, OfferVersion, policy, validity, License code и plaintext key не принимаются из request. Первый committed issue возвращает 201 и существующий receipt выпуска из Subscription, включая format-v2 plaintext key. Точный durable replay возвращает 200, те же references и mask, но plaintextKey: null. Повторное использование ключа для другого semantic request даёт conflict. Revoked и Archived Licenses пожизненно занимают исходную Subscription.

Терминальные действия

POST /api/v1/customers/organizations/{organizationId}/licenses/{licenseId}/revoke
POST /api/v1/customers/organizations/{organizationId}/licenses/{licenseId}/archive
Content-Type: application/json

{
"expectedStatus": "Active",
"reasonCode": "customer_request",
"reasonDetail": "optional bounded safe detail"
}

JSON-объект строгий: разрешены только expectedStatus, reasonCode и необязательный reasonDetail; неизвестные и повторяющиеся поля отклоняются. expectedStatus чувствителен к регистру и защищает от stale UI state. Эти действия не используют Idempotency-Key и не принимают в body Tenant, Customer, Product, policy, operation или actor identity.

ДействиеРазрешённый исходный statusИтоговый status
RevokeDraft, Active или SuspendedRevoked
ArchiveDraft или RevokedArchived

Success возвращает 200 с operationId, идентичным effectId, licenseId, action, fromStatus, toStatus, completedAtUtc и isIdempotentReplay. Точный replay возвращает исходный receipt с isIdempotentReplay: true; изменённая или stale-попытка даёт conflict. Path Customer должен существовать и точно совпадать с Customer, сохранённым в License.

Переход License, audit evidence и сообщение integration outbox фиксируются одной durable-транзакцией до ответа. После revoke выполняются только best-effort cache hint/invalidation; archive только инвалидирует cached assists. Сбой cache не отменяет committed action, а PostgreSQL остаётся authoritative.

Существующие глобальные single и bulk revoke routes сохраняют request и response envelopes. Их legacy-поле body revokedByActorId deprecated, принимается и игнорируется; всегда используется authenticated Management actor. Повторный global single revoke по-прежнему даёт HTTP 409, а уже применённый bulk item остаётся успешным с alreadyApplied: true.

Durable-ротация ключа

POST /api/v1/customers/organizations/{organizationId}/licenses/{licenseId}/key/rotations
Idempotency-Key: customer-generated-value
Content-Type: application/json

{
"expectedPrimaryMaterialRefId": "0198f35d-7b00-7000-8000-000000000001"
}

Строгий body принимает только reference текущего primary material как stale-write precondition. Header обязан содержать ровно один ограниченный Idempotency-Key. Authority Tenant, Customer, License и actor никогда не берётся из body. Для нового effect path License должна принадлежать точному Active Customer; затем BC-LIC внутри атомарной ротации повторно проверяет state License, migration state, текущий primary и immutable policy snapshot.

Success возвращает 200 с operationId, licenseId, keyMaterialRefId, maskedKey, formatVersion, createdAtUtc, plaintextKey и isIdempotentReplay. Первый полностью успешный вызов может один раз раскрыть новый v2 plaintext. Точный retry возвращает durable references и mask с plaintextKey: null и isIdempotentReplay: true; повторное использование ключа с изменённым содержимым даёт conflict.

Операция BC-LIC фиксируется до идемпотентного append audit-события license.key.rotated, но HTTP success отправляется только после durable-записи обоих результатов. Если audit или доставка ответа падает после commit ротации, повторите идентичные header и body: второй ключ не создаётся, недостающий audit завершается, а replay всё равно возвращает null plaintext. Изменение status после уже committed effect не блокирует такой recovery replay. Каждый ответ handler помечен no-store и no-cache; plaintext никогда не сохраняется, не попадает в audit и не логируется.

Существующий global route POST /api/v1/licenses/{licenseId}/key/rotate сохраняет v1 success envelope и требует только licenses.manage. Reliable mode использует тот же expected-primary body вместе с Idempotency-Key. Отсутствие обоих включает deprecated headerless compatibility mode: он остаётся внутренне атомарным, но не допускает надёжного network retry и возвращает headers Deprecation и Warning. Передача только header или только body невалидна. Новые интеграции обязаны использовать reliable mode; Customer route никогда не переходит в compatibility mode.

Ошибки

Все ошибки используют platform error body и correlation ID.

HTTPCodeЗначение
400customer_license_actions.invalid_requestОшибка path, paging, header или strict body
404customer_license_actions.not_foundОтсутствующий, foreign или wrong-customer source
409customer_license_actions.customer_not_activeCustomer не Active
409customer_license_actions.not_issuableCommercial или policy source больше не разрешает выпуск
409customer_license_actions.subscription_occupiedSubscription уже занята License
409customer_license_actions.stale_preconditionOwner evidence изменился
409customer_license_actions.transition_not_allowedTerminal transition запрещён из текущего state
409customer_license_actions.idempotency_conflictКлюч повторно использован с другим содержимым
503customer_license_actions.dependency_unavailableНедоступен обязательный owner, audit или infrastructure
500customer_license_actions.stored_result_invalidDurable evidence issue или rotation нарушает контракт

Ошибки и replay никогда не раскрывают plaintext key material. Существующий глобальный route POST /api/v1/licenses/subscription-issues остаётся совместим.