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

Всё, что нужно разработчику для приёма платежей через E-Xezine: создание платежей, вебхуки, возвраты, встроенная форма.

Редакция от 22.09.2026, версия API 2026-09-22

Документация к API версии 2026-09-22. Базовый адрес — https://exezine.az/api/v2, машинное описание — openapi.json. Интерактивная справка по всем методам: открыть.

Как это устроено

  1. Ваш сервер создаёт платёж: POST /payments.
  2. В ответе приходит checkout_urlнаша страница оплаты.
  3. Вы отправляете покупателя на этот адрес (или показываете его в рамке на своём сайте).
  4. Когда оплата завершится, мы присылаем вам вебхук payment.succeeded.
  5. Покупателя мы возвращаем на ваш return_url.

Вы интегрируетесь только с E-Xezine. Какой банк-эквайер обрабатывает платёж — наша внутренняя кухня, и мы можем сменить его в любой момент: адреса, формат ответа и вебхуков при этом не меняются, ваш код ничего не заметит.

Ключи и авторизация

Ключи выдаются в личном кабинете, раздел «API и вебхуки».

КлючВидЧто делает
Боевойexz_live_…Настоящие деньги
Тестовыйexz_test_…Песочница, денег нет

Ключ показывается один раз при создании. Потерялся — выпустите новый, старый отзовите. Передаётся в каждом запросе:

Authorization: Bearer exz_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Тестовый и боевой миры полностью изолированы: тестовым ключом боевой платёж не найдётся (вернётся 404), и наоборот.

Ключ — это доступ к деньгам. Он живёт только на вашем сервере. Никогда не кладите его в код сайта, в мобильное приложение и в репозиторий.

Общие правила

Суммы — целые числа в минимальных единицах валюты. 10 манат = 1000, 25.50 = 2550. Дробных чисел в API нет вообще — так нельзя ошибиться на копейку из-за округления.

Время — строки RFC 3339: 2026-09-22T18:30:00+04:00. Кодировка — UTF-8, тело запроса — JSON, заголовок Content-Type: application/json. Каждый ответ содержит request_id — назовите его, если пишете в поддержку.

Ошибки

Всегда один и тот же конверт:

{
  "error": {
    "type": "invalid_request_error",
    "code": "amount_invalid",
    "message": "Сумма должна быть целым числом больше нуля",
    "param": "amount"
  },
  "request_id": "req_4f2c9a1b8e0d3a71"
}

Ориентируйтесь на code — он стабилен, а не на message: текст может поменяться и зависит от заголовка Accept-Language: ru | az | en.

HTTPtypeКогда
400invalid_request_errorКривой JSON
401authentication_errorНет ключа, ключ неверный или отозван
403authentication_errorAPI выключен для магазина
404invalid_request_errorОбъект не найден
409invalid_request_errorКонфликт: идемпотентность, неподходящий статус
422invalid_request_errorЗначение поля не подходит
429rate_limit_errorСлишком часто
5xxapi_errorНаша сторона или шлюз

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

По умолчанию 120 запросов в минуту на ключ. В ответе всегда:

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118

При превышении — 429 и Retry-After в секундах. Подождите указанное время и повторите; не долбите в цикле.

Идемпотентность

При создании платежа, возврата или ссылки передавайте заголовок:

Idempotency-Key: любая-ваша-уникальная-строка
  • Тот же ключ и то же тело → вернём первый результат и заголовок Idempotent-Replayed: true. Двойного списания не будет.
  • Тот же ключ, но другое тело → 409 idempotency_conflict.
  • Ключи живут сутки.

Так безопасно повторять запрос после обрыва сети: вы никогда не создадите два платежа вместо одного.

Создать платёж

Создание платежаPOST /api/v2/payments
{
  "amount": 4990,
  "currency": "AZN",
  "order_id": "1024",
  "description": "Заказ №1024",
  "method": "card",
  "return_url": "https://shop.az/thanks",
  "customer": {
    "email": "buyer@example.com",
    "name": "Aysel Məmmədova",
    "phone": "+994501234567",
    "ip": "85.132.1.1",
    "country": "AZ",
    "city": "Baku",
    "address": "Nizami 10",
    "zip": "AZ1000"
  },
  "metadata": { "cart": "17", "source": "mobile" },
  "locale": "az"
}
ПолеОбяз.Описание
amountдаЦелое, минимальные единицы
currencyнетAZN по умолчанию; список подключается нами
order_idнетВаш номер заказа. По нему потом можно искать
descriptionнетДо 200 символов, видно покупателю
methodнетcard (по умолчанию) или sbp
return_urlнетКуда вернуть покупателя. Публичный https://
customer.*нетЧем полнее — тем меньше отказов 3-D Secure
metadataнетДо 20 полей и 4 КБ. Вернём как есть
localeнетaz / ru / en — язык страницы оплаты

Про customer. Банки-эмитенты в 3-D Secure 2 оценивают риск по этим данным. Передавайте хотя бы email, ip и country — это ощутимо уменьшает долю отказов.

Про СБП. method: "sbp" работает только с currency: "RUB". В другой валюте вернётся 422 method_not_available.

Ответ 201

{
  "id": "EXZ-20260922-7FD54BA1D8",
  "object": "payment",
  "livemode": true,
  "status": "pending",
  "amount": 4990,
  "currency": "AZN",
  "amount_refunded": 0,
  "fee": 125,
  "net": 4865,
  "charged": null,
  "order_id": "1024",
  "description": "Заказ №1024",
  "metadata": { "cart": "17" },
  "method": "card",
  "customer": { "email": "buyer@example.com", "name": "Aysel", "phone": "" },
  "card": null,
  "failure": null,
  "checkout_url": "https://exezine.az/checkout/EXZ-20260922-7FD54BA1D8",
  "return_url": "https://shop.az/thanks",
  "source": "api",
  "created_at": "2026-09-22T18:30:00+04:00",
  "paid_at": null,
  "request_id": "req_c0ad2cf33ed31637"
}

id — идентификатор платежа у нас. Сохраните его рядом с заказом.

ПолеЧто значит
livemodetrue — боевой платёж, false — песочница
feeНаша комиссия по этому платежу
netСколько поступит вам после комиссии
chargedЕсли списание в другой валюте — {"amount":…, "currency":"…"}, иначе null
amount_refundedСколько уже возвращено
sourceОткуда платёж: api, link, form, insales
cardПосле оплаты: {"brand":"visa","last4":"4242","country":"AZ"}
paid_atМомент оплаты; до неё — null

Статусы

СтатусЗначение
pendingСоздан, покупатель ещё не заплатил
succeededОплачен
failedОтказ, см. failure
expiredИстёк срок оплаты
refundedВозвращён полностью
partially_refundedВозвращён частично
reviewНа проверке у нас

Финальные: succeeded, failed, expired, refunded, partially_refunded.

Причина отказа

"failure": { "code": "insufficient_funds", "message": "Недостаточно средств на карте" }
codeЧто сказать покупателю
authentication_failedБанк не подтвердил 3-D Secure — попробуйте ещё раз
insufficient_fundsНедостаточно средств
card_expiredИстёк срок действия карты
limit_exceededПревышен лимит по карте
card_declinedБанк отклонил операцию
expiredИстёк срок оплаты, создайте платёж заново

Узнать статус

Статус платежаGET /api/v2/payments/{id}
curl -H "Authorization: Bearer $KEY" \
  https://exezine.az/api/v2/payments/EXZ-20260922-7FD54BA1D8

{id} — наш EXZ-… или ваш order_id (тогда вернётся последний платёж с этим номером).

Опрашивать статус в цикле не нужно и не следует — для этого есть вебхуки. Разумное применение опроса: разовая проверка, когда покупатель вернулся на ваш сайт, и сверка раз в час по «зависшим» заказам.

Список платежей

СписокGET /api/v2/payments
GET /api/v2/payments?limit=25&status=succeeded&order_id=1024
ПараметрОписание
limit1–100, по умолчанию 25
statusЛюбой из статусов выше
order_idВаш номер заказа
from, toДаты YYYY-MM-DD или YYYY-MM-DD HH:MM
starting_afterКурсор следующей страницы
{
  "object": "list",
  "data": [ { "…платёж…" } ],
  "has_more": true,
  "next_cursor": "EXZ-20260922-11E5866313",
  "request_id": "req_…"
}

Пока has_more равен true, передавайте next_cursor в starting_after.

Возврат

ВозвратPOST /api/v2/payments/{id}/refunds
{ "amount": 3000, "reason": "Клиент отказался от товара" }

Без amount — возврат полной суммы остатка.

{
  "object": "refund_result",
  "refund": {
    "id": "ref_12",
    "object": "refund",
    "payment_id": "EXZ-20260922-7FD54BA1D8",
    "amount": 3000,
    "currency": "AZN",
    "status": "succeeded",
    "reason": "Клиент отказался от товара",
    "created_at": "2026-09-22T19:05:00+04:00"
  },
  "payment": { "…обновлённый платёж…" },
  "request_id": "req_…"
}

Список возвратов по платежу: GET /api/v2/payments/{id}/refunds.

КодКогда
payment_not_payableПлатёж не оплачен — возвращать нечего
refund_amount_invalidСумма больше остатка
refund_failedШлюз не принял возврат

Возврат тоже поддерживает Idempotency-Key — используйте его обязательно.

Когда сайта нет или счёт выставляется вручную.

Создание ссылкиPOST /api/v2/links
{ "amount": 12000, "currency": "AZN", "description": "Счёт №77", "max_uses": 3, "expires_at": "2026-10-01" }
ПолеОписание
amountЦелое в минимальных единицах. Не задавать — сумму введёт плательщик
currencyПо умолчанию — валюта магазина
descriptionДо 200 символов
max_usesСколько раз можно заплатить. Не задавать — без ограничения
expires_atПосле этого момента ссылка не работает
typedynamic (по умолчанию) или static
{
  "id": "idP62kFt",
  "object": "payment_link",
  "url": "https://exezine.az/l/idP62kFt",
  "amount": 12000,
  "currency": "AZN",
  "max_uses": 3,
  "uses": 0,
  "expires_at": null,
  "active": true,
  "created_at": "2026-09-22T19:10:00+04:00"
}

Чтение: GET /api/v2/links/{id}.

Вебхуки — главный способ узнать результат

Адрес и события настраиваются в кабинете: «API и вебхуки».

СобытиеКогда
payment.succeededПлатёж оплачен
payment.failedОтказ
payment.expiredИстёк срок оплаты
refund.succeededПрошёл возврат
Тело событияPOST на ваш адрес
{
  "id": "evt_5f0f0c55264b546ecb1a3045",
  "type": "payment.succeeded",
  "api_version": "2026-09-22",
  "livemode": true,
  "created_at": "2026-09-22T18:45:00+04:00",
  "data": { "object": { "…платёж…" } }
}

У refund.succeeded в data дополнительно лежит refund.

Заголовки
X-Exezine-Event: payment.succeeded
X-Exezine-Event-Id: evt_5f0f0c55264b546ecb1a3045
X-Exezine-Attempt: 1
X-Exezine-Signature: t=1790095200,v1=6c1f…a93

Проверка подписи — обязательно

Подпись считается от строки "<t>.<сырое тело запроса>" алгоритмом HMAC-SHA256 на вашем секрете вебхука — он в кабинете.

PHP
<?php
$secret = 'ваш_секрет_из_кабинета';
$body   = file_get_contents('php://input');          // СЫРОЕ тело, до json_decode
$sig    = $_SERVER['HTTP_X_EXEZINE_SIGNATURE'] ?? '';

if (!preg_match('~t=(\d+),v1=([a-f0-9]{64})~', $sig, $m)) {
    http_response_code(400); exit;
}
$expected = hash_hmac('sha256', $m[1] . '.' . $body, $secret);
if (!hash_equals($expected, $m[2]) || abs(time() - (int) $m[1]) > 300) {
    http_response_code(400); exit;                    // подпись не сошлась или запоздала
}

$event = json_decode($body, true);
// … ваша обработка …
http_response_code(200);
echo 'OK';
Node.js
const crypto = require('crypto');

function verify(rawBody, header, secret) {
  const m = /t=(\d+),v1=([a-f0-9]{64})/.exec(header || '');
  if (!m) return false;
  const expected = crypto.createHmac('sha256', secret)
    .update(`${m[1]}.${rawBody}`).digest('hex');
  const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(m[2]));
  return ok && Math.abs(Date.now() / 1000 - Number(m[1])) < 300;
}

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

Правила обработки

  1. Отвечайте 200 быстро. Тяжёлую работу — в фоновую очередь. Любой ответ кроме 2xx считается неудачей.
  2. Событие может прийти дважды. Запоминайте id события (evt_…) и повторные игнорируйте.
  3. Повторы. При неудаче мы повторим 8 раз: через 1 мин, 5 мин, 30 мин, 2 ч, 6 ч, 12 ч, 24 ч. Потом событие помечается недоставленным, и его можно переотправить кнопкой из кабинета.
  4. Порядок не гарантирован. Сверяйтесь со status в теле, а не с порядком прихода.
  5. Деньги считайте по вебхуку, а не по возвращению покупателя на сайт: покупатель может закрыть вкладку, а платёж пройдёт.

В кабинете есть кнопка «Отправить тестовое событие» — придёт test.ping с настоящей подписью. Удобно проверить приёмник до боевого запуска.

Возврат покупателя на ваш сайт

После оплаты мы перенаправляем покупателя на ваш return_url и добавляем:

https://shop.az/thanks?payment_id=EXZ-20260922-7FD54BA1D8&status=succeeded

Эти параметры — только для показа страницы «Спасибо». Заказ можно считать оплаченным лишь после вебхука или после GET /payments/{id}: адресную строку покупатель может отредактировать.

Встроенная форма

Покупатель остаётся на вашей странице, форма карты открывается в рамке, которую отдаём мы. Данные карты через ваш сайт не проходят.

  1. Сообщите нам домены вашего сайта — мы внесём их в белый список, иначе браузер запретит встраивание.
  2. На странице:
ПодключениеHTML + JS
<div id="pay"></div>
<script src="https://exezine.az/assets/js/exezine-checkout.js"></script>
<script>
  // checkout_url пришёл в ответе POST /api/v2/payments
  ExezineCheckout.mount('#pay', {
    url: payment.checkout_url,
    onSuccess: function (p) { location.href = '/thanks?id=' + p.payment_id; },
    onFailure: function (p) { alert('Оплата не прошла'); }
  });
</script>

Простой переход вместо рамки:

ExezineCheckout.redirect(payment.checkout_url);

Результат, полученный этим скриптом, — для интерфейса. Отгружать товар и менять статус заказа у себя — только по вебхуку.

Порядок подключения

  1. Получите тестовый ключ, вызовите GET /api/v2/ping — убедитесь в связи.
  2. Настройте адрес вебхука, нажмите «Отправить тестовое событие», добейтесь, чтобы подпись сходилась.
  3. Проведите тестовый платёж от создания до payment.succeeded.
  4. Проверьте повторный запрос с тем же Idempotency-Key.
  5. Проверьте возврат — полный и частичный.
  6. Получите боевой ключ и повторите пункты 3–5 на маленькой сумме.
  7. Уберите тестовый ключ из настроек боевого сервера.

Чего делать не нужно

  • Опрашивать GET /payments/{id} в цикле — для этого есть вебхуки.
  • Доверять параметрам в адресной строке при возврате покупателя.
  • Хранить ключ на стороне браузера или в мобильном приложении.
  • Слать платёж без Idempotency-Key — при обрыве сети получите дубль.
  • Разбирать message ошибки текстом — смотрите на code.
СимптомПричина
Подпись не сходитсяТело разобрано фреймворком до проверки. Нужно сырое
Суммы больше в 100 разПередали 49.90 вместо 4990
422 amount_invalidПрислали дробное число. В API только целые
Два платежа на один заказНет Idempotency-Key
404 на существующем платежеТестовый ключ в боевом окружении или наоборот

Поддержка

Пишите на support@exezine.az. Приложите request_id из ответа и время с точностью до минуты — так мы найдём запрос в журнале сразу.

Не нашли ответа или что-то ведёт себя не так, как написано? Напишите нам — отвечаем по существу.

support@exezine.az