Документация API
Всё, что нужно разработчику для приёма платежей через E-Xezine: создание платежей, вебхуки, возвраты, встроенная форма.
Редакция от 22.09.2026, версия API 2026-09-22
Документация к API версии 2026-09-22. Базовый адрес —
https://exezine.az/api/v2, машинное описание —
openapi.json. Интерактивная справка по всем
методам: открыть.
Как это устроено
- Ваш сервер создаёт платёж:
POST /payments. - В ответе приходит
checkout_url— наша страница оплаты. - Вы отправляете покупателя на этот адрес (или показываете его в рамке на своём сайте).
- Когда оплата завершится, мы присылаем вам вебхук
payment.succeeded. - Покупателя мы возвращаем на ваш
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.
| HTTP | type | Когда |
|---|---|---|
| 400 | invalid_request_error | Кривой JSON |
| 401 | authentication_error | Нет ключа, ключ неверный или отозван |
| 403 | authentication_error | API выключен для магазина |
| 404 | invalid_request_error | Объект не найден |
| 409 | invalid_request_error | Конфликт: идемпотентность, неподходящий статус |
| 422 | invalid_request_error | Значение поля не подходит |
| 429 | rate_limit_error | Слишком часто |
| 5xx | api_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 — идентификатор платежа у нас. Сохраните его рядом с заказом.
| Поле | Что значит |
|---|---|
livemode | true — боевой платёж, 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/paymentsGET /api/v2/payments?limit=25&status=succeeded&order_id=1024| Параметр | Описание |
|---|---|
limit | 1–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 | После этого момента ссылка не работает |
type | dynamic (по умолчанию) или 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
$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';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 даст другие байты, и подпись не сойдётся. Это самая
частая причина «у меня подпись не проверяется».
Правила обработки
- Отвечайте
200быстро. Тяжёлую работу — в фоновую очередь. Любой ответ кроме 2xx считается неудачей. - Событие может прийти дважды. Запоминайте
idсобытия (evt_…) и повторные игнорируйте. - Повторы. При неудаче мы повторим 8 раз: через 1 мин, 5 мин, 30 мин, 2 ч, 6 ч, 12 ч, 24 ч. Потом событие помечается недоставленным, и его можно переотправить кнопкой из кабинета.
- Порядок не гарантирован. Сверяйтесь со
statusв теле, а не с порядком прихода. - Деньги считайте по вебхуку, а не по возвращению покупателя на сайт: покупатель может закрыть вкладку, а платёж пройдёт.
В кабинете есть кнопка «Отправить тестовое событие» — придёт
test.ping с настоящей подписью. Удобно проверить приёмник до боевого запуска.
Возврат покупателя на ваш сайт
После оплаты мы перенаправляем покупателя на ваш return_url и добавляем:
https://shop.az/thanks?payment_id=EXZ-20260922-7FD54BA1D8&status=succeededЭти параметры — только для показа страницы «Спасибо».
Заказ можно считать оплаченным лишь после вебхука или после
GET /payments/{id}: адресную строку покупатель может отредактировать.
Встроенная форма
Покупатель остаётся на вашей странице, форма карты открывается в рамке, которую отдаём мы. Данные карты через ваш сайт не проходят.
- Сообщите нам домены вашего сайта — мы внесём их в белый список, иначе браузер запретит встраивание.
- На странице:
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);Результат, полученный этим скриптом, — для интерфейса. Отгружать товар и менять статус заказа у себя — только по вебхуку.
Порядок подключения
- Получите тестовый ключ, вызовите
GET /api/v2/ping— убедитесь в связи. - Настройте адрес вебхука, нажмите «Отправить тестовое событие», добейтесь, чтобы подпись сходилась.
- Проведите тестовый платёж от создания до
payment.succeeded. - Проверьте повторный запрос с тем же
Idempotency-Key. - Проверьте возврат — полный и частичный.
- Получите боевой ключ и повторите пункты 3–5 на маленькой сумме.
- Уберите тестовый ключ из настроек боевого сервера.
Чего делать не нужно
- Опрашивать
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