ДЛЯ РАЗРАБОТЧИКОВ

Документация API

Активация, проверка лицензий и доставка обновлений вашим приложениям.

Авторизация

Все запросы рабочего сайта выполняются по HTTPS. Создайте код продукта и ключ приложения в админке. В заголовке X-API-Key передавайте Public API Key. Ключ лицензии передаётся в JSON, а при проверке обновлений — в X-License-Key.

Public API Key идентифицирует приложение и не является секретом клиента. Secret Key нужен только для запросов между доверенными серверами. Не встраивайте его в дистрибутив.

Методы

МетодАдресДействие
POST/api/v1/license/activateЗанять место для устройства
POST/api/v1/license/checkПроверить право запуска на устройстве
POST/api/v1/license/deactivateОсвободить место устройства
POST/api/v1/license/heartbeatЗафиксировать активность
GET/api/v1/products/{product_code}/update?version=1.0.0Проверка версии и временная ссылка скачивания
GET/api/v1/public-keyПубличный ключ Ed25519

Тело запроса лицензии

{
    "license_key": "IOC-XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX",
    "product_code": "MY_PRODUCT",
    "device_id": "installation-unique-id",
    "hardware_hash": "sha256-of-stable-hardware-fingerprint",
    "app_version": "1.0.0",
    "os": "Windows 11",
    "computer_name": "Рабочий компьютер"
}

license_key, product_code, device_id, hardware_hash обязательны. Идентификатор и аппаратный отпечаток: 3–190 символов. Device ID должен сохраняться между запусками. При ограничении версий передавайте разрешённую app_version.

Ответ и подпись

В ответе есть success, status, license, server_time, valid_until, device_id, product_code, а также signed_payload и signature.

  1. Закрепите публичный ключ сервера в клиенте во время сборки. Не доверяйте ключу, полученному вместе с непроверенным ответом.
  2. Декодируйте signature из Base64 и проверьте Ed25519 по точным UTF-8 байтам строки signed_payload.
  3. Только после проверки декодируйте signed_payload из Base64 в JSON. Используйте поля этого проверенного JSON.
  4. Сравните код продукта, Device ID, время сервера и срок действия. Разрешайте работу только при success=true и status=active.

Обычный ответ действителен максимум 15 минут. Клиент должен регулярно запрашивать проверку. Доверенный источник времени и защита от отката часов реализуются в клиентском приложении.

Статусы лицензии

activeЛицензия действует
expiredИстёк срок
blockedЛицензия, аккаунт или заказ заблокированы
invalidНеверный ключ, устройство, отпечаток или версия
device_limitИсчерпан лимит устройств или активаций
product_mismatchКлюч относится к другой программе
offline_leaseМесто занято автономной активацией

Неверный ключ приложения: HTTP 401. Неверный запрос: 400. Лимит запросов: 429 с заголовком Retry-After. Ошибки протокола возвращают JSON; бизнес-статусы лицензии возвращаются с HTTP 200.

Автономная активация

Клиент передаёт администратору Machine Code: JSON с кодом продукта, Device ID и hardware_hash. Администратор открывает лицензию и создаёт подписанный ответ на 1–720 часов. Клиент проверяет подпись, продукт, устройство, отпечаток и valid_until. Место устройства остаётся занятым до истечения автономного срока.

Заблокированная на сервере автономная лицензия может продолжать работу до окончания уже выданного срока: мгновенный отзыв без связи с сервером невозможен.

Подпись запросов доверенного сервера

Необязательные заголовки: X-Timestamp (Unix UTC), X-Nonce (16–100 букв, цифр, дефисов или подчёркиваний), X-Signature. Допуск времени — 5 минут. Подпись — hex HMAC-SHA256 с Secret Key от строки:

METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + SHA256_HEX(RAW_BODY)

PATH — путь без query string. Повторный nonce отклоняется. Для GET тело пустое; Secret Key даёт дополнительную проверку отправителя, но право на платный файл по-прежнему определяется лицензией.

Примеры клиентов

Готовые функции приведены ниже и находятся в каталоге api/examples архива. Передайте адрес вашего сервера, ключ приложения, закреплённый ключ Ed25519 и параметры лицензии. Версию 1.0.0 замените версией своего приложения.

PHP
<?php
// PHP 8.1+, curl, sodium. Используйте HTTPS и закреплённый публичный ключ сервера.
function activateLicense(string $base,string $apiKey,string $publicKey,string $licenseKey,string $productCode,string $deviceId,string $hardwareHash): array {
 $request=['license_key'=>$licenseKey,'product_code'=>$productCode,'device_id'=>$deviceId,'hardware_hash'=>$hardwareHash,'app_version'=>'1.0.0','os'=>PHP_OS_FAMILY,'computer_name'=>gethostname()];
 $ch=curl_init(rtrim($base,'/').'/api/v1/license/activate');curl_setopt_array($ch,[CURLOPT_POST=>true,CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>20,CURLOPT_SSL_VERIFYPEER=>true,CURLOPT_HTTPHEADER=>['Content-Type: application/json','X-API-Key: '.$apiKey],CURLOPT_POSTFIELDS=>json_encode($request)]);$raw=curl_exec($ch);$status=curl_getinfo($ch,CURLINFO_RESPONSE_CODE);curl_close($ch);if($raw===false||$status!==200)throw new RuntimeException('Ошибка связи с сервером лицензий.');
 $envelope=json_decode($raw,true,512,JSON_THROW_ON_ERROR);$payload=$envelope['signed_payload']??'';$signature=base64_decode($envelope['signature']??'',true);$public=base64_decode($publicKey,true);
 if(!$signature||strlen($signature)!==SODIUM_CRYPTO_SIGN_BYTES||!$public||strlen($public)!==SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES||!sodium_crypto_sign_verify_detached($signature,$payload,$public))throw new RuntimeException('Подпись лицензии недействительна.');
 $data=json_decode(base64_decode($payload,true),true,512,JSON_THROW_ON_ERROR);
 if(($data['product_code']??'')!==$productCode||($data['device_id']??'')!==$deviceId||!($data['success']??false)||($data['status']??'')!=='active'||($data['valid_until']??0)<time()||abs(time()-($data['server_time']??0))>300)throw new RuntimeException('Лицензия недействительна.');return $data;
}
Python
"""Python 3.10+, pip install cryptography. Secret Key не нужен клиенту."""
import base64,json,time,urllib.request
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

def activate(base,api_key,public_key,license_key,product_code,device_id,hardware_hash):
    body=dict(license_key=license_key,product_code=product_code,device_id=device_id,
              hardware_hash=hardware_hash,app_version='1.0.0',os='Windows',computer_name='Рабочий компьютер')
    request=urllib.request.Request(base.rstrip('/')+'/api/v1/license/activate',
        data=json.dumps(body).encode(),headers={'Content-Type':'application/json','X-API-Key':api_key},method='POST')
    with urllib.request.urlopen(request,timeout=20) as response:
        envelope=json.load(response)
    Ed25519PublicKey.from_public_bytes(base64.b64decode(public_key)).verify(
        base64.b64decode(envelope['signature']),envelope['signed_payload'].encode())
    data=json.loads(base64.b64decode(envelope['signed_payload']))
    if (not data.get('success') or data.get('status')!='active' or data.get('product_code')!=product_code
        or data.get('device_id')!=device_id or data.get('valid_until',0)<time()
        or abs(time.time()-data.get('server_time',0))>300):
        raise ValueError('Лицензия недействительна')
    return data
JavaScript
// Современный JavaScript / WebCrypto Ed25519. Используйте на доверенной странице того же origin
// или в приложении, которое поддерживает HTTPS fetch. Секрет приложения не встраивается.
export async function activate(base, apiKey, publicKey, input) {
  const response = await fetch(base.replace(/\/$/, '') + '/api/v1/license/activate', {
    method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },
    body: JSON.stringify(input)
  });
  if (!response.ok) throw new Error('Ошибка сервера лицензий');
  const envelope = await response.json();
  const decode = value => Uint8Array.from(atob(value), c => c.charCodeAt(0));
  const key = await crypto.subtle.importKey('raw', decode(publicKey), 'Ed25519', false, ['verify']);
  if (!await crypto.subtle.verify('Ed25519', key, decode(envelope.signature), new TextEncoder().encode(envelope.signed_payload)))
    throw new Error('Подпись лицензии недействительна');
  const data = JSON.parse(new TextDecoder().decode(decode(envelope.signed_payload)));
  if (!data.success || data.status !== 'active' || data.product_code !== input.product_code ||
      data.device_id !== input.device_id || data.valid_until < Date.now()/1000 || Math.abs(Date.now()/1000-data.server_time)>300)
    throw new Error('Лицензия недействительна');
  return data;
}
C#
// .NET 8+, пакет NSec.Cryptography (Ed25519). Публичный ключ закрепите в клиенте.
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using NSec.Cryptography;

public static class LicenseClient {
 public static async Task<JsonElement> Activate(HttpClient http, string baseUrl, string apiKey,
  string publicKey, string licenseKey, string productCode, string deviceId, string hardwareHash) {
  using var request = new HttpRequestMessage(HttpMethod.Post, baseUrl.TrimEnd('/')+"/api/v1/license/activate");
  request.Headers.Add("X-API-Key", apiKey);
  request.Content=JsonContent.Create(new { license_key=licenseKey, product_code=productCode,
   device_id=deviceId, hardware_hash=hardwareHash, app_version="1.0.0", os="Windows", computer_name=Environment.MachineName });
  using var response=await http.SendAsync(request);
  response.EnsureSuccessStatusCode();
  using var envelope=JsonDocument.Parse(await response.Content.ReadAsStringAsync());
  var root=envelope.RootElement; var payload=root.GetProperty("signed_payload").GetString()!;
  var algorithm=SignatureAlgorithm.Ed25519;
  var key=PublicKey.Import(algorithm,Convert.FromBase64String(publicKey),KeyBlobFormat.RawPublicKey);
  if(!algorithm.Verify(key,Encoding.UTF8.GetBytes(payload),Convert.FromBase64String(root.GetProperty("signature").GetString()!)))
   throw new InvalidOperationException("Подпись лицензии недействительна");
  using var parsed=JsonDocument.Parse(Convert.FromBase64String(payload));var data=parsed.RootElement;
  long now=DateTimeOffset.UtcNow.ToUnixTimeSeconds();
  if(!data.GetProperty("success").GetBoolean() || data.GetProperty("status").GetString()!="active" ||
   data.GetProperty("product_code").GetString()!=productCode || data.GetProperty("device_id").GetString()!=deviceId ||
   data.GetProperty("valid_until").GetInt64()<now || Math.Abs(now-data.GetProperty("server_time").GetInt64())>300)
   throw new InvalidOperationException("Лицензия недействительна");
  return data.Clone();
 }
}
C++
// C++17; библиотеки libcurl, libsodium, nlohmann/json. Не отключайте проверку TLS.
#include <curl/curl.h>
#include <sodium.h>
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
#include <stdexcept>
#include <ctime>
#include <cstdlib>
using nlohmann::json;
static size_t receive(char* data,size_t size,size_t count,void* context){
 auto& out=*static_cast<std::string*>(context);size_t len=size*count;if(out.size()+len>1048576)return 0;out.append(data,len);return len;
}
static std::vector<unsigned char> decode64(const std::string& value){
 std::vector<unsigned char> out(value.size());size_t size=0;
 if(sodium_base642bin(out.data(),out.size(),value.data(),value.size(),nullptr,&size,nullptr,sodium_base64_VARIANT_ORIGINAL)!=0)
  throw std::runtime_error("Некорректный Base64");out.resize(size);return out;
}
json activate(const std::string& base,const std::string& apiKey,const std::string& publicKey,const json& input){
 if(sodium_init()<0)throw std::runtime_error("Криптография недоступна");
 CURL* curl=curl_easy_init();if(!curl)throw std::runtime_error("HTTP недоступен");
 std::string output,body=input.dump(),url=base+"/api/v1/license/activate",header="X-API-Key: "+apiKey;
 auto headers=curl_slist_append(nullptr,"Content-Type: application/json");headers=curl_slist_append(headers,header.c_str());
 curl_easy_setopt(curl,CURLOPT_URL,url.c_str());curl_easy_setopt(curl,CURLOPT_POSTFIELDS,body.c_str());
 curl_easy_setopt(curl,CURLOPT_HTTPHEADER,headers);curl_easy_setopt(curl,CURLOPT_TIMEOUT,20L);
 curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,receive);curl_easy_setopt(curl,CURLOPT_WRITEDATA,&output);
 auto result=curl_easy_perform(curl);long status=0;curl_easy_getinfo(curl,CURLINFO_RESPONSE_CODE,&status);
 curl_slist_free_all(headers);curl_easy_cleanup(curl);
 if(result!=CURLE_OK||status!=200)throw std::runtime_error("Ошибка сервера лицензий");
 auto envelope=json::parse(output);std::string payload=envelope.at("signed_payload");
 auto signature=decode64(envelope.at("signature"));auto pub=decode64(publicKey);
 if(signature.size()!=crypto_sign_BYTES||pub.size()!=crypto_sign_PUBLICKEYBYTES||
  crypto_sign_verify_detached(signature.data(),reinterpret_cast<const unsigned char*>(payload.data()),payload.size(),pub.data())!=0)
  throw std::runtime_error("Подпись лицензии недействительна");
 auto bytes=decode64(payload);auto data=json::parse(bytes);auto now=std::time(nullptr);
 if(!data.value("success",false)||data.value("status","")!="active"||data.at("product_code")!=input.at("product_code")||
  data.at("device_id")!=input.at("device_id")||data.value("valid_until",0LL)<now||std::llabs(now-data.value("server_time",0LL))>300)
  throw std::runtime_error("Лицензия недействительна");
 return data;
}

Скачивание и обновления

Ссылки действуют 15 минут. При каждом скачивании сервер повторно проверяет право доступа. После блокировки или возврата оплаты старые ссылки перестают работать. Бесплатный продукт может требовать вход; тогда скачивание доступно через кабинет.

Ограничения частоты

До 120 API-запросов в минуту с IP и до 90 запросов в минуту на приложение с этого IP. Не выполняйте heartbeat каждую секунду. Обрабатывайте сетевые сбои с увеличивающимся интервалом повторов.