Документация API
Всё, что нужно разработчику для приёма платежей через E-Xezine: создание платежей, вебхуки, возвраты и отмена, сохранённые карты и автосписания, встроенная форма.
Редакция от 03.10.2026, версия API 2026-09-22
Документация к API версии 2026-09-22. Базовый адрес —
https://exezine.az/api/v2, машинное описание —
openapi.json. Интерактивная справка по всем
методам: открыть.
Новое 03.10.2026: оформление формы оплаты полем appearance, встроенная форма открывается сразу и сама подбирает высоту рамки (скрипт ?v=2), галочка «Сохранить карту» — только по вашей просьбе. Отменённый или истёкший платёж больше не оплатить на уже открытой странице банка (подробнее); уточнены правила обработки вебхуков.
Новое 01.10.2026: сохранённые карты и автосписания,
отмена платежа, свой срок оплаты и быстрый payment.expired,
защита от двойной оплаты по order_id, отдельный адрес для тестовых вебхуков.
Все изменения — дополнения: существующая интеграция работает без правок.
Как это устроено
- Ваш сервер создаёт платёж:
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.id | нет | Id покупателя в вашей системе (до 64 символов: латиница, цифры, _ - . : @). К нему привязываются сохранённые карты |
expires_in_minutes | нет | Свой срок оплаты: 5–1440 минут. Не задан — срок вашего магазина (по умолчанию 24 часа) |
save_payment_method | нет | true — сохранить карту для следующих списаний, "optional" — спросить покупателя галочкой. См. «Сохранённые карты» |
payment_method | нет | pm_… — списать сохранённой картой без страницы оплаты |
allow_duplicate_order | нет | true — разрешить второй платёж по уже оплаченному order_id |
appearance | нет | Оформление формы карты: цвета, шрифты, скрыть шапку и подвал. См. «Оформление формы оплаты» |
Про customer. Банки-эмитенты в 3-D Secure 2 оценивают риск
по этим данным. Передавайте хотя бы email, ip и
country — это ощутимо уменьшает долю отказов.
Про СБП. method: "sbp" работает только с
currency: "RUB". В другой валюте вернётся
422 method_not_available.
Защита от двойной оплаты. Если по этому order_id уже есть
оплаченный платёж, второй не создаётся: вернётся 409 order_already_paid,
а в поле error.payment_id — id той оплаты. Так повторное нажатие
«Оплатить» или повтор задачи на вашей стороне не спишет деньги дважды. Если вторая
оплата нужна намеренно (например, доплата), передайте "allow_duplicate_order": true.
Если покупатель всё же оплатил заказ дважды (открыл две страницы оплаты до первой
оплаты), у второго платежа будет заполнено поле duplicate_of, а мы
пришлём уведомление в Telegram.
Срок оплаты
У каждого платежа есть срок — поле expires_at. Его задаёт
expires_in_minutes, а если он не передан — настройка вашего магазина
(по умолчанию 24 часа). После срока страница оплаты перестаёт принимать карту,
и в течение нескольких минут приходит вебхук payment.expired.
Если банк в последний момент ещё проводит 3-D Secure, мы ждём до 15 минут после
срока и только потом закрываем платёж.
Ответ 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": { "id": "user-77", "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",
"next_action": null,
"initiator": "customer",
"save_payment_method": false,
"payment_method": null,
"duplicate_of": null,
"expires_at": "2026-09-23T18:30:00+04:00",
"paid_at": null,
"canceled_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 |
customer.id | Ваш id покупателя, если передавали |
expires_at | До какого момента платёж можно оплатить |
initiator | customer — платит покупатель, merchant — списание сохранённой картой без покупателя |
save_payment_method | Что просили: true, "optional" или false |
payment_method | Сохранённая карта (pm_…): сохранённая по итогам этой оплаты или та, которой списали. Иначе null |
next_action | При requires_action: {"type":"redirect","url":"…"} — куда отправить покупателя |
duplicate_of | Id первой оплаты того же order_id, если заказ оплачен дважды |
canceled_at | Когда вы отменили платёж |
Статусы
| Статус | Значение |
|---|---|
pending | Создан, покупатель ещё не заплатил |
succeeded | Оплачен |
failed | Отказ, см. failure |
expired | Истёк срок оплаты |
refunded | Возвращён полностью |
partially_refunded | Возвращён частично |
review | На проверке у нас |
canceled | Отменён вами (POST /payments/{id}/cancel) |
requires_action | Списание сохранённой картой ждёт подтверждения покупателя (3-D Secure), см. next_action |
Финальные: succeeded, failed, expired,
canceled, 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 | Курсор следующей страницы |
customer_id | Платежи одного покупателя (ваш customer.id) |
{
"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/payments/{id}/cancelcurl -X POST -H "Authorization: Bearer $KEY" -H "Idempotency-Key: cancel-1024" \
https://exezine.az/api/v2/payments/EXZ-20260922-7FD54BA1D8/cancel- Успех —
200и платёж в статусеcanceled, плюс вебхукpayment.canceled. - Отменить можно только платёж в статусе
pendingилиrequires_action. Иначе —409 payment_not_cancelable, а вerror.payment_statusего текущий статус (например,succeeded: покупатель успел заплатить). - Перед отменой мы ещё раз спрашиваем банк: оплаченный платёж отменить нельзя.
- Ссылка
checkout_urlотменённого платежа к оплате больше не ведёт.
Если страница банка уже открыта. Покупатель мог открыть
страницу оплаты до отмены. Провести оплату по ней, как правило, уже не получится:
перед списанием платёжный шлюз спрашивает нас, и отменённый платёж мы отклоняем —
покупатель увидит «Платёж отменён магазином». Но гарантировать это на все 100% нельзя
(например, при сбое связи со шлюзом). Поэтому payment.succeeded, пришедший
после отмены, обрабатывайте как обычную оплату (или сделайте возврат).
Сохранённые карты и автосписания
Для подписок, автоплатежей и оплаты «в один клик»: покупатель один раз вводит
карту на нашей странице, а дальше вы списываете по ней сами, без страницы оплаты.
Номер карты не видите ни вы, ни мы — у вас только идентификатор pm_…
и маска карты.
Функция включается по запросу: напишите в поддержку, мы
согласуем её с банком. Включена ли она — видно в ответе GET /ping
(features.saved_cards) и в кабинете.
1. Сохранить карту при оплате
POST /api/v2/payments{
"amount": 4990,
"order_id": "sub-77-1",
"save_payment_method": true,
"customer": { "id": "user-77", "email": "buyer@example.com" },
"return_url": "https://shop.az/thanks"
}| Значение | Как работает |
|---|---|
true | Карта сохраняется всегда. Согласие покупателя на будущие списания вы получаете у себя (галочка при оформлении подписки, условия оферты) |
"optional" | На странице оплаты покупатель видит галочку «Сохранить карту для следующих оплат». Не поставил — карта не сохранится |
Покупатель вводит карту и проходит 3-D Secure как обычно. Когда платёж станет
succeeded, в нём появится payment_method:
"payment_method": {
"id": "pm_3f9a1c7e5b2d4f6a8c0e1b3d",
"object": "payment_method",
"type": "card",
"livemode": true,
"customer_id": "user-77",
"card": { "brand": "visa", "last4": "4242", "exp_month": 7, "exp_year": 2030, "country": "AZ" },
"status": "active",
"source": "checkout",
"metadata": {},
"created_at": "2026-10-01T12:00:00+04:00",
"last_used_at": null
}Тот же объект придёт отдельным вебхуком payment_method.saved.
Сохраните pm_… у себя рядом с покупателем. Если покупатель снова
оплатит той же картой, новая карта не появится — обновится прежняя.
2. Списать сохранённой картой
POST /api/v2/payments{
"amount": 4990,
"order_id": "sub-77-2026-11",
"description": "Подписка, ноябрь",
"payment_method": "pm_3f9a1c7e5b2d4f6a8c0e1b3d",
"customer": { "id": "user-77" }
}Страницы оплаты нет: банк отвечает сразу, и ответ 201 уже
содержит итог в status:
| status | Что делать |
|---|---|
succeeded | Деньги списаны. Придёт и вебхук payment.succeeded |
failed | Банк отказал, причина в failure. Придёт payment.failed |
requires_action | Банк требует подтверждения покупателем (3-D Secure). Отправьте покупателю ссылку next_action.url; итог придёт вебхуком |
pending | Банк ещё не ответил. Итог придёт вебхуком; не повторяйте списание с новым ключом идемпотентности |
| Поле | Описание |
|---|---|
payment_method | Обязательно для списания: pm_… |
initiator | merchant (по умолчанию) — списание без покупателя (подписка, автоплатёж);
customer — покупатель сейчас у вас и сам нажал «Оплатить» сохранённой картой |
mit_type | Только по согласованию с нами: причина списания без покупателя —
delayed_charge, increment, resubmission, reauthorization, no_show |
customer.id | Если передан, должен совпадать с покупателем карты — иначе 422 customer_mismatch |
Обязательно передавайте Idempotency-Key —
например sub-77-2026-11. Повтор запроса после обрыва сети вернёт тот же
платёж, а не спишет деньги второй раз. А вот при отказе банка
(failed) новую попытку делайте с новым ключом и не чаще раза в сутки:
частые повторы по отказной карте банки воспринимают как мошенничество.
3. Управление картами
| Запрос | Что делает |
|---|---|
GET /api/v2/payment_methods?customer_id=user-77 | Карты покупателя (без customer_id — все). Параметры limit, starting_after, include_revoked=1 |
GET /api/v2/payment_methods/{pm_id} | Одна карта |
DELETE /api/v2/payment_methods/{pm_id} | Удалить карту (покупатель отвязал её у вас). Списать ею больше нельзя |
Статус карты: active, expired (истёк срок действия
карты) или revoked (удалена). Тестовые и боевые карты разделены так же,
как платежи.
Перенос ранее сохранённых карт
Если вы раньше работали с банком-эквайером напрямую и у вас уже есть токены карт покупателей того же магазина, их можно перенести — покупателям не придётся вводить карты заново. Перенос включаем по запросу.
POST /api/v2/payment_methods{
"customer_id": "user-77",
"token": "токен карты, полученный раньше от банка",
"card": { "brand": "visa", "last4": "4242", "exp_month": 7, "exp_year": 2030 },
"metadata": { "migrated": "2026-10" }
}Ответ — 201 и объект карты (pm_…). Повторный перенос
того же токена тому же покупателю вернёт ту же карту (200).
Проверить токен заранее нельзя: если он недействителен, первое списание вернёт отказ.
Ошибки сохранённых карт
| Код | Когда |
|---|---|
saved_cards_disabled | Функция не включена для вашего магазина |
payment_method_invalid | Карта не найдена (опечатка, другой режим ключа) |
payment_method_revoked | Карта удалена |
payment_method_expired | Истёк срок действия карты — попросите покупателя оплатить новой картой с сохранением |
payment_method_unusable | Карту нельзя использовать — попросите покупателя оплатить заново с сохранением |
customer_mismatch | Карта принадлежит другому покупателю |
parameters_conflict | В одном запросе и payment_method, и save_payment_method |
save_requires_card | Сохранить можно только карту, не СБП |
Платёжные ссылки
Когда сайта нет или счёт выставляется вручную.
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 | Прошёл возврат |
payment.canceled | Вы отменили платёж |
payment_method.saved | Карта покупателя сохранена (в data.object — карта, в data.payment_id — платёж) |
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Отдельный адрес для тестовых событий
В кабинете можно указать второй адрес — «URL для тестовых событий».
Тогда события по платежам с тестовым ключом ("livemode": false) уходят
туда, а боевые — на основной адрес. У тестового адреса свой секрет подписи:
тестовому серверу не нужно знать боевой секрет. Не указан — все события идут на
основной адрес, как раньше; различайте их по полю livemode.
Проверка подписи — обязательно
Подпись считается от строки "<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быстро — но только когда событие сохранено. Ждём ответ 15 секунд; тяжёлую работу — в фоновую очередь. Любой ответ кроме 2xx считается неудачей, и мы повторим доставку. Не отвечайте200заранее: если обработка потом упадёт, повтора уже не будет. - Событие может прийти дважды. Запоминайте
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?v=2"></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);Результат, полученный этим скриптом, — для интерфейса. Отгружать товар и менять статус заказа у себя — только по вебхуку.
Как ведёт себя рамка
Форма карты открывается сразу, без промежуточного экрана. Если покупатель закрыл её крестиком, в рамке появится кнопка «Оплатить картой» — она откроет форму снова. Если форма закрылась после успешной оплаты, рамка показывает «Проверяем оплату…» до подтверждения банка — оплатить второй раз в этот момент нельзя.
Высоту рамки скрипт подбирает сам: пока открыта форма карты — formHeight
(по умолчанию 640 px), после оплаты — под сообщение с итогом.
Параметр mount | Описание |
|---|---|
url | Обязательно. checkout_url из ответа API |
autoOpen | false — сначала наш экран с суммой и кнопкой (как раньше). По умолчанию true |
formHeight | Высота рамки, пока открыта форма карты, px (360–1400). По умолчанию 640 |
height | Фиксированная высота рамки, px. Тогда авто-высота выключена |
onSuccess, onFailure | Итог оплаты (для интерфейса) |
onReady, onResize, onMessage | Рамка загрузилась / сменила высоту / любое наше сообщение |
Подключайте скрипт по адресу с ?v=2, как в примере выше:
браузеры долго хранят старые копии файлов, а новый адрес гарантирует новую версию.
Своя рамка без нашего скрипта. <iframe src="{checkout_url}?embed=1">.
Параметры адреса: auto=0 — не открывать форму сразу, fh=640 —
высота формы. Высоту меняйте по событию message: если
event.origin === "https://exezine.az", data.source === "exezine"
и data.type === "resize", то в data.height — нужная высота в пикселях.
Оформление формы оплаты
Форму карты можно оформить в стиле вашего сайта: цвета, шрифты, скругления,
скрыть шапку и подвал. Оформление передаётся в каждом платеже полем
appearance — в кабинете ничего настраивать не нужно, а поменять его можно
в любой момент у себя в коде. Действует и во встроенной форме, и на отдельной
странице оплаты.
POST /api/v2/payments{
"amount": 3500,
"order_id": "fee-77-2026-11",
"appearance": {
"widget": { "backgroundColor": "#0E1114", "buttonsColor": "#C6FF00", "backgroundType": 0 },
"header": { "display": "none" },
"price": { "color": "#C6FF00", "fontSize": "20px", "fontWeight": "700" },
"button": { "pay": { "backgroundColor": "#C6FF00", "color": "#0E1114", "borderRadius": "12px" } },
"inputs": { "backgroundColor": "#1A2027", "color": "#F4F6F8", "cvc": { "type": "password" } },
"footer": { "display": "none" }
}
}| Раздел | Что оформляет |
|---|---|
widget | Форма целиком: backgroundColor — фон, buttonsColor — цвет кнопок, backgroundType — фоновый узор (0 — без узора), шрифт |
header | Шапка (название магазина, «безопасная оплата»). "display": "none" — скрыть |
price, description | Сумма и назначение платежа |
button | Кнопки: card — «Оплатить картой», brands — Apple Pay / Google Pay, pay — «Оплатить» |
inputs | Поля ввода; inputs.cvc.type: "password" — скрывать CVC точками |
cardFace | Изображение карты |
footer | Подвал с логотипами. "display": "none" — скрыть |
Имена свойств — как в CSS, но слитно: backgroundColor, fontSize,
fontFamily, fontWeight, borderRadius, letterSpacing,
margin. Значения — строки ("#0E1114", "14px"), числа,
true/false.
Ограничения. Вложенность — до 4 уровней, размер объекта — до 64 КБ. В строках
нельзя символы < > { }, а также javascript:,
expression( и @import. Картинки в base64 сюда не помещаются —
оформление задаётся цветами и шрифтами. При ошибке вернётся
422 appearance_invalid, а в error.param — точное место, например
appearance.header.display.
Мы проверяем формат, а не смысл свойств: опечатка в имени свойства ошибки не вызовет — свойство просто не сработает. Проверьте вид на одном платеже, прежде чем включать для всех. Если платёж будет проведён через провайдера, который не поддерживает оформление, оплата пройдёт как обычно, только форма будет стандартной.
Галочка «Сохранить карту» в форме появляется только при
save_payment_method: "optional". В остальных платежах через API её нет.
Порядок подключения
- Получите тестовый ключ, вызовите
GET /api/v2/ping— убедитесь в связи. - Настройте адрес вебхука, нажмите «Отправить тестовое событие», добейтесь, чтобы подпись сходилась.
- Проведите тестовый платёж от создания до
payment.succeeded. - Проверьте повторный запрос с тем же
Idempotency-Key. - Проверьте возврат — полный и частичный.
- Получите боевой ключ и повторите пункты 3–5 на маленькой сумме.
- Уберите тестовый ключ из настроек боевого сервера.
- Если используете сохранённые карты — проверьте первую оплату с
save_payment_method, списание поpm_…, отказ и удаление карты.
Чего делать не нужно
- Опрашивать
GET /payments/{id}в цикле — для этого есть вебхуки. - Доверять параметрам в адресной строке при возврате покупателя.
- Хранить ключ на стороне браузера или в мобильном приложении.
- Слать платёж без
Idempotency-Key— при обрыве сети получите дубль. - Разбирать
messageошибки текстом — смотрите наcode.
| Симптом | Причина |
|---|---|
| Подпись не сходится | Тело разобрано фреймворком до проверки. Нужно сырое |
| Суммы больше в 100 раз | Передали 49.90 вместо 4990 |
422 amount_invalid | Прислали дробное число. В API только целые |
| Два платежа на один заказ | Нет Idempotency-Key |
404 на существующем платеже | Тестовый ключ в боевом окружении или наоборот |
409 order_already_paid | Заказ с этим order_id уже оплачен. Это защита от двойной оплаты |
Списание картой всё время failed | Карта заблокирована или пуста. Повторяйте не чаще раза в сутки, после 3–4 отказов попросите покупателя оплатить заново |
Поддержка
Пишите на support@exezine.az. Приложите
request_id из ответа и время с точностью до минуты — так мы найдём
запрос в журнале сразу.
Не нашли ответа или что-то ведёт себя не так, как написано? Напишите нам — отвечаем по существу.
support@exezine.az