license-service — API v1
OpenAPI-подобное описание. Базовые URL: облако —
https://license.meridian-office.ru(регионru),https://license.meridian-office.eu(регионeu); on-prem — любой (issuerв лицензии). Контекст —docs/10-licensing-accounts.md(разделы 7, 10). Схемы.mlic—license-file.schema.json, ключа —license-key-format.md.
1. Общие соглашения
| Аспект | Правило |
|---|---|
| Транспорт | HTTPS, TLS 1.3, HTTP/2; тело —
application/json; charset=utf-8; бинарные
.mlic/.mreq — base64 в JSON
("base64:…" или без префикса) либо
application/cbor там, где указано |
| Версия | префикс /v1; несовместимые изменения —
/v2, параллельная поддержка ≥ 24 мес |
| Идентификация клиента | обязательный
X-Meridian-Client: <app>/<version> (<os> <os_version>; <arch>; build <build>);
app ∈
hub, write, sheets, …, cli, portal, admin |
| Идемпотентность | все POST принимают Idempotency-Key: <uuid>; ответ
кэшируется 24 ч по (ключ, субъект, SHA-256 тела); другое тело →
409 IDEMPOTENCY_CONFLICT |
| Аутентификация | (а) Bearer — access JWT Meridian ID
(Authorization: Bearer …); (б) подпись
устройства — X-Meridian-Device-Id: dev_…,
X-Meridian-Device-Ts: <unix>,
X-Meridian-Device-Proof: base64(Ed25519.sign(device_sk, "MRDN-REQ-v1" ‖ method ‖ path ‖ ts ‖ SHA-256(body)));
ts ±5 мин; (в) api_key —
Authorization: Bearer mok_live_… (SCIM/консоль/интеграции);
(г) mTLS — внутренние вызовы |
| Лимиты частоты | token bucket; заголовки RateLimit-Limit,
RateLimit-Remaining, RateLimit-Reset, при 429
— Retry-After |
| Время | все даты — RFC 3339 UTC (Z); каждый ответ содержит
server_time (клиент обновляет якорь часов) |
| Ошибки | {"error":{"code":"…","message":"…","details":{…},"retry_after":0,"request_id":"req_…"}};
message — на языке Accept-Language (ru/en), не
для парсинга |
| Пагинация | ?cursor=&limit= (≤ 200), ответ
{"items":[…],"next_cursor":"…"} |
| Локализация | Accept-Language влияет только на
message |
2. Общие объекты
// Device — описание устройства (клиент → сервер)
{ "fingerprint_hash": "<64 hex>", "components": ["<64 hex>", "<64 hex>", "<64 hex>"],
"components_meta": ["os_id", "firmware_uuid", "disk_serial"],
"name": "IVAN-DESKTOP", "os": "windows", "os_version": "11.0.26100", "arch": "x86_64", "virtual": false,
"weak_fingerprint": false, "user_scope": null, "device_pubkey": "base64:<32 bytes>", "instance_id": "ins_<ULID>" }
// Client — описание приложения
{ "app": "hub", "version": "1.2.0", "build": "1.2.0.418", "locale": "ru-RU", "supported_license_formats": [1], "channel": "stable" }
// LicenseResult — результат активации/перевыпуска
{ "license_id": "lic_…", "license": "base64:<.mlic CBOR>", "license_json": { "…": "payload как в license-file.schema.json" },
"seats_used": 1, "seats_total": 3, "reused": false, "transferred_from": null,
"server_time": "2027-03-16T09:12:44Z", "crl_seq": 1042, "next_validation_at": "2027-03-23T09:12:44Z" }
// DeviceInfo — устройство в списках (без хэшей)
{ "device_id": "dev_…", "name": "IVAN-OLD", "os": "windows", "last_seen_at": "2027-02-01T08:00:00Z", "activated_at": "…", "this_device": false }
// Entitlement — право
{ "entitlement_id": "ent_…", "edition": "pro", "kind": "subscription", "seats": 5, "seats_used": 2, "holder": { "account_id": "usr_…", "display": "Иван Петров" },
"valid_from": "…", "expires_at": "…", "status": "active", "source": { "type": "order", "id": "ord_…" }, "devices": [ "DeviceInfo…" ],
"key_fingerprint": null, "update_until": null, "org": null }3. Эндпоинты: клиентские
3.1
POST /v1/activate — активация ключом
Auth: нет (ключи
perpetual/trial/partner/volume),
Bearer — если ключ требует аккаунт (subscription,
Business/Education). Лимит: 10/мин на IP, 30/сут на
fingerprint_hash.
POST /v1/activate HTTP/1.1
Host: license.meridian-office.ru
Content-Type: application/json
Idempotency-Key: 6f1c3b2e-2a7b-4e1a-9d3c-5b2f1a9e8c7d
X-Meridian-Client: hub/1.2.0 (windows 11.0.26100; x86_64; build 1.2.0.418)
Accept-Language: ru
{ "key": "MRDN-28800-C0G3N-EQW3V-00JBK-T2E7W", "device": { "…": "Device" }, "client": { "…": "Client" }, "org_hint": null }
HTTP/1.1 200 OK
Content-Type: application/json
{ "license_id": "lic_01J9ZM7Q0P1R2S3T4V5W6X7Y8Z", "license": "base64:o2F2AWdwYXlsb2Fk…", "license_json": { "…": "…" },
"seats_used": 1, "seats_total": 3, "reused": false, "transferred_from": null,
"server_time": "2027-03-16T09:12:44Z", "crl_seq": 1042, "next_validation_at": "2027-04-15T09:12:44Z" }
Ошибки:
400 KEY_INVALID_FORMAT | KEY_CHECKSUM | VALIDATION_ERROR,
404 KEY_NOT_FOUND,
409 ACCOUNT_REQUIRED {login_url},
409 KEY_ALREADY_BOUND,
409 KEY_EXHAUSTED {devices:[DeviceInfo], can_release: bool},
409 TRIAL_ALREADY_USED,
403 KEY_REGION_MISMATCH {region},
403 ORG_POLICY_DENIED {policy_rule},
410 KEY_REVOKED {reason, support_url},
410 KEY_EXPIRED {expired_at, renew_url},
426 CLIENT_TOO_OLD {min_client_version},
429 RATE_LIMITED, 503 SIGNING_UNAVAILABLE.
Семантика: идемпотентна по (key,
fingerprint_hash): повтор → та же активация
(reused: true), лицензия перевыпускается. Совпадение
устройства ≥ 2 компонентов с ранее активированным — переиспользование
места. При seats_used == seats_total и доступном
автопереносе (устройство без контакта > 30 дн., лимит 1/30 дн. на
право) — transferred_from.
3.2
POST /v1/validate — периодическая проверка
Auth: подпись устройства (ключ из активации). Лимит: 12/сут на
license_id (клиент — раз в 7 дней; чаще — при открытом
портале апгрейда).
// запрос
{ "license_id": "lic_01J9ZK4M2XQ8R7S6T5V4W3Y2Z1", "device": { "…": "Device" }, "client": { "…": "Client" },
"state": { "license_sha256": "<64 hex>", "crl_seq": 1040, "policy_version": 3, "last_validation_at": "2027-05-01T10:00:00Z" } }// ответ 200 — варианты поля status
{ "status": "ok", "server_time": "2027-05-08T10:00:03Z", "license": null, "crl_seq": 1042, "next_validation_at": "2027-05-15T10:00:03Z" }
{ "status": "ok", "server_time": "…", "license": "base64:…", "license_json": { "…": "…" }, "reason": "policy_updated|renewed|edition_changed|rebind", "crl_seq": 1042, "next_validation_at": "…" }
{ "status": "past_due", "server_time": "…", "pay_url": "https://account.meridian-office.ru/billing/pay?inv=inv_…", "grace_until": "2027-05-15T00:00:00Z", "next_validation_at": "2027-05-09T10:00:03Z" }
{ "status": "expired", "server_time": "…", "expired_at": "2027-05-01T00:00:00Z", "soft_until": "2027-05-15T10:00:03Z", "renew_url": "…" }
{ "status": "revoked", "server_time": "…", "reason": "transfer|leak|refund|chargeback|admin|fraud", "support_url": "…" }
{ "status": "reissued", "server_time": "…", "license": "base64:…", "new_key": "MRDN-…", "reason": "batch_revoked_honest_reissue" }
{ "status": "device_mismatch", "server_time": "…", "action": "reactivate" }Ошибки: 401 UNAUTHENTICATED (подпись устройства
неверна), 404 LICENSE_NOT_FOUND,
426 CLIENT_TOO_OLD (только предупреждение
warnings[], статус остаётся ok),
429 RATE_LIMITED.
3.3
POST /v1/deactivate — освобождение места
Auth: подпись устройства или Bearer (владелец права). Лимит: 10/сут на право.
{ "license_id": "lic_…", "device": { "fingerprint_hash": "…" }, "reason": "user|reinstall|lost|admin" }
→ 200 { "released": true, "seats_used": 2, "seats_total": 3, "server_time": "…" }Ошибки: 404 LICENSE_NOT_FOUND,
409 ALREADY_RELEASED (ответ 200 с
released: false при идемпотентном повторе),
429 TRANSFER_LIMIT {next_allowed_at}.
3.4
GET /v1/entitlements — права аккаунта и его
организаций
Auth: Bearer. Лимит: 60/мин.
→ 200 { "items": [ { "…": "Entitlement" } ], "server_time": "…" }Параметры: ?include=devices,org (по умолчанию устройства
включены), ?org_id= (только права организации, назначенные
пользователю).
3.5
POST /v1/entitlements/{entitlement_id}/activate — активация
входом
Auth: Bearer. Лимит: 10/мин.
{ "device": { "…": "Device" }, "client": { "…": "Client" } }
→ 200 { "…": "LicenseResult" }Ошибки: 404 ENTITLEMENT_NOT_FOUND,
409 DEVICE_LIMIT {devices:[DeviceInfo], can_release: true},
410 ENTITLEMENT_EXPIRED,
402 SUBSCRIPTION_PAST_DUE {pay_url, grace_until},
403 ORG_POLICY_DENIED, 403 SEAT_NOT_ASSIGNED
(Business: место пользователю не назначено).
3.6
POST /v1/trial/start — пробный период
Auth: нет. Лимит: 5/сут на IP, 1 на
fingerprint_hash.
{ "device": { "…": "Device" }, "client": { "…": "Client" }, "local_trial": { "started_at": "2027-04-20T08:00:00Z", "proof": "base64:…" } }
→ 200 { "…": "LicenseResult (edition trial→pro features, kind trial, expires_at = started_at + 30d)" }Ошибки:
409 TRIAL_ALREADY_USED {used_at, available_after},
403 ORG_POLICY_DENIED (устройство организации, trial
запрещён), 400 VALIDATION_ERROR
(local_trial.started_at старше 30 дней → trial выдаётся от
now).
3.7
POST /v1/transfer — перенос одним вызовом
Auth: Bearer. Лимит: 3 за 30 дней на право (личные), без лимита для организаций.
{ "entitlement_id": "ent_…", "from_device_id": "dev_…", "device": { "…": "Device (новое)" }, "client": { "…": "Client" } }
→ 200 { "…": "LicenseResult", "transferred_from": "IVAN-OLD" }Ошибки: 429 TRANSFER_LIMIT,
404 ENTITLEMENT_NOT_FOUND | DEVICE_NOT_FOUND.
3.8 GET /v1/crl —
список отзыва
Auth: нет. Лимит: 60/мин на IP. Поддерживает
If-None-Match (ETag = "seq-1042"),
Accept: application/cbor (по умолчанию) или
application/json.
HTTP/1.1 200 OK
Content-Type: application/cbor
ETag: "seq-1042"
Cache-Control: public, max-age=3600
X-CRL-Seq: 1042
X-CRL-Next-Update: 2027-05-09T00:00:00Z
JSON-вид payload:
{"v":1,"issued_at":"…","next_update":"…","seq":1042,"min_client_version":"1.0.0", "revoked_licenses":[{"id":"lic_…","reason":"transfer","at":"…"}],"revoked_keys":["<fp32>"],"revoked_batches":[16], "revoked_holders":["org_…"],"revoked_signing_keys":[]}
+ signature.
3.9
GET /v1/signing-keys — публичные ключи
→ 200 { "keys": [ { "key_id": "mo-lic-ce69679b", "alg": "Ed25519", "pub": "base64:KNbsvO2trIfyplUVL6VGYKCUFNQ/J/qcHtQFWafDpmo=", "role": "primary", "valid_from": "2027-01-01T00:00:00Z" },
{ "key_id": "mo-lic-0a1b2c3d", "alg": "Ed25519", "pub": "base64:…", "role": "reserve" } ], "server_time": "…" }Клиент использует список только для подсказки о ротации; доверяет лишь ключам, зашитым в сборку (и делегированным через цепочку).
3.10
GET /v1/time — подписанное время
→ 200 { "server_time": "2027-05-08T10:00:03Z", "sig": "base64:<Ed25519 над \"MRDN-TIME-v1\" ‖ server_time>", "key_id": "mo-lic-ce69679b" }3.11
GET /v1/catalog?region=ru¤cy=RUB — цены для Hub
(прокси в billing-service)
→ 200 { "region": "ru", "currency": "RUB", "items": [ { "price_id": "prc_…", "edition": "pro", "kind": "subscription", "period": "year", "amount": 499000, "amount_display": "4 990 ₽", "per": "year", "min_seats": 1 } ], "cache_ttl": 86400 }Суммы — в минимальных единицах валюты (копейки/центы).
4. Эндпоинты: офлайн
4.1
POST /v1/offline/request — .mreq →
.mlic
Auth: Bearer (пользователь портала) или api_key
(licenses:manage) / консоль администратора. Лимит: 100/сут
на субъект.
{ "request": "base64:<.mreq CBOR>", "assign_to": { "org_id": "org_…", "user_id": null } }
→ 200 { "license": "base64:<.mlic>", "license_json": { "…": "…" }, "license_text": "-----BEGIN MERIDIAN LICENSE-----\n…\n-----END MERIDIAN LICENSE-----",
"request_id": "req_…", "seats_used": 213, "seats_total": 500 }Пакетный режим: POST /v1/offline/batch с
multipart/form-data (zip из .mreq, ≤ 5000
файлов) → 202 { "job_id": "job_…" },
GET /v1/offline/batch/{job_id} →
{ "status": "done", "ok": 498, "failed": [ { "file": "WS-0007.mreq", "code": "KEY_EXHAUSTED" } ], "download_url": "…zip" }.
Ошибки: все коды /activate, плюс
400 REQUEST_INVALID (подпись устройства в
.mreq неверна), 410 REQUEST_EXPIRED (> 30
дней), 409 REQUEST_REPLAYED (nonce использован другим
устройством), 403 OFFLINE_NOT_ALLOWED (редакция/тип без
офлайн-активации: подписки Home/Pro).
4.2
POST /v1/offline/deactivate — офлайн-деактивация
{ "request": "base64:<.mreq kind=deactivate>" }
→ 200 { "released": true, "seats_used": 212, "seats_total": 500 }4.3 Формат .mreq
CBOR-контейнер
{ "v": 1, "payload": bstr, "signature": { "alg": "Ed25519", "key_id": "dev", "sig": bstr } };
подпись — ключом устройства. Payload: format_version,
request_id (req_ + ULID), kind
(activate | deactivate | rebind), key (строка,
опц.), entitlement_id (опц.), license_id (для
deactivate/rebind), device
(Device), device_pubkey, client,
requested_at, nonce (16 байт),
org_hint. Текстовая форма —
-----BEGIN MERIDIAN REQUEST----- + base64; QR — тот же
base64.
5. Эндпоинты: плавающие лицензии (on-prem сервер; в облаке — отсутствуют)
Auth: подпись устройства (device_pubkey регистрируется
при первой аренде; при policy.license_scope = user — плюс
Bearer локального id-service/SSO).
| Метод | Путь | Тело → Ответ |
|---|---|---|
| POST | /v1/float/lease |
{pool: "pro", device, client, user_scope?} →
200 { "…": "LicenseResult (kind float_lease)", "lease_id": "lse_…", "lease_expires_at": "…", "offline_reserve_until": "…", "pool": {"size": 200, "in_use": 57} };
409 FLOAT_POOL_EXHAUSTED {in_use, size, queue_position, retry_after} |
| POST | /v1/float/renew |
{lease_id} →
200 {lease_expires_at, offline_reserve_until, license?: (новая аренда, если политика/пул изменились)};
404 FLOAT_LEASE_NOT_FOUND → клиент делает новый
lease |
| POST | /v1/float/release |
`{lease_id, reason: "exit |
| POST | /v1/float/checkout |
{lease_id, days: 10} →
200 {offline_reserve_until, checkout: true};
403 CHECKOUT_DENIED {max_days} |
| GET | /v1/float/status |
admin →
200 {pools: [{name, size, in_use, queued, peak_24h, denied_24h}], leases: [{lease_id, user, device, since, expires_at, checkout}], server: {server_id, license_expires_at, version}} |
| POST | /v1/float/leases/{lease_id}/revoke |
admin → 200 {revoked: true}; клиент узнаёт при
следующем renew (404) |
| GET | /v1/float/leases?cursor= |
admin — журнал аренд (CSV при Accept: text/csv) |
Тайминги по умолчанию (из delegation лицензии сервера):
аренда 60 мин, продление клиентом каждые 15 мин, офлайн-резерв 24 ч,
checkout ≤ 30 дн.
6.
Эндпоинты: организация (консоль администратора, Bearer с ролью
license_admin/admin/owner, лимит
600/мин)
| Метод | Путь | Назначение |
|---|---|---|
| GET | /v1/orgs/{org_id}/entitlements |
права организации (подписки, объёмные ключи, плавающие пулы) |
| GET/POST | /v1/orgs/{org_id}/seats |
список назначений; `POST {entitlement_id, user_id |
| DELETE | /v1/orgs/{org_id}/seats/{user_id} |
снять место (лицензии пользователя — в CRL, устройства освобождены) |
| POST | /v1/orgs/{org_id}/seats/import |
CSV email,group → назначения; ответ — отчёт |
| GET | /v1/orgs/{org_id}/devices?inactive_days=90 |
устройства организации |
| POST | /v1/orgs/{org_id}/devices/release-inactive |
{inactive_days: 90} → освобождение |
| GET | /v1/orgs/{org_id}/keys |
ключи организации (маска, тип, места, использовано,
activate_before) |
| POST | /v1/orgs/{org_id}/keys/{key_fingerprint}/revoke |
отзыв объёмного ключа организации (например, утёк конфиг) и перевыпуск нового |
| GET/POST | /v1/orgs/{org_id}/float/servers |
регистрация on-prem сервера:
POST {name, delegate_pubkey, pools} →
server.mlic |
| GET | /v1/orgs/{org_id}/reports/usage?from&to |
использование мест/устройств/активаций (JSON/CSV) |
7.
Эндпоинты: внутренние (mTLS; /internal/v1/*, недоступны
через edge)
| Метод | Путь | Вызывающий | Назначение |
|---|---|---|---|
| POST | /internal/v1/keys/batches |
keygen (подпись запроса ключом keygen) | создать партию:
{batch_id, edition, kind, seats, count, channel, activate_before, region, keys:[{rnd, mac28, key_enc}]} |
| POST | /internal/v1/keys/batches/{batch_id}/status |
keygen / ops | `draft → issued → live → closed |
| POST | /internal/v1/keys/revoke |
ops, billing (chargeback) | `{key_fingerprint |
| POST | /internal/v1/licenses/{license_id}/revoke |
ops, id-service (device removed) | {reason} |
| POST | /internal/v1/entitlements |
billing-service (события entitlement.upsert) |
{idempotency: invoice_id, holder, edition, kind, seats, valid_from, expires_at, limits, source} |
| POST | /internal/v1/entitlements/{id}/suspend /
/resume |
billing (past_due/suspended/paid) | |
| POST | /internal/v1/holders/{org_id}/policy |
id-service (org.policy_updated) |
пометить лицензии организации stale → перевыпуск при
/validate |
| POST | /internal/v1/users/{user_id}/purge |
id-service (удаление аккаунта) | отзыв всех лицензий, обезличивание |
| GET | /internal/v1/health,
/internal/v1/metrics |
оркестратор, Prometheus |
8. Коды ошибок
| Код | HTTP | Повторять | Поля details |
|---|---|---|---|
VALIDATION_ERROR |
400 | нет | fields: {path: message} |
KEY_INVALID_FORMAT, KEY_CHECKSUM,
KEY_UNSUPPORTED_VERSION, REQUEST_INVALID |
400 | нет | — |
UNAUTHENTICATED |
401 | после входа/перевыпуска подписи | `reason: token_expired |
SUBSCRIPTION_PAST_DUE |
402 | после оплаты | pay_url, grace_until |
FORBIDDEN, ORG_POLICY_DENIED,
KEY_REGION_MISMATCH, OFFLINE_NOT_ALLOWED,
SEAT_NOT_ASSIGNED, CHECKOUT_DENIED |
403 | нет | `policy_rule |
KEY_NOT_FOUND, ENTITLEMENT_NOT_FOUND,
LICENSE_NOT_FOUND, DEVICE_NOT_FOUND,
FLOAT_LEASE_NOT_FOUND |
404 | нет (float: новый lease) | — |
ACCOUNT_REQUIRED |
409 | после входа | login_url |
KEY_ALREADY_BOUND, KEY_EXHAUSTED,
DEVICE_LIMIT, FLOAT_POOL_EXHAUSTED,
IDEMPOTENCY_CONFLICT, TRIAL_ALREADY_USED,
REQUEST_REPLAYED |
409 | по ситуации | `devices[], can_release |
KEY_REVOKED, LICENSE_REVOKED,
KEY_EXPIRED, ENTITLEMENT_EXPIRED,
REQUEST_EXPIRED |
410 | нет | `reason, support_url |
CLIENT_TOO_OLD |
426 | после обновления | min_client_version, update_url |
TRANSFER_LIMIT, RATE_LIMITED |
429 | Retry-After |
next_allowed_at |
INTERNAL |
500 | экспоненциально, ≤ 5 | request_id |
SIGNING_UNAVAILABLE |
503 | через Retry-After |
— |
9. Пример полного цикла (curl)
# 1. Активация вечной Home без аккаунта
curl -sS https://license.meridian-office.ru/v1/activate \
-H 'Content-Type: application/json' -H "Idempotency-Key: $(uuidgen)" \
-H 'X-Meridian-Client: cli/1.2.0 (linux 6.8; x86_64; build 1.2.0.418)' \
-d @activate.json | jq -r .license | base64 -d > license.mlic
# 2. Проверка через неделю (подпись устройства считает meridian-license-cli)
meridian-license-cli validate --license license.mlic --server https://license.meridian-office.ru
# 3. Деактивация перед продажей компьютера
meridian-license-cli deactivate --license license.mlic --reason user10. Совместимость и эволюция
- Новые поля в ответах добавляются без смены версии; клиенты игнорируют неизвестные поля.
- Новые коды ошибок возможны в пределах того же HTTP-класса; клиент обрабатывает неизвестный код по HTTP-статусу.
supported_license_formatsвClientограничивает формат выпускаемой лицензии; сервер никогда не выпускает формат выше заявленного.- On-prem сервер реализует разделы 3 (кроме 3.6, 3.11), 4, 5, 6
(частично) и
/internalдля локального id-service; облачные эндпоинты биллинга и trial на on-prem отсутствуют (404 NOT_SUPPORTED).