Meridian Office

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&currency=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 user

10. Совместимость и эволюция