Документация 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, отдельный адрес для тестовых вебхуков. Все изменения — дополнения: существующая интеграция работает без правок.

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

  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.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 — идентификатор платежа у нас. Сохраните его рядом с заказом.

ПолеЧто значит
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
customer.idВаш id покупателя, если передавали
expires_atДо какого момента платёж можно оплатить
initiatorcustomer — платит покупатель, merchant — списание сохранённой картой без покупателя
save_payment_methodЧто просили: true, "optional" или false
payment_methodСохранённая карта (pm_…): сохранённая по итогам этой оплаты или та, которой списали. Иначе null
next_actionПри requires_action: {"type":"redirect","url":"…"} — куда отправить покупателя
duplicate_ofId первой оплаты того же 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/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Курсор следующей страницы
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}/cancel
curl -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_…
initiatormerchant (по умолчанию) — списание без покупателя (подписка, автоплатёж); 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После этого момента ссылка не работает
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Прошёл возврат
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
<?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 быстро — но только когда событие сохранено. Ждём ответ 15 секунд; тяжёлую работу — в фоновую очередь. Любой ответ кроме 2xx считается неудачей, и мы повторим доставку. Не отвечайте 200 заранее: если обработка потом упадёт, повтора уже не будет.
  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?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
autoOpenfalse — сначала наш экран с суммой и кнопкой (как раньше). По умолчанию 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 её нет.

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

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