API Reference
Два контура в одном процессе: panel API для управления и client API для встроенного SDK. Все примеры — против http://localhost:5100.
Тарҷумаи тоҷикии ҳуҷҷатҳо дар ҳоли омодашавӣ аст. Матни зерин ба забони русӣ оварда шудааст.
Контуры и rate limits
| Контур | Префикс | Назначение | Аутентификация | Rate limit |
|---|---|---|---|---|
| Panel API | /api/v1/panel/* | Управление: оператор и тенант | cookie certified.auth / PAT Bearer | 300 req/мин на IP |
| Panel · auth | /api/v1/panel/auth/* | Логин, 2FA, приглашения, сброс пароля | см. ниже | 20 req/мин на IP |
| Client API | /api/v1/client/* | Встроенный SDK клиентского приложения | публичный; мутации по HMAC | 100 req/мин на IP |
Лимиты — fixed-window по IP. При отказе ответ 429 несёт стандартные заголовки:
HTTP/1.1 429 Too Many RequestsRetry-After: 60RateLimit-Limit: 20RateLimit-Remaining: 0RateLimit-Reset: 60Модель авторизации
Panel: сессионная cookie
Логин POST /api/v1/panel/auth/login выставляет cookie certified.auth (HttpOnly, SameSite=Lax, sliding 7 дней). Если у аккаунта включён TOTP, вместо cookie возвращается короткоживущий ticket — сессия открывается только после POST /auth/login/2fa.
| Роль | Что может |
|---|---|
| Operator | Глобальный скоуп: шаблоны, webhooks, аудит, корневые компании; обходит все ограничения по поддереву |
| Owner | Полные права на узле компании и всём его поддереве |
| Admin | Выпуск и отзыв лицензий, создание дочерних компаний в пределах поддерева |
| Viewer | Только чтение в пределах поддерева |
Panel: personal access token (Bearer)
Для машинной интеграции вместо cookie можно предъявить персональный токен в заголовке Authorization: Bearer cfed_pat_…. Формат токена — cfed_pat_ + base64url(32 случайных байта); в базе хранится только SHA-256-хеш и короткий несекретный префикс для поиска.
- Токен привязан к компании и действует с правами Admin внутри её поддерева — глобальных операторских прав он не даёт.
- Отозванный (
revokedAt) или просроченный (expiresAt) токен отклоняется. - Секрет показывается один раз — при создании; далее доступны только метаданные.
| Метод | Путь | Назначение |
|---|---|---|
| GET | /api/v1/panel/api-tokens | Список токенов (без секретов) |
| POST | /api/v1/panel/api-tokens | Создать; req { name, companyId, expiresInDays? } → { token, secret } |
| DELETE | /api/v1/panel/api-tokens/{id} | Отозвать токен |
Client: HMAC-подпись
Client-эндпоинты публичны. activate использует сам license key как общий секрет. heartbeat и deactivate требуют подписи запроса:
X-CertifiEd-Timestamp: <unix_seconds>X-CertifiEd-Signature: v1=<hex( HMAC_SHA256(key = licenseKey, msg = timestamp) )>Timestamp вне окна ±5 минут отклоняется — это защита от replay. Схему реализует HmacRequestValidator; SDK делает подпись самостоятельно.
Формат ошибок
Единый JSON во всех эндпоинтах обоих контуров:
{ "code": "license.company_not_active", "message": "Company is Archived.", "status": 400}| `ErrorKind` | HTTP | Когда |
|---|---|---|
| Validation | 400 | Некорректный запрос, нарушение инварианта |
| Unauthorized | 401 | Нет сессии / неверный или просроченный токен |
| Forbidden | 403 | Роли недостаточно для скоупа |
| NotFound | 404 | Сущность не найдена или вне видимого поддерева |
| Conflict | 409 | Конфликт состояния (например, занятый slug) |
| Failure | 500 | Внутренняя ошибка |
Panel · Auth
| Метод | Путь | Назначение | Auth |
|---|---|---|---|
| POST | /api/v1/panel/auth/login | Вход; ставит cookie certified.auth | — |
| POST | /api/v1/panel/auth/login/2fa | Второй шаг при включённом TOTP | ticket |
| POST | /api/v1/panel/auth/logout | Выход (204) | cookie |
| GET | /api/v1/panel/auth/me | Текущий пользователь | cookie / PAT |
| GET | /api/v1/panel/auth/invite/{token} | Превью приглашения перед регистрацией | — |
| POST | /api/v1/panel/auth/accept-invite | Принять приглашение: задать пароль и войти | — |
| POST | /api/v1/panel/auth/forgot-password | Письмо со ссылкой на сброс (204) | — |
| POST | /api/v1/panel/auth/reset-password | Сброс пароля по токену (204) | — |
| POST | /api/v1/panel/auth/change-password | Смена своего пароля (204) | cookie |
| POST | /api/v1/panel/auth/2fa/enroll | Начать enrollment TOTP | cookie |
| POST | /api/v1/panel/auth/2fa/enable | Подтвердить кодом и включить | cookie |
| POST | /api/v1/panel/auth/2fa/disable | Выключить: пароль + код (204) | cookie |
- login — req
{ email, password }→LoginResponse { twoFactorRequired, ticket, user }. ПриtwoFactorRequired: trueполеuserпустое и cookie не выставляется. - login/2fa — req
{ ticket, code }→AuthUserResponse { id, email, displayName, isOperator }.code— живой TOTP либо неиспользованный recovery-код; ticket живёт 5 минут. - invite/{token} —
InvitePreviewResponse { email, companyName, role, expiresAt }. accept-invite — req{ token, password }; пароль от 8 символов, токен приглашения живёт 7 дней. - forgot-password — req
{ email }, всегда 204: существование адреса не раскрывается. reset-password — req{ token, newPassword }; change-password — req{ currentPassword, newPassword }. - 2fa/enroll →
{ secret, otpauthUri }(base32-секрет и URI для QR). 2fa/enable — req{ code }→{ recoveryCodes[] }, показываются один раз. 2fa/disable — req{ password, code }.
Panel · Companies
| Метод | Путь | Назначение | Мин. роль |
|---|---|---|---|
| GET | /api/v1/panel/companies | Список видимых компаний | любой аутентифицированный (scoped) |
| GET | /{id} | Одна компания | Viewer |
| GET | /{id}/children | Прямые потомки | Viewer |
| GET | /{id}/subtree | Всё поддерево | Viewer |
| POST | / | Создать компанию (201) | Operator (корень) / Admin на родителе |
| POST | /{id}/reparent | Перенести узел вместе с поддеревом | Operator |
| DELETE | /{id} | Архивировать (204) | Owner |
- create — req
{ name, slug, parentId?, contactEmail? }.slug— 1–64 символа[a-z0-9-], не начинается и не заканчивается дефисом, уникален среди сиблингов.parentId: nullсоздаёт корневую компанию — только Operator. - reparent — req
{ newParentId }(null— вынести в корень) →CompanyResponse. Перестраивает ltree-pathиdepthдля узла и всех потомков в одной транзакции. CompanyResponse { id, name, slug, parentId, path, depth, status, contactEmail, createdAt }.
Panel · Templates
| Метод | Путь | Назначение | Auth |
|---|---|---|---|
| GET | /api/v1/panel/templates | Список шаблонов | любой аутентифицированный |
| GET | /{id} | Один шаблон | любой аутентифицированный |
| POST | / | Создать шаблон (авто-генерит первый signing key) | Operator |
| PUT | /{id} | Обновить | Operator |
| DELETE | /{id} | Архивировать (204) | Operator |
| GET | /{id}/versions | Версии шаблона | любой аутентифицированный |
| POST | /{id}/versions | Создать версию (станет current) | Operator |
| GET | /{id}/signing-keys | Ключи подписи | Operator |
| POST | /{id}/signing-keys/rotate | Ротация ключа | Operator |
| POST | /{id}/signing-keys/{keyId}/retire | Вывести неактивный ключ (204) | Operator |
- create template — req
{ name, productCode, description?, defaultOfflineDays, defaultValidityDays }.productCodeуникален;defaultOfflineDaysзажимается в диапазон 1..30. - create version — req
{ configSchema, defaults?, changelog? };configSchemaиdefaults— строки JSON. Версия сразу становитсяcurrentVersionId. LicenseTemplateResponse { id, name, productCode, description, defaultOfflineDays, defaultValidityDays, status, currentVersionId, createdAt }.TemplateVersionResponse { id, templateId, version, configSchema, defaults, signingKeyId, changelog, createdAt }.SigningKeyResponse { id, status, notBefore, notAfter, createdAt, publicKeyHex }.
Panel · Licenses
| Метод | Путь | Назначение | Мин. роль |
|---|---|---|---|
| GET | /api/v1/panel/licenses | Поиск лицензий (paged) | любой аутентифицированный (scoped) |
| GET | /{id} | Одна лицензия | Viewer |
| POST | / | Выпустить лицензию (201) | Admin на компании |
| POST | /bulk | Массовый выпуск (до 200 шт.) | Admin на каждой компании |
| POST | /{id}/revoke | Отозвать (204) | Admin |
| POST | /{id}/transfer | Перенести в другую компанию | Admin на обеих |
| POST | /{id}/rebind | Перепривязать к другому железу | Admin |
| POST | /{id}/activations/{activationId}/reset | Освободить место (204) | Admin |
| GET | /{id}/download | Скачать .ced (application/octet-stream) | Viewer |
| GET | /{id}/public-key | Публичный ключ (hex) | Viewer |
| GET | /{id}/activations | Активации лицензии | Viewer |
| GET | /{id}/heartbeats | Последние heartbeat'ы (?limit, 1..1000) | Viewer |
Поиск
GET / — query companyId?, templateId?, status? (Active / Revoked / Expired …), search?, page=1, pageSize=50 (максимум 200) → PagedResult<LicenseResponse> { items, total, page, pageSize }. Неизвестный статус — license.invalid_status (400), компания вне скоупа — auth.company_out_of_scope (403).
Выпуск
{ "companyId": "019f7ef0-0ca2-73b1-8830-f016df1bb6d0", "templateId": "019f7ef0-0d1c-7207-9c20-21d14619ed4a", "config": "{\"features\":[\"export\"],\"limits\":{\"maxSeats\":25}}", "expiresAt": null, "offlineDays": 7, "isTrial": false, "hwBinding": "none", "hwFingerprint": null}config— строка JSON (features / limits), встраивается в подписанный токен.expiresAt: null→now + template.defaultValidityDays, а приisTrial: true→now + Licensing:DefaultTrialDays(по умолчанию 14 дней).offlineDaysзажимается в1..Licensing:MaxOfflineDays(по умолчанию 30).hwBinding—none/fixed/firstActivation, см. Привязка к железу. Если поле опущено, режим выводится изhwFingerprint: есть —fixed, нет —none.- Ответ
LicenseIssueResponse { id, licenseKey, token, publicKey },publicKey— hex.
Массовый выпуск
POST /bulk — req { templateId, items: [{ companyId, config? }], config?, expiresAt?, offlineDays?, hwFingerprint?, isTrial?, hwBinding? }. Item-level config перекрывает батчевый, остальные поля общие. Максимум 200 элементов (license.bulk_too_large), пустой список — license.bulk_empty.
Ошибка отдельного элемента (нет прав, компания архивирована, битый config) не роняет батч — она возвращается в его строке результата. Успешные лицензии сохраняются одной транзакцией.
{ "requested": 2, "succeeded": 2, "failed": 0, "results": [ { "companyId": "019f7ef0-0ca2-73b1-8830-f016df1bb6d0", "licenseId": "019f7ef0-48f7-7e69-afda-dbe061cd6da8", "licenseKey": "CFED-N2XH-SMPJ-VCF2-G2GB", "errorCode": null, "errorMessage": null }, { "companyId": "019f7ef0-0ce1-708d-9621-f89c477b7faf", "licenseId": "019f7ef0-48fc-74f7-9bd9-cf15478c5ae4", "licenseKey": "CFED-RY8S-S4F8-Y9ZJ-UDBZ", "errorCode": null, "errorMessage": null } ]}Перенос
POST /{id}/transfer — req { newCompanyId } → LicenseTransferResponse { licenseId, licenseKey, companyId, currentVersion, token, publicKey, deactivatedActivations }.
Лицензиат (sub) зашит в подписанный токен, поэтому перенос подписывает новую версию лицензии (тот же license key, currentVersion + 1, config сохраняется) и деактивирует все активные активации — их места освобождаются. Клиенту нужно заново скачать `.ced`. Требует Admin на исходной и целевой компании.
Перепривязка к оборудованию
POST /{id}/rebind — req { hwFingerprint, reason?, hwBinding? } → LicenseResponse.
hwFingerprint: "<новый отпечаток>"— закрепить за новой машиной (режим становитсяfixed).hwFingerprint: null— снять привязку:fixedоткатывается вfirstActivation(закрепится на следующей активации),noneостаётсяnone.hwBindingявно задаёт итоговый режим вместо выводимого.
Все активные активации деактивируются, лицензия переподписывается новой версией — клиенту нужно заново скачать .ced. Требует Admin на компании лицензии (оператор проходит всегда). Событие вебхука — license.rebound. Подробно — Привязка к железу.
Прочее и модели
- reset activation —
POST /{id}/activations/{activationId}/reset→ 204. Точечно деактивирует одну активацию, освобождая место (floating-seat). Повторный вызов идемпотентен, чужая активация —activation.not_found(404). - download — тело
.ced:{ licenseKey, token, publicKey }; имя файла{licenseKey}.ced. - public-key —
LicensePublicKeyResponse { publicKey }(hex). LicenseResponse { id, licenseKey, companyId, templateId, status, config, offlineDays, currentVersion, expiresAt, issuedAt, activatedAt, lastHeartbeatAt, revokedAt, revocationReason, isTrial, hwBinding, hwFingerprint }.ActivationResponse { id, hwFingerprint, machineName, status, activatedAt, lastHeartbeatAt, deactivatedAt }.HeartbeatRecordResponse { id, activationId, occurredAt }.
Panel · Users
| Метод | Путь | Назначение | Мин. роль |
|---|---|---|---|
| GET | /api/v1/panel/users?companyId=… | Пользователи компании и её поддерева | Admin |
| POST | /invite | Пригласить пользователя в компанию | Admin (Owner — чтобы выдать роль Owner) |
| POST | /{id}/resend-invite | Переотправить приглашение (204) | Admin |
| DELETE | /{id} | Деактивировать аккаунт (204) | Admin (Operator — для оператора) |
- invite — req
{ email, companyId, role, displayName? };role∈owner/admin/viewer. Создаётpending-аккаунт и шлёт письмо со ссылкой/accept-invite?token=…(живёт 7 дней). UserResponse { id, email, displayName, isOperator, totpEnabled, status, companyId, role, lastLoginAt, createdAt };status—pending/active/revoked.
Panel · Operator
Вся группа доступна только оператору — иначе auth.operator_required (403).
| Метод | Путь | Назначение |
|---|---|---|
| GET | /api/v1/panel/operator/overview | Платформенные счётчики |
| GET | /tenants | Корневые компании с размером поддерева (paged, ?search) |
| GET | /licenses | Все лицензии платформы (paged, ?status, ?templateId, ?companyId, ?trial) |
| GET | /users | Все операторские аккаунты |
| POST | /users/invite | Пригласить нового оператора |
| POST | /users/{id}/promote | Выдать операторские права существующему пользователю |
| POST | /users/{id}/demote | Снять операторские права |
- overview —
OperatorOverviewResponse { totalCompanies, rootCompanies, licenses { active, suspended, revoked, expired, total }, trialLicenses, activeActivations, licensesExpiringIn30Days, templates, signingKeys { … }, webhookEndpoints, deliveryHealthLast24h { pending, failed, success } }. - tenants —
OperatorTenantResponse { id, name, slug, status, contactEmail, descendantCount, licenseCount, createdAt }. - licenses —
OperatorLicenseResponse { id, licenseKey, companyId, companyName, templateId, productCode, status, isTrial, issuedAt, expiresAt, activatedAt, revokedAt }. - users —
OperatorUserResponse { id, email, displayName, status, totpEnabled, lastLoginAt, createdAt };status—pending/active/revoked. - users/invite — req
{ email, displayName? }→OperatorUserResponseсоstatus: "pending". Если пользователь уже существует и активен —user.already_exists(409): его нужно promote, а не приглашать.
Panel · Analytics
| Метод | Путь | Назначение | Auth |
|---|---|---|---|
| GET | /api/v1/panel/analytics/usage | Дневные ряды активаций / heartbeat'ов / выпусков | любой аутентифицированный (scoped) |
| GET | /api/v1/panel/analytics/distribution | Срезы по шаблонам и компаниям | любой аутентифицированный (scoped) |
- Скоуп: оператор без
companyIdвидит всю платформу, остальные — объединение видимых поддеревьев.companyIdсужает до поддерева этой компании (нужен Viewer на ней). - usage — query
from?,to?,companyId?. Окно по умолчанию — последние 30 дней, максимум 366 дней (analytics.range_too_large, а приfrom > to—analytics.invalid_range). - distribution — query
companyId?→{ licensesByTemplate[], activationsByTemplate[], topCompaniesByActivations[] }; первые два —{ templateId, productCode, templateName, count }, третий —{ companyId, companyName, count }(топ-10).
{ "from": "2026-06-20T09:53:33.90+00:00", "to": "2026-07-20T09:53:33.90+00:00", "activations": [ { "date": "2026-07-20", "count": 3 } ], "heartbeats": [], "licensesIssued": [ { "date": "2026-07-20", "count": 4 } ]}Panel · Webhooks
| Метод | Путь | Назначение | Auth |
|---|---|---|---|
| GET | /api/v1/panel/webhooks | Список эндпоинтов | Operator |
| GET | /{id} | Один эндпоинт | Operator |
| POST | / | Создать (201) | Operator |
| PATCH | /{id} | Обновить | Operator |
| DELETE | /{id} | Удалить (204) | Operator |
| GET | /{id}/deliveries | История доставок (?limit, 1..200) | Operator |
| POST | /deliveries/{deliveryId}/replay | Переотправить (204) | Operator |
| GET | /event-types | Каталог типов событий | любой аутентифицированный |
- create — req
{ url, secret, description, events[] }.url— абсолютный URI;secretне короче 16 символов (это ключ HMAC-подписи доставок);events— значения изevent-types. - update — req
{ url?, secret?, description?, isActive?, events? }. WebhookEndpointResponse { id, url, description, isActive, events, lastDeliveryAt, createdAt }.WebhookDeliveryResponse { id, endpointId, eventType, payload, status, attemptCount, responseStatusCode, responseBody, lastError, nextAttemptAt, lastAttemptAt, createdAt }.- Каталог событий (
GET /event-types):license.issued,license.revoked,license.expired,license.activated,license.deactivated,license.heartbeat_missed,license.transferred, `license.rebound`.
Формат доставки и проверка подписи — в разделе Webhooks.
Panel · API tokens
| Метод | Путь | Назначение | Мин. роль |
|---|---|---|---|
| GET | /api/v1/panel/api-tokens | Токены в видимом поддереве (без секретов) | любой аутентифицированный (scoped) |
| POST | / | Создать токен | Admin на целевой компании |
| DELETE | /{id} | Отозвать (204) | Admin |
- create — req
{ name, companyId, expiresInDays? }(1..3650) →CreatedApiTokenResponse { token, secret }. `secret` показывается один раз; в базе только SHA-256-хеш и несекретныйprefix. ApiTokenResponse { id, name, companyId, prefix, createdAt, lastUsedAt, expiresAt, revokedAt }.- Коды:
api_token.name_required,api_token.invalid_expiry(400);api_token.not_found(404).
Panel · Audit
| Метод | Путь | Назначение | Auth |
|---|---|---|---|
| GET | /api/v1/panel/audit | Запрос журнала аудита | Operator |
| GET | /api/v1/panel/audit/export | Выгрузка журнала файлом (CSV / JSON) | Operator |
- GET / — query
action?,actorEmail?,entityType?,entityId?,from?,until?,limit=100(максимум 500),offset=0. - GET /export — те же фильтры без
limit/offset, плюсformat=csv|json(по умолчаниюcsv). Ответ стримится вложениемContent-Disposition: attachment; filename="audit-YYYYMMDD.csv", без пагинации. CSV — шапкаtimestamp,actor,action,entityType,entityId,meta, экранирование по RFC 4180. Неизвестный формат —audit.invalid_format(400). - Сам факт экспорта попадает в журнал как действие
audit.exported. AuditLogResponse { id, action, actorEmail, entityType, entityId, meta, ipAddress, occurredAt }.
Client API
| Метод | Путь | Назначение | Auth |
|---|---|---|---|
| POST | /api/v1/client/activate | Активация машины | license key в теле |
| POST | /api/v1/client/heartbeat | Check-in + новый offline-маркер | HMAC |
| POST | /api/v1/client/deactivate | Снять активацию (204) | HMAC |
| GET | /api/v1/client/public-key/{licenseKey} | Публичный ключ по ключу лицензии | — |
- activate — req
{ licenseKey, hwFingerprint?, machineName? }→ActivateResponse { activationId, heartbeatToken, heartbeatIntervalSeconds, maxOfflineDays }. Повторная активация с той же машины (тот жеhwFingerprint) переиспользует активацию и не занимает второе место. - heartbeat — req
{ licenseKey, activationId }+ HMAC-заголовки →HeartbeatResponse { heartbeatToken }(новый подписанный offline-маркер). - deactivate — req
{ licenseKey, activationId }+ HMAC-заголовки → 204. Освобождает место (floating-seat) — SDK делает это вDisposeAsync. Идемпотентен. - public-key —
ClientPublicKeyResponse { publicKey }(hex).
heartbeatToken из activate / heartbeat — это offline-маркер вида base64url(payload).base64url(signature), см. Протокол лицензий. SDK кэширует его как <license>.ced.hb.
Привязка к железу и места
Активация проверяет две независимые вещи. Первая — режим привязки license.hwBinding:
| Режим | Поведение при активации |
|---|---|
none | Любая машина; hwFingerprint необязателен |
fixed | Только машина с отпечатком, заданным при выпуске или перепривязке |
firstActivation | Первая активация обязана прислать hwFingerprint и закрепляет его на лицензии; дальше — как fixed |
Коды: activation.fingerprint_mismatch (400, чужая машина), activation.fingerprint_required (400, firstActivation без отпечатка), activation.not_bound (409, fixed со снятым отпечатком — ждёт перепривязки). Подробно — Привязка к железу.
Вторая — seat-лимит limits.maxSeats из подписанного config. Положительное целое ограничивает число активных активаций; отсутствие поля, ноль или нечисловое значение означают «без ограничения». Место занимает только новая активация; повторная с тем же отпечатком — нет. Освобождают место deactivate (клиент), сброс активации, transfer и rebind (панель).