Docs Документация
Max Systems Max Systems
API Status: Operational Статус API: Работает

KeyForge Developer API

KeyForge API для разработчиков

Integrate digital key distribution into your platform. Two API versions, one powerful engine.

Интегрируйте дистрибуцию цифровых ключей в свою платформу. Два API — один мощный движок.

v1

Legacy API v1

Query-parameter based API with 7 actions. Battle-tested and stable — ideal for payment terminals and kiosks. Not going away: no shutdown date is set.

API на query-параметрах с 7 действиями. Проверенный временем и стабильный — идеален для платёжных терминалов и киосков. Никуда не денется: дата отключения не назначена.

7 Actions
v2

REST API v2

Resource-oriented API with Bearer authentication — no credentials in the URL. Real HTTP status codes and machine-readable error codes. Same sales engine as v1.

Ресурсный API с аутентификацией по Bearer-токену — никаких секретов в URL. Настоящие HTTP-статусы и машинные коды ошибок. То же ядро продаж, что и в v1.

9 Endpoints

Authentication Аутентификация

How to authenticate your requests depending on the API version.

Как аутентифицировать запросы в зависимости от версии API.

Bearer token — recommended for both versions

Bearer-токен — рекомендуется для обеих версий

API credentials travel in the Authorization header, never in the URL. The separator is the first dot — the secret itself may contain dots. Works with REST v2 and with the legacy endpoint; with Bearer the settings parameter becomes optional and lang / currency are passed as ordinary query parameters.

API-креденшелы передаются в заголовке Authorization, а не в URL. Разделитель — первая точка: сам секрет может содержать точки. Работает и для REST v2, и для legacy-эндпоинта; при Bearer параметр settings становится необязательным, а lang / currency передаются обычными query-параметрами.

Example Request
curl https://api.pay.net.az/api/v2/products \
  -H "Authorization: Bearer <api_key>.<api_secret>"

Credentials are issued by a KeyForge administrator and support an IP whitelist and an expiry date. The secret is shown once; if lost, it can only be rotated.

Креденшелы выдаёт администратор KeyForge; поддерживаются IP-whitelist и срок действия. Секрет показывается один раз, при утере его можно только заменить.

Legacy API v1 — query credentials

Legacy API v1 — креденшелы в query

Still supported for backward compatibility: a settings query parameter containing a JSON object with your partner login and password (validated with bcrypt). Be aware that such URLs end up in web server logs, proxy history and Referer headers — prefer the Bearer header above.

По-прежнему поддерживается для обратной совместимости: query-параметр settings с JSON-объектом, содержащим login и password партнёра (проверяются через bcrypt). Учтите, что такие URL оседают в логах веб-сервера, в истории прокси и в заголовке Referer — предпочтительнее Bearer выше.

Example Request
GET /public/json/web.service.2024.php
  ?action=1
  &settings={"login":"your_login","password":"your_password","lang":"en"}
FieldTypeDescriptionОписание
login* string Partner login (issued upon registration) Логин партнёра (выдаётся при регистрации)
password* string Partner password (bcrypt-verified) Пароль партнёра (проверяется через bcrypt)
lang string Response language: en, ru, az Язык ответа: en, ru, az
currency string Currency override: AZN, USD, EUR, TRY, RUB Валюта: AZN, USD, EUR, TRY, RUB

REST API v2

Uses JWT Bearer tokens. Obtain a token via the login endpoint, then include it in every request header.

Используются JWT Bearer токены. Получите токен через эндпоинт авторизации, затем передавайте его в заголовке запроса.

Authorization Header
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Base URLs Базовые URL

Legacy API v1
https://admin.pay.net.az/api/v1/partners/service
Alternative path
Альтернативный путь
https://admin.pay.net.az/public/json/web.service.2024.php
REST API v2
https://admin.pay.net.az/api
Both API versions run on the same server. Legacy v1 is maintained for backward compatibility. New integrations should use REST API v2. Обе версии API работают на одном сервере. Legacy v1 поддерживается для обратной совместимости. Новые интеграции рекомендуем делать через REST API v2.

Error Handling Обработка ошибок

Legacy API v1

Returns a JSON object with code and message. Code "0" means success. Any other code indicates an error.

Возвращает JSON с полями code и message. Код "0" — успех. Любой другой код — ошибка.

Error Response Example
{
  "code": "0-3",
  "message": "PARTNER_INACTIVE"
}

Common Error Codes (prefix 0-X)

Общие коды ошибок (префикс 0-X)

CodeMessageDescriptionОписание
0-1INVALID_REQUESTMissing required query parametersОтсутствуют обязательные параметры
0-2LOGIN_MISSINGMissing 'login' in settingsОтсутствует 'login' в settings
0-3PASSWORD_MISSINGMissing 'password' in settingsОтсутствует 'password' в settings
0-10ACCESS_DENIEDInvalid login or passwordНеверный логин или пароль
0-11PARTNER_BLOCKEDPartner account is blockedАккаунт партнёра заблокирован
0-7INVALID_CURRENCYUnsupported currency codeНеподдерживаемый код валюты
0SUCCESSRequest completed successfullyЗапрос выполнен успешно

REST API v2

Uses standard HTTP status codes with a JSON body containing success boolean and error message.

Использует стандартные HTTP коды с JSON-ответом, содержащим success (boolean) и error (сообщение).

v2 Error Response
{
  "success": false,
  "error": "Unauthorized: Invalid or expired token"
}

Legacy API v1 STABLE

Query-parameter based API designed for payment terminals, kiosks, and simple integrations. All requests use GET method with JSON-encoded data parameter.

API на query-параметрах, разработанный для платёжных терминалов, киосков и простых интеграций. Все запросы через GET с JSON-параметром data.

Request Format

Формат запроса

Base URL
GET /public/json/web.service.2024.php
    ?action={1-7}
    &settings={"login":"...","password":"...","lang":"en","currency":"AZN"}
    &data={url_encoded_json}
ParameterTypeDescriptionОписание
action* integer Action number (1-6) Номер действия (1-6)
settings* json string JSON with login, password, lang, currency JSON с login, password, lang, currency
data json string URL-encoded JSON with action-specific parameters URL-кодированный JSON с параметрами действия

GET  Action 1 — Get Products Получить продукты

Returns the catalog of available products accessible to the partner. Products are filtered by partner's access permissions.

Возвращает каталог доступных продуктов для партнёра. Продукты фильтруются по правам доступа партнёра.

GET /public/json/web.service.2024.php?action=1

Request

cURL
curl -G "https://admin.pay.net.az/public/json/web.service.2024.php" \
  --data-urlencode "action=1" \
  --data-urlencode 'settings={"login":"YOUR_LOGIN","password":"YOUR_PASSWORD","lang":"en"}' \
  --data-urlencode "lang=en"

Response

200 OK
{
  "code": "0",
  "message": "SUCCESS",
  "data": [
    {
      "productId": "42",
      "productName": "Xbox Game Pass Ultimate 1 Month",
      "imageUrl": "https://admin.pay.net.az/uploads/products/xbox-gp.png",
      "thumbnailUrl": "https://admin.pay.net.az/uploads/products/thumb/xbox-gp.png"
    },
    {
      "productId": "58",
      "productName": "Steam Wallet $50",
      "imageUrl": "https://admin.pay.net.az/uploads/products/steam-50.png",
      "thumbnailUrl": null
    }
  ]
}

Error Codes

Коды ошибок

CodeMessage
1-1Failed to retrieve products

GET  Action 2 — Get Packages Получить пакеты

Returns available packages for a specific product, with prices in the requested currency.

Возвращает доступные пакеты для конкретного продукта с ценами в запрошенной валюте.

GET /public/json/web.service.2024.php?action=2

Data Parameter

FieldTypeDescription
product* string Product ID from Action 1

Request

cURL
curl -G "https://admin.pay.net.az/public/json/web.service.2024.php" \
  --data-urlencode "action=2" \
  --data-urlencode 'settings={"login":"YOUR_LOGIN","password":"YOUR_PASSWORD","lang":"en"}' \
  --data-urlencode 'data={"product":"42"}' \
  --data-urlencode "currency=AZN"

Response

200 OK
{
  "code": "0",
  "message": "SUCCESS",
  "data": [
    {
      "packageId": "101",
      "packageName": "Xbox Game Pass Ultimate 1 Month (TR)",
      "packagePrice": "25.50",
      "currency": "AZN"
    }
  ]
}

Error Codes

CodeMessage
2-1Invalid JSON in 'data' parameter
2-2Missing 'product' in data
2-3Invalid Product ID
2-9System error

GET  Action 3 — Reserve Key Зарезервировать ключ

Reserves a digital key from a package. The key is locked for 10 minutes. Must be confirmed (Action 4) or cancelled (Action 5) within this time.

Резервирует цифровой ключ из пакета. Ключ блокируется на 10 минут. Необходимо подтвердить (Action 4) или отменить (Action 5) в течение этого времени.

Important: Reserved keys are automatically released after 10 minutes if not confirmed. Always confirm or cancel reservations promptly. Важно: Зарезервированные ключи автоматически освобождаются через 10 минут. Всегда подтверждайте или отменяйте резервации своевременно.
GET /public/json/web.service.2024.php?action=3

Data Parameter

FieldTypeDescription
package* string Package ID from Action 2

Request

cURL
curl -G "https://admin.pay.net.az/public/json/web.service.2024.php" \
  --data-urlencode "action=3" \
  --data-urlencode 'settings={"login":"YOUR_LOGIN","password":"YOUR_PASSWORD","lang":"en"}' \
  --data-urlencode 'data={"package":"101"}' \
  --data-urlencode "currency=AZN"

Response

200 OK
{
  "code": "0",
  "message": "SUCCESS",
  "data": {
    "keyId": "8847",
    "keyText": "XXXX-YYYY-ZZZZ-WWWW",
    "serialNumber": "SN-12345",
    "price": "25.50",
    "currency": "AZN"
  }
}

Error Codes

CodeMessage
3-1Invalid JSON in 'data' parameter
3-2Missing 'package' in data
3-3Package not found or inactive
3-4No available keys in stock
3-5Key reservation failed

GET  Action 4 — Confirm Sale Подтвердить продажу

Confirms the purchase of a previously reserved key. Creates an order record and marks the key as sold. The package auto-deactivates if this was the last key.

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

Idempotency & refund awareness: calling action=4 again for the same key returns SUCCESS with status: "confirmed" instead of an error. If the key has been refunded in KeyForge, the response is SUCCESS with status: "cancelled", reason: "refunded" — treat the operation as reversed on your side. Use Action 7 for dedicated status polling. Идемпотентность и возвраты: повторный action=4 на том же ключе вернёт SUCCESS со status: "confirmed" вместо ошибки. Если ключ был возвращён в KeyForge — ответ будет SUCCESS со status: "cancelled", reason: "refunded"; пометьте у себя операцию как отменённую. Для регулярной проверки статуса используйте Action 7.
GET /public/json/web.service.2024.php?action=4

Data Parameter

FieldTypeDescription
key*stringKey ID from Action 3
order*stringYour order ID (max 200 chars)
currencystringCurrency code (default: AZN)

Request

cURL
curl -G "https://admin.pay.net.az/public/json/web.service.2024.php" \
  --data-urlencode "action=4" \
  --data-urlencode 'settings={"login":"YOUR_LOGIN","password":"YOUR_PASSWORD","lang":"en"}' \
  --data-urlencode 'data={"key":"8847","order":"PAY-2026-001","currency":"AZN"}'

Response

200 OK
{
  "code": "0",
  "message": "SUCCESS"
}

Error Codes

CodeMessage
4-1Invalid JSON in 'data' parameter
4-2Missing 'key' in data
4-3Missing 'order' in data
4-4Order ID exceeds 200 characters
4-5Invalid key ID or key not reserved
4-6Sale confirmation failed

GET  Action 5 — Cancel Reservation Отменить резервацию

Cancels a previously reserved key, returning it to the available pool.

Отменяет резервацию ключа, возвращая его в доступные.

GET /public/json/web.service.2024.php?action=5

Data Parameter

FieldTypeDescription
key*stringKey ID from Action 3

Request

cURL
curl -G "https://admin.pay.net.az/public/json/web.service.2024.php" \
  --data-urlencode "action=5" \
  --data-urlencode 'settings={"login":"YOUR_LOGIN","password":"YOUR_PASSWORD","lang":"en"}' \
  --data-urlencode 'data={"key":"8847"}'

Response

200 OK
{
  "code": "0",
  "message": "SUCCESS"
}

Error Codes

CodeMessage
5-1Invalid JSON in 'data' parameter
5-2Missing 'key' in data
5-3Invalid key ID or key not reserved
5-4Cancellation failed

GET  Action 6 — Direct Purchase (One-Step) Прямая покупка (один шаг)

Combines reservation and confirmation in a single call. Optionally sends the key to the customer via email with activation instructions. Auto-deactivates the package when the last key is sold.

Объединяет резервацию и подтверждение в одном вызове. Опционально отправляет ключ клиенту по email с инструкцией активации. Автоматически деактивирует пакет при продаже последнего ключа.

Recommended: Use Action 6 for simple integrations. It handles the entire purchase flow in one request, including optional email delivery to end customers. Рекомендуется: Используйте Action 6 для простых интеграций. Он обрабатывает весь процесс покупки в одном запросе, включая опциональную email-доставку клиенту.
GET /public/json/web.service.2024.php?action=6

Data Parameter

FieldTypeDescription
package*stringPackage ID
order*stringYour order ID (max 200 chars)
emailstringCustomer email — key will be sent with activation instructions
langstringEmail language (en/ru/az)

Request

cURL
curl -G "https://admin.pay.net.az/public/json/web.service.2024.php" \
  --data-urlencode "action=6" \
  --data-urlencode 'settings={"login":"YOUR_LOGIN","password":"YOUR_PASSWORD","lang":"en"}' \
  --data-urlencode 'data={"package":"101","order":"PAY-2026-002","email":"customer@example.com","lang":"en"}' \
  --data-urlencode "currency=AZN"

Response

200 OK
{
  "code": "0",
  "message": "SUCCESS",
  "data": {
    "keyId": "8848",
    "keyText": "AAAA-BBBB-CCCC-DDDD",
    "serialNumber": "SN-67890"
  }
}

Error Codes

CodeMessage
6-1Invalid JSON in 'data' parameter
6-2Missing 'package' in data
6-3Missing 'order' in data
6-4Package not found or inactive
6-5No available keys in stock
6-6Email delivery failed (key still sold)

GET  Action 7 — Get Order Status Статус заказа

Look up the current status of a previously-created order by your own client_order_id. Use this to detect refunds / cancellations that happened on the KeyForge side after the original sale.

Получение актуального статуса ранее созданного заказа по вашему client_order_id. Используйте, чтобы узнать про возвраты / отмены, выполненные на стороне KeyForge уже после выдачи ключа.

When to call: once after you receive a successful action=6/action=4 response, you may periodically poll this endpoint (e.g. every 15 min for the first 24h) to catch status changes like refunds. If status: "cancelled" — mark the operation as failed/reversed on your side. Когда вызывать: после успешного action=6/action=4 можно периодически (например раз в 15 минут в течение 24 часов) опрашивать этот endpoint, чтобы поймать изменения статуса, такие как возвраты. Если пришёл status: "cancelled" — у себя помечайте операцию как отменённую/ошибочную.
GET /public/json/web.service.2024.php?action=7

Data Parameter

FieldTypeDescription
order*stringYour client_order_id from the original Action 3 / Action 6 call

Request

cURL
curl -G "https://admin.pay.net.az/public/json/web.service.2024.php" \
  --data-urlencode "action=7" \
  --data-urlencode 'settings={"login":"YOUR_LOGIN","password":"YOUR_PASSWORD"}' \
  --data-urlencode 'data={"order":"PAY-2026-002"}'

Response — confirmed (ключ выдан)

200 OK
{
  "code": "0",
  "message": "SUCCESS",
  "data": {
    "orderId": "ORD-2026-45031507",
    "clientOrderId": "PAY-2026-002",
    "status": "confirmed",
    "amount": "7.00",
    "currency": "AZN",
    "keyId": "7251",
    "keyText": "DBVC-26RE-2X1P-46B3",
    "serialNumber": "SN-67890",
    "createdAt": "2026-04-23T11:50:31.525Z",
    "updatedAt": "2026-04-23T11:50:31.525Z"
  }
}

Response — cancelled (возврат / отмена)

200 OK
{
  "code": "0",
  "message": "SUCCESS",
  "data": {
    "orderId": "ORD-2026-45031507",
    "clientOrderId": "PAY-2026-002",
    "status": "cancelled",
    "amount": "7.00",
    "currency": "AZN",
    "updatedAt": "2026-04-23T12:10:05.118Z"
  }
}

Status values

StatusMeaning
confirmedKey delivered, order is closed successfully
cancelledOrder was cancelled OR the delivered key was later refunded. Treat as failed/reversed on your side.
pendingReserved but not yet confirmed (rare — usually transient)

Error Codes

CodeMessage
7-1Invalid JSON in 'data' parameter
7-2Missing 'order' in data
7-3Order not found
7-4Order status lookup failed (internal error)

Purchase Flow Поток покупки

Standard Flow (Two-Step)

Стандартный поток (два шага)

For payment terminals that need to show the price before charging the customer.

Для платёжных терминалов, которым нужно показать цену до списания средств.

1
Action 1 → Products
2
Action 2 → Packages
3
Action 3 → Reserve
4
Payment
5
Action 4 → Confirm

Express Flow (One-Step)

Экспресс-поток (один шаг)

For online stores where payment is already completed.

Для интернет-магазинов, где оплата уже произведена.

1
Action 1 → Products
2
Action 2 → Packages
3
Payment
4
Action 6 → Buy + Email
Tip: If a reservation expires (10 min timeout) and the customer has already paid, use Action 6 to complete the purchase immediately instead of starting a new reserve-confirm cycle. Совет: Если резервация истекла (10 мин), а клиент уже оплатил, используйте Action 6 для завершения покупки вместо нового цикла резервации.

REST API v2 LATEST

Resource-oriented API for partner integrations: real HTTP verbs and status codes, Bearer authentication, no credentials in the URL. Built on the same sales engine as the Legacy API — both behave identically.

Ресурсный API для партнёрских интеграций: настоящие HTTP-глаголы и статусы, аутентификация по Bearer-токену, никаких секретов в URL. Работает поверх того же ядра продаж, что и Legacy API, — поведение полностью совпадает.

Base URL: https://api.pay.net.az/api/v2

Базовый URL: https://api.pay.net.az/api/v2

Why v2

Зачем v2

Legacy API passes login and password as query parameters. Such URLs end up in web server logs, proxy history and Referer headers. v2 removes that: the secret travels in the Authorization header only. Everything else — prices, reservations, idempotency — works exactly as in v1.

Legacy API передаёт login и password в query-строке. Такие URL оседают в логах веб-сервера, в истории прокси и в заголовке Referer. v2 это убирает: секрет уходит только в заголовке Authorization. Всё остальное — цены, брони, идемпотентность — работает ровно так же, как в v1.

Response Format

Формат ответа

Success — collection
{
  "data": [ ... ],
  "pagination": {
    "total": 148,
    "limit": 50,
    "offset": 0
  }
}
Error
{
  "error": {
    "code": "OUT_OF_STOCK",
    "message": "There is no any free key for this package"
  }
}

Always branch on error.code, never on message — messages may be reworded, codes are part of the contract.

Ветвитесь по error.code, а не по message: формулировки могут меняться, коды — часть контракта.

Authentication Аутентификация

Every request carries API credentials in the Authorization header. The separator is the first dot — the secret itself may contain dots.

Каждый запрос несёт API-креденшелы в заголовке Authorization. Разделитель — первая точка: сам секрет может содержать точки.

Request
curl https://api.pay.net.az/api/v2/products \
  -H "Authorization: Bearer 3f9a…c1.7d2e…b4"

Credentials are issued by a KeyForge administrator. The secret is shown once; if lost, it can only be rotated. Each credential supports an IP whitelist and an expiry date.

Креденшелы выдаёт администратор KeyForge. Секрет показывается один раз; при утере его можно только заменить (ротация). У каждой учётки поддерживаются IP-whitelist и срок действия.

Authentication errors

Ошибки аутентификации

HTTPcodeПричина / Reason
401UNAUTHORIZEDНет заголовка Authorization / header missing
401INVALID_CREDENTIALSНеверный ключ или секрет / bad key or secret
403CREDENTIALS_DISABLEDУчётка отключена / credential disabled
403CREDENTIALS_EXPIREDИстёк срок действия / expired
403PARTNER_BLOCKEDПартнёр заблокирован / partner blocked
403IP_NOT_ALLOWEDАдрес не в whitelist / IP not whitelisted

Account Аккаунт

GET /api/v2/me Profile, currencies and balance
Response
{
  "partner": {
    "id": 5,
    "login": "paynet",
    "name": "PayNet Processing",
    "tier": "gold",
    "sandbox": false
  },
  "currency": {
    "mode": "MULTI_CURRENCY",
    "default": "AZN",
    "allowed": ["AZN", "USD"],
    "active": "AZN"
  },
  "balance": {
    "mode": "postpaid",
    "amount": -120.50,
    "creditLimit": 5000,
    "currency": "AZN",
    "available": 4879.50
  }
}

balance is null when balance control is not enabled for your account — the number would be meaningless as it affects nothing.

balance равен null, если контроль баланса для вашей учётки не включён: цифра ни на что не влияла бы и только вводила в заблуждение.

Catalog Каталог

GET /api/v2/products Products available to you
ПараметрТипОписание
langqueryaz · ru · en — язык поля name. Полный набор переводов приходит всегда в names.
Response
{
  "data": [
    {
      "id": 12,
      "name": "PlayStation Plus",
      "names": {
        "az": "PlayStation Plus",
        "ru": "PlayStation Plus",
        "en": "PlayStation Plus"
      },
      "imageUrl": "https://admin.pay.net.az/uploads/products/ps.jpg"
    }
  ]
}
GET /api/v2/products/{productId}/packages Packages with prices and stock

Packages with no keys in stock are omitted. Prices are already converted to your active currency — add ?currency=USD to switch (multi-currency accounts only).

Пакеты без доступных ключей не возвращаются. Цены уже пересчитаны в вашу активную валюту — добавьте ?currency=USD, чтобы сменить (только для мультивалютных учёток).

Response
{
  "data": [
    {
      "id": 104,
      "name": "PS Plus 3 месяца",
      "names": { "az": "…", "ru": "…", "en": "…" },
      "price": 25.00,
      "currency": "AZN",
      "available": 37
    }
  ]
}

Reservations Брони

Reservations expire. A reserved key is held for a limited time (30 minutes by default) and then returns to stock automatically. The deadline is in expiresAt. After it passes, confirmation fails — subscribe to the reservation.expired webhook to learn about it without polling.

Бронь истекает. Ключ удерживается ограниченное время (по умолчанию 30 минут), затем автоматически возвращается в продажу. Дедлайн — в поле expiresAt. После него подтверждение не пройдёт; подпишитесь на вебхук reservation.expired, чтобы узнавать об этом без опроса.

POST /api/v2/reservations Reserve a key (quote is locked)
Request Body
{
  "packageId": 104,
  "orderId": "PAY-100500"
}
201 Created
{
  "reservation": {
    "keyId": 50231,
    "keyText": "XXXX-YYYY-ZZZZ",
    "price": 25.00,
    "currency": "AZN",
    "expiresAt": "2026-07-31T15:42:00.000Z",
    "clientOrderId": "PAY-100500"
  }
}

The quoted price is locked. You are charged exactly price at confirmation, even if exchange rates or the price list change in between.

Названная цена фиксируется. При подтверждении спишется ровно price, даже если между вызовами изменились курсы или прайс.

Repeating the call with the same orderId returns 200 with the original reservation and "duplicate": true — never a second key.

Повтор с тем же orderId вернёт 200 с исходной бронью и признаком "duplicate": true — второй ключ не выдаётся.

POST /api/v2/reservations/{keyId}/confirm Confirm the sale
Request Body
{ "orderId": "PAY-100500" }
200 OK
{
  "order": {
    "orderId": "PAY-100500",
    "clientOrderId": "PAY-100500",
    "status": "confirmed",
    "keyId": 50231,
    "keyText": "XXXX-YYYY-ZZZZ",
    "amount": 25.00,
    "currency": "AZN"
  }
}

Confirming twice is safe: the second call returns the same order with "duplicate": true.

Повторное подтверждение безопасно: второй вызов вернёт тот же заказ с признаком "duplicate": true.

DELETE /api/v2/reservations/{keyId} Release the key back to stock

Returns 204 No Content. Also returns 204 if the reservation had already expired — from your side the outcome is the same: the key is not yours.

Возвращает 204 No Content. Тот же 204 придёт, если бронь уже истекла — для вас результат идентичен: ключ не за вами.

Orders Заказы

POST /api/v2/orders Direct purchase — reserve and confirm in one call
Request Body
{
  "packageId": 104,
  "orderId": "PAY-100501",
  "customerEmail": "buyer@example.com"
}

customerEmail is optional. If your account is allowed to deliver keys by email, the key is sent to that address with activation instructions.

customerEmail опционален. Если вашей учётке разрешена доставка ключей на email, ключ уйдёт на этот адрес вместе с инструкцией по активации.

201 Created
{
  "order": {
    "orderId": "ORD-2026-45120033",
    "clientOrderId": "PAY-100501",
    "status": "confirmed",
    "keyId": 50232,
    "keyText": "AAAA-BBBB-CCCC",
    "amount": 25.00,
    "currency": "AZN"
  }
}
GET /api/v2/orders Your orders, filtered and paginated
ПараметрОписание
statuspending · confirmed · cancelled · refunded
from, toДиапазон дат, YYYY-MM-DD
limit, offsetПо умолчанию 50 / 0; максимум limit — 200
GET /api/v2/orders/{orderNo} Order status by your own number

Accepts both your own order number and the KeyForge internal one. keyText is returned only for confirmed orders. A refunded key yields status: "cancelled" even if the order itself still reads as confirmed.

Принимает и ваш номер заказа, и внутренний номер KeyForge. keyText отдаётся только для confirmed. Возвращённый ключ даёт status: "cancelled", даже если сам заказ ещё числится подтверждённым.

Error codes Коды ошибок

HTTPcodeЧто делать / What to do
400BAD_REQUESTНе хватает обязательного поля — исправить запрос
402INSUFFICIENT_FUNDSНе хватает баланса или превышен кредитный лимит. Бронь сохранена — пополните и подтвердите снова
404PACKAGE_NOT_FOUNDПакет не существует либо недоступен вашей учётке
404KEY_NOT_FOUNDБронь не найдена или принадлежит другому партнёру
404ORDER_NOT_FOUNDЗаказ с таким номером не найден
409OUT_OF_STOCKСвободных ключей нет — повторять бессмысленно, проверьте остатки
409KEY_WRONG_STATEБронь уже подтверждена, отменена или истекла
409REFUNDEDПо заказу оформлен возврат
503VENDOR_UNAVAILABLEПоставщик недоступен — можно повторить позже
503VENDOR_UNKNOWN_OUTCOMEНе повторять. Исход операции у поставщика неизвестен; обратитесь в поддержку

Never retry on VENDOR_UNKNOWN_OUTCOME. It means the vendor request timed out and the key may have been issued and paid for. A retry can buy a second key. Contact support instead.

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

Sandbox Песочница

Ask an administrator to switch your account into sandbox mode. Everything behaves as in production — same endpoints, same error codes, same idempotency — but keys are synthetic (SANDBOX-…), stock is untouched and nothing reaches revenue. Responses carry "sandbox": true.

Попросите администратора перевести вашу учётку в режим песочницы. Всё ведёт себя как в бою — те же эндпоинты, те же коды ошибок, та же идемпотентность, — но ключи синтетические (SANDBOX-…), склад не затрагивается и в выручку ничего не попадает. В ответах присутствует "sandbox": true.

Triggering failures on purpose

Управляемые отказы

You cannot make a live catalog run out of stock, yet error handling must be tested. Prefix your order number to force an outcome:

Заставить живой каталог «закончиться» невозможно, а обработку отказов проверять надо. Префикс в номере заказа задаёт исход:

Префикс orderIdРезультат
FAIL_NO_KEYS…Нет свободных ключей
FAIL_PACKAGE…Пакет не найден
FAIL_SYSTEM…Системная ошибка
FAIL_TIMEOUT…Неизвестный исход у поставщика

The prefix only triggers at the start of the number, so a legitimate order such as ORDER-FAIL_SYSTEM-1 is unaffected.

Префикс срабатывает только в начале номера, поэтому легитимный заказ вида ORDER-FAIL_SYSTEM-1 ничего не ломает.

Migrating from Legacy v1 Переход с Legacy v1

Legacy v1REST v2
action=1 GetProductsGET /api/v2/products
action=2 GetPackageGET /api/v2/products/{id}/packages
action=3 CreateOrderPOST /api/v2/reservations
action=4 Confirm SalePOST /api/v2/reservations/{keyId}/confirm
action=5 Cancel reservationDELETE /api/v2/reservations/{keyId}
action=6 Direct PurchasePOST /api/v2/orders
action=7 GetOrderStatusGET /api/v2/orders/{orderNo}

Legacy v1 is not going away. Both APIs run on the same engine and can be used side by side — migrate one environment at a time. There is no shutdown date for v1.

Legacy v1 никуда не денется. Оба API работают на одном ядре и могут использоваться параллельно — переводите контуры по одному. Дата отключения v1 не назначена.

If you cannot move off v1 yet, you can still drop credentials from the URL today: the legacy endpoint accepts the same Authorization: Bearer header, and then settings becomes optional.

Если перейти на v2 пока нельзя, убрать секреты из URL можно уже сегодня: legacy-эндпоинт принимает тот же заголовок Authorization: Bearer, и параметр settings становится необязательным.

Email Integration Email интеграция

How to send digital keys to customers via email using Action 6.

Как отправлять цифровые ключи клиентам по email через Action 6.

When you include the email parameter in Action 6, KeyForge automatically sends a beautifully formatted email to the customer containing:

Когда вы включаете параметр email в Action 6, KeyForge автоматически отправляет красиво оформленное письмо клиенту, содержащее:

  • Product name and image
  • Название и изображение продукта
  • Digital key (PIN code)
  • Цифровой ключ (PIN-код)
  • Serial number (if applicable)
  • Серийный номер (если есть)
  • Product-specific activation instructions
  • Инструкции по активации для конкретного продукта
  • Partner branding (logo, support email)
  • Брендинг партнёра (логотип, email поддержки)
Emails are sent with delivery tracking. The email language is determined by the lang parameter (en/ru/az). Письма отправляются с отслеживанием доставки. Язык письма определяется параметром lang (en/ru/az).
Example: Action 6 with Email
// Send key + email to customer in Russian
GET /public/json/web.service.2024.php
  ?action=6
  &settings={"login":"YOUR_LOGIN","password":"YOUR_PASSWORD","lang":"ru"}
  &data={"package":"101","order":"ORD-001","email":"user@mail.com","lang":"ru"}

Webhooks Вебхуки

Get notified about what happens to your orders — instead of polling for it.

Узнавайте о событиях по вашим заказам — вместо того чтобы опрашивать нас.

Order webhooks

Вебхуки по заказам

An administrator registers your HTTPS endpoint and issues a signing secret. KeyForge then pushes events to you. This is the only way to learn about a refund or an expired reservation without calling action=7 in a loop.

Администратор регистрирует ваш HTTPS-эндпоинт и выдаёт секрет подписи. После этого KeyForge сам присылает события. Это единственный способ узнать о возврате или истёкшей брони, не опрашивая action=7 по кругу.

Событие / EventКогда / When
order.confirmedПродажа подтверждена / sale confirmed
order.refundedОформлен возврат / refund issued
order.cancelledЗаказ отменён / order cancelled
reservation.expiredБронь снята по таймауту / reservation released
key.deliveredКлюч отправлен клиенту на email / key emailed to customer
Request body
{
  "event_id": "8f14e45f-ea0f-4b2c-9c1a-1d2f3a4b5c6d",
  "event_type": "order.refunded",
  "created_at": "2026-07-31T13:45:12.345Z",
  "data": { "order_id": "PAY-100500", "amount": "25.00", "currency": "AZN" }
}

Verifying the signature

Проверка подписи

Header X-KeyForge-Signature: t=<timestamp>,v1=<hmac>. The HMAC-SHA256 is computed over the string "<timestamp>.<raw body>" using your secret. Compute it over the raw body, before JSON parsing: re-serialising changes the bytes and the signature will not match. Reject requests whose timestamp differs from now by more than a few minutes — that is what protects you from replay.

Заголовок X-KeyForge-Signature: t=<timestamp>,v1=<hmac>. HMAC-SHA256 считается от строки "<timestamp>.<сырое тело>" на вашем секрете. Считайте от СЫРОГО тела, до разбора JSON: повторная сериализация меняет байты, и подпись не сойдётся. Отвергайте запросы, у которых timestamp расходится с текущим временем больше чем на несколько минут — именно это защищает от переигрывания перехваченного запроса.

Node.js
const crypto = require('crypto');

function verify(rawBody, header, secret, tolerance = 300) {
  const p = Object.fromEntries(header.split(',').map(x => x.split('=')));
  const ts = Number(p.t);
  if (!Number.isFinite(ts) || Math.abs(Date.now()/1000 - ts) > tolerance) return false;

  const expected = crypto.createHmac('sha256', secret)
    .update(`${ts}.${rawBody}`).digest('hex');

  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(p.v1));
}

Delivery and retries

Доставка и повторы

Any 2xx counts as success. On any other response or a network error the delivery is retried: 1 min → 5 min → 15 min → 1 h → 6 h → 24 h. Delivery is at-least-once — the same event may arrive twice, so deduplicate by event_id. Answer 200 before heavy processing: if you hold the response longer than 10 seconds we treat it as a failure and retry.

Успех — любой 2xx. При другом ответе или сетевой ошибке доставка повторяется: 1 мин → 5 мин → 15 мин → 1 ч → 6 ч → 24 ч. Доставка at-least-once — одно и то же событие может прийти дважды, дедуплицируйте по event_id. Отвечайте 200 до тяжёлой обработки: если держите ответ дольше 10 секунд, мы считаем доставку неудачной и повторяем её.

Email delivery events

События доставки email

KeyForge tracks email delivery status internally. The following events are surfaced in the admin panel (Communications → Delivery log):

KeyForge отслеживает статус доставки email внутренне. Следующие события доступны в админ-панели (Коммуникации → Журнал отправок):

EventDescription
email.sentEmail accepted by provider
email.deliveredEmail delivered to inbox
email.openedRecipient opened the email
email.clickedRecipient clicked a link
email.bouncedEmail bounced (invalid address)
email.complainedRecipient marked as spam