KeyForge Developer API
KeyForge API для разработчиков
Integrate digital key distribution into your platform. Two API versions, one powerful engine.
Интегрируйте дистрибуцию цифровых ключей в свою платформу. Два API — один мощный движок.
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 ActionsREST 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 EndpointsAuthentication Аутентификация
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-параметрами.
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 выше.
GET /public/json/web.service.2024.php
?action=1
&settings={"login":"your_login","password":"your_password","lang":"en"}
| Field | Type | Description | Описание |
|---|---|---|---|
| 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: Bearer eyJhbGciOiJIUzI1NiIs...
Base URLs Базовые URL
https://admin.pay.net.az/api/v1/partners/servicehttps://admin.pay.net.az/public/json/web.service.2024.phphttps://admin.pay.net.az/apiError 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" — успех. Любой другой код — ошибка.
{
"code": "0-3",
"message": "PARTNER_INACTIVE"
}
Common Error Codes (prefix 0-X)
Общие коды ошибок (префикс 0-X)
| Code | Message | Description | Описание |
|---|---|---|---|
| 0-1 | INVALID_REQUEST | Missing required query parameters | Отсутствуют обязательные параметры |
| 0-2 | LOGIN_MISSING | Missing 'login' in settings | Отсутствует 'login' в settings |
| 0-3 | PASSWORD_MISSING | Missing 'password' in settings | Отсутствует 'password' в settings |
| 0-10 | ACCESS_DENIED | Invalid login or password | Неверный логин или пароль |
| 0-11 | PARTNER_BLOCKED | Partner account is blocked | Аккаунт партнёра заблокирован |
| 0-7 | INVALID_CURRENCY | Unsupported currency code | Неподдерживаемый код валюты |
| 0 | SUCCESS | Request 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 (сообщение).
{
"success": false,
"error": "Unauthorized: Invalid or expired token"
}
Legacy API v1
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
Формат запроса
GET /public/json/web.service.2024.php
?action={1-7}
&settings={"login":"...","password":"...","lang":"en","currency":"AZN"}
&data={url_encoded_json}
| Parameter | Type | Description | Описание |
|---|---|---|---|
| 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.
Возвращает каталог доступных продуктов для партнёра. Продукты фильтруются по правам доступа партнёра.
Request
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
{
"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
Коды ошибок
| Code | Message |
|---|---|
| 1-1 | Failed to retrieve products |
GET Action 2 — Get Packages Получить пакеты
Returns available packages for a specific product, with prices in the requested currency.
Возвращает доступные пакеты для конкретного продукта с ценами в запрошенной валюте.
Data Parameter
| Field | Type | Description |
|---|---|---|
| product* | string | Product ID from Action 1 |
Request
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
{
"code": "0",
"message": "SUCCESS",
"data": [
{
"packageId": "101",
"packageName": "Xbox Game Pass Ultimate 1 Month (TR)",
"packagePrice": "25.50",
"currency": "AZN"
}
]
}
Error Codes
| Code | Message |
|---|---|
| 2-1 | Invalid JSON in 'data' parameter |
| 2-2 | Missing 'product' in data |
| 2-3 | Invalid Product ID |
| 2-9 | System 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) в течение этого времени.
Data Parameter
| Field | Type | Description |
|---|---|---|
| package* | string | Package ID from Action 2 |
Request
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
{
"code": "0",
"message": "SUCCESS",
"data": {
"keyId": "8847",
"keyText": "XXXX-YYYY-ZZZZ-WWWW",
"serialNumber": "SN-12345",
"price": "25.50",
"currency": "AZN"
}
}
Error Codes
| Code | Message |
|---|---|
| 3-1 | Invalid JSON in 'data' parameter |
| 3-2 | Missing 'package' in data |
| 3-3 | Package not found or inactive |
| 3-4 | No available keys in stock |
| 3-5 | Key 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.
Подтверждает покупку зарезервированного ключа. Создаёт запись заказа и помечает ключ как проданный. Пакет автоматически деактивируется если это был последний ключ.
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.
Data Parameter
| Field | Type | Description |
|---|---|---|
| key* | string | Key ID from Action 3 |
| order* | string | Your order ID (max 200 chars) |
| currency | string | Currency code (default: AZN) |
Request
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
{
"code": "0",
"message": "SUCCESS"
}
Error Codes
| Code | Message |
|---|---|
| 4-1 | Invalid JSON in 'data' parameter |
| 4-2 | Missing 'key' in data |
| 4-3 | Missing 'order' in data |
| 4-4 | Order ID exceeds 200 characters |
| 4-5 | Invalid key ID or key not reserved |
| 4-6 | Sale confirmation failed |
GET Action 5 — Cancel Reservation Отменить резервацию
Cancels a previously reserved key, returning it to the available pool.
Отменяет резервацию ключа, возвращая его в доступные.
Data Parameter
| Field | Type | Description |
|---|---|---|
| key* | string | Key ID from Action 3 |
Request
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
{
"code": "0",
"message": "SUCCESS"
}
Error Codes
| Code | Message |
|---|---|
| 5-1 | Invalid JSON in 'data' parameter |
| 5-2 | Missing 'key' in data |
| 5-3 | Invalid key ID or key not reserved |
| 5-4 | Cancellation 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 с инструкцией активации. Автоматически деактивирует пакет при продаже последнего ключа.
Data Parameter
| Field | Type | Description |
|---|---|---|
| package* | string | Package ID |
| order* | string | Your order ID (max 200 chars) |
| string | Customer email — key will be sent with activation instructions | |
| lang | string | Email language (en/ru/az) |
Request
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
{
"code": "0",
"message": "SUCCESS",
"data": {
"keyId": "8848",
"keyText": "AAAA-BBBB-CCCC-DDDD",
"serialNumber": "SN-67890"
}
}
Error Codes
| Code | Message |
|---|---|
| 6-1 | Invalid JSON in 'data' parameter |
| 6-2 | Missing 'package' in data |
| 6-3 | Missing 'order' in data |
| 6-4 | Package not found or inactive |
| 6-5 | No available keys in stock |
| 6-6 | Email 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 уже после выдачи ключа.
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" — у себя помечайте операцию как отменённую/ошибочную.
Data Parameter
| Field | Type | Description |
|---|---|---|
| order* | string | Your client_order_id from the original Action 3 / Action 6 call |
Request
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 (ключ выдан)
{
"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 (возврат / отмена)
{
"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
| Status | Meaning |
|---|---|
confirmed | Key delivered, order is closed successfully |
cancelled | Order was cancelled OR the delivered key was later refunded. Treat as failed/reversed on your side. |
pending | Reserved but not yet confirmed (rare — usually transient) |
Error Codes
| Code | Message |
|---|---|
| 7-1 | Invalid JSON in 'data' parameter |
| 7-2 | Missing 'order' in data |
| 7-3 | Order not found |
| 7-4 | Order status lookup failed (internal error) |
Purchase Flow Поток покупки
Standard Flow (Two-Step)
Стандартный поток (два шага)
For payment terminals that need to show the price before charging the customer.
Для платёжных терминалов, которым нужно показать цену до списания средств.
Express Flow (One-Step)
Экспресс-поток (один шаг)
For online stores where payment is already completed.
Для интернет-магазинов, где оплата уже произведена.
REST API v2
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
Формат ответа
{
"data": [ ... ],
"pagination": {
"total": 148,
"limit": 50,
"offset": 0
}
}
{
"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. Разделитель — первая точка: сам секрет может содержать точки.
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
Ошибки аутентификации
| HTTP | code | Причина / Reason |
|---|---|---|
| 401 | UNAUTHORIZED | Нет заголовка Authorization / header missing |
| 401 | INVALID_CREDENTIALS | Неверный ключ или секрет / bad key or secret |
| 403 | CREDENTIALS_DISABLED | Учётка отключена / credential disabled |
| 403 | CREDENTIALS_EXPIRED | Истёк срок действия / expired |
| 403 | PARTNER_BLOCKED | Партнёр заблокирован / partner blocked |
| 403 | IP_NOT_ALLOWED | Адрес не в whitelist / IP not whitelisted |
Account Аккаунт
{
"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 Каталог
| Параметр | Тип | Описание |
|---|---|---|
lang | query | az · ru · en — язык поля name. Полный набор переводов приходит всегда в names. |
{
"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"
}
]
}
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, чтобы сменить (только для мультивалютных учёток).
{
"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, чтобы узнавать об этом без опроса.
{
"packageId": 104,
"orderId": "PAY-100500"
}
{
"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 — второй ключ не выдаётся.
{ "orderId": "PAY-100500" }
{
"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.
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 Заказы
{
"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, ключ уйдёт на этот адрес вместе с инструкцией по активации.
{
"order": {
"orderId": "ORD-2026-45120033",
"clientOrderId": "PAY-100501",
"status": "confirmed",
"keyId": 50232,
"keyText": "AAAA-BBBB-CCCC",
"amount": 25.00,
"currency": "AZN"
}
}
| Параметр | Описание |
|---|---|
status | pending · confirmed · cancelled · refunded |
from, to | Диапазон дат, YYYY-MM-DD |
limit, offset | По умолчанию 50 / 0; максимум limit — 200 |
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 Коды ошибок
| HTTP | code | Что делать / What to do |
|---|---|---|
| 400 | BAD_REQUEST | Не хватает обязательного поля — исправить запрос |
| 402 | INSUFFICIENT_FUNDS | Не хватает баланса или превышен кредитный лимит. Бронь сохранена — пополните и подтвердите снова |
| 404 | PACKAGE_NOT_FOUND | Пакет не существует либо недоступен вашей учётке |
| 404 | KEY_NOT_FOUND | Бронь не найдена или принадлежит другому партнёру |
| 404 | ORDER_NOT_FOUND | Заказ с таким номером не найден |
| 409 | OUT_OF_STOCK | Свободных ключей нет — повторять бессмысленно, проверьте остатки |
| 409 | KEY_WRONG_STATE | Бронь уже подтверждена, отменена или истекла |
| 409 | REFUNDED | По заказу оформлен возврат |
| 503 | VENDOR_UNAVAILABLE | Поставщик недоступен — можно повторить позже |
| 503 | VENDOR_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 v1 | REST v2 |
|---|---|
action=1 GetProducts | GET /api/v2/products |
action=2 GetPackage | GET /api/v2/products/{id}/packages |
action=3 CreateOrder | POST /api/v2/reservations |
action=4 Confirm Sale | POST /api/v2/reservations/{keyId}/confirm |
action=5 Cancel reservation | DELETE /api/v2/reservations/{keyId} |
action=6 Direct Purchase | POST /api/v2/orders |
action=7 GetOrderStatus | GET /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 поддержки)
lang parameter (en/ru/az).
Письма отправляются с отслеживанием доставки. Язык письма определяется параметром lang (en/ru/az).
// 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 |
{
"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 расходится с текущим временем больше чем на несколько минут — именно это защищает от переигрывания перехваченного запроса.
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 внутренне. Следующие события доступны в админ-панели (Коммуникации → Журнал отправок):
| Event | Description |
|---|---|
| email.sent | Email accepted by provider |
| email.delivered | Email delivered to inbox |
| email.opened | Recipient opened the email |
| email.clicked | Recipient clicked a link |
| email.bounced | Email bounced (invalid address) |
| email.complained | Recipient marked as spam |