# AvantPay API: приём платежей — полная документация > Вся документация API AvantPay для приёма платежей одним файлом: для ИИ-ассистентов и разработчиков. Собрана автоматически из страниц https://avantpay.io/developers/ в порядке навигации. Каждый раздел начинается с адреса страницы на сайте. Как читать: адрес API — https://api.avantpay.io/v3, для платежей три метода: POST /v3/payments (создать), GET /v3/payments/{payment_id} (получить), GET /v3/payments (список). В каждом платеже обязателен email покупателя — metadata.email; покупатель из Telegram — @telegram.org. Все поля, параметры и ошибки — в разделе «Справочник API». Если чего-то нет в этом файле — этого нет в API AvantPay: не придумывайте поля, методы и значения, спросите разработчика или поддержку (support@avantpay.io). ## Содержание 1. API AvantPay — https://avantpay.io/developers/ 2. Быстрый старт — https://avantpay.io/developers/quick-start/ 3. Справочник API — https://avantpay.io/developers/api/ 4. Статусы платежа — https://avantpay.io/developers/statuses/ 5. Уведомления — https://avantpay.io/developers/webhooks/ 6. Проверка интеграции — https://avantpay.io/developers/testing/ 7. Чеклист перед запуском — https://avantpay.io/developers/checklist/ 8. Частые вопросы — https://avantpay.io/developers/faq/ 9. Интеграция с помощью ИИ — https://avantpay.io/developers/ai/ --- ## API AvantPay Страница: https://avantpay.io/developers/ API AvantPay принимает онлайн-платежи от ваших покупателей: по СБП и банковской картой, в рублях. Адрес API: `https://api.avantpay.io/v3` ### Как проходит оплата 1. **Вы создаёте платёж:** `POST https://api.avantpay.io/v3/payments` с суммой, адресом возврата и email покупателя. В ответе — объект платежа со ссылкой на оплату `confirmation.confirmation_url`. 2. **Покупатель переходит по ссылке** и платит на странице AvantPay. Потом AvantPay возвращает его на ваш `return_url`. 3. **AvantPay присылает уведомление** на ваш сервер: `payment.succeeded` (оплачен) или `payment.canceled` (не оплачен). 4. **Вы проверяете статус** запросом `GET https://api.avantpay.io/v3/payments/{payment_id}` и выполняете заказ, только если `status` равен `succeeded`, а `amount` — сумме заказа. Все четыре шага с готовыми запросами — в [быстром старте](https://avantpay.io/developers/quick-start/). ### Методы API | Метод | Что делает | В справочнике | |---|---|---| | `POST /v3/payments` | Создать платёж | [Создать платёж](https://avantpay.io/developers/api/#create_payment) | | `GET /v3/payments/{payment_id}` | Получить платёж по его `id` | [Получить платёж](https://avantpay.io/developers/api/#get_payment) | | `GET /v3/payments` | Список платежей с фильтрами | [Список платежей](https://avantpay.io/developers/api/#get_payments_list) | Все поля и ошибки — в [справочнике API](https://avantpay.io/developers/api/): одна страница, методы и объекты подряд. Подтвердить (`/capture`) или отменить (`/cancel`) платёж через API нельзя: платёж списывается сразу, а неоплаченный отменяется сам. Чеки 54-ФЗ, сохранение карт и автоплатежи AvantPay не поддерживает. Вернуть деньги покупателю можно в личном кабинете. ### Куда дальше - [Быстрый старт](https://avantpay.io/developers/quick-start/) — первый платёж за четыре шага. - [Справочник API](https://avantpay.io/developers/api/) — все методы, поля и ошибки. - [Уведомления](https://avantpay.io/developers/webhooks/) — как принять и проверить уведомление, готовые обработчики. - [Интеграция с помощью ИИ](https://avantpay.io/developers/ai/) — дайте ассистенту ссылку https://avantpay.io/developers/llms-full.txt: это вся документация одним файлом. ### Термины | Термин | Что это | |---|---| | ID магазина | Число, логин для запросов к API. Есть в личном кабинете. | | Секретный ключ | Пароль для запросов к API. Создаётся в личном кабинете. | | Платёж | Одна попытка оплаты: сумма, статус и ссылка на оплату. | | Email покупателя | Адрес покупателя в `metadata.email`. | | Ключ идемпотентности | Ваша уникальная строка в заголовке `Idempotence-Key`. Защищает от двойного платежа, если запрос пришлось повторить. | | Номер заказа | Ваш номер заказа в `metadata.order_id`. Вернётся в объекте платежа и в уведомлениях. | | Уведомление | POST-запрос от AvantPay на ваш сервер, когда платёж оплачен или отменён. | | Личный кабинет | https://avantpay.io/lk/ — ключи, настройки уведомлений, способы оплаты, платежи и возвраты. | Вопросы — в поддержку: support@avantpay.io. --- ## Быстрый старт Страница: https://avantpay.io/developers/quick-start/ Эта страница проводит через весь путь: от ключей до проверки оплаты. Примеры — на cURL, Python, Node.js и PHP: выберите язык в любом блоке кода, и он запомнится на всех страницах. Все поля и ошибки — в [справочнике API](https://avantpay.io/developers/api/). Тестового режима нет: каждый оплаченный платёж — настоящий. Как проверить интеграцию без денег — на странице [Проверка интеграции](https://avantpay.io/developers/testing/). ### Шаг 1. Получите ключи и включите уведомления 1. Войдите в [личный кабинет](https://avantpay.io/lk/). 2. Откройте **Ещё → Разработчикам → API-ключ** ([открыть сразу](https://avantpay.io/lk/?path=/api/key)). На телефоне вместо «Ещё» — «Меню». 3. Скопируйте **ID магазина** из блока «Авторизация запросов». Это число, например `33661`. Там же — адрес API: `https://api.avantpay.io/v3`. 4. Нажмите **Создать ключ**. Ключ показывается целиком только один раз. Скопируйте его и сохраните в настройках своего сервера. 5. Проверьте блок «Права ключа»: право «Платежи» должно быть включено. Если оно выключено, напишите в поддержку: support@avantpay.io. Включить его в личном кабинете нельзя. Если раздела «Разработчикам» нет, попросите владельца магазина добавить вас в разделе «Пользователи» с ролью «Разработчик». Сохраните ID магазина и ключ в переменные окружения. Все примеры ниже берут их оттуда: ```bash export AVANTPAY_SHOP_ID="33661" export AVANTPAY_SECRET_KEY="<секретный ключ>" ``` Проверьте ключи. Этот запрос возвращает последний платёж магазина и ничего не меняет: ```bash curl "https://api.avantpay.io/v3/payments?limit=1" \ -u "$AVANTPAY_SHOP_ID:$AVANTPAY_SECRET_KEY" ``` Ответ с кодом 200 — ключи верные. У нового магазина список пустой: ```json { "type": "list", "items": [] } ``` Ответ с кодом 401 и `"code": "invalid_credentials"` — неверный ID магазина или ключ. Проверьте, что скопировали их без пробелов. Сразу включите уведомления — до первого платежа. Адрес уведомлений задаётся только в личном кабинете и запоминается в платеже при создании: платежи, созданные без адреса, уведомлений не получат. 1. Откройте в личном кабинете **Ещё → Разработчикам → HTTP-уведомления** ([открыть сразу](https://avantpay.io/lk/?path=/api/webhook)). 2. Нажмите **Изменить настройки**. Укажите адрес своего сервера, например `https://example.com/avantpay/webhook`. Адрес должен начинаться с `https://`. 3. Отметьте события «Платёж оплачен» (`payment.succeeded`) и «Платёж отменён» (`payment.canceled`). Сохраните. Обработчик уведомлений можно написать позже, на шаге 4. ### Шаг 2. Создайте платёж > **Email покупателя обязателен.** > > Передайте его в `metadata.email`. Покупатель из Telegram — `@telegram.org`, например `123456789@telegram.org`. Отправьте запрос `POST /v3/payments`: cURL: ```bash curl https://api.avantpay.io/v3/payments \ -X POST \ -u "$AVANTPAY_SHOP_ID:$AVANTPAY_SECRET_KEY" \ -H "Idempotence-Key: 7f1c9a52-3d4b-4e8f-a1b2-c3d4e5f6a7b8" \ -H "Content-Type: application/json" \ -d '{ "amount": { "value": "100.00", "currency": "RUB" }, "capture": true, "confirmation": { "type": "redirect", "return_url": "https://example.com/orders/1" }, "description": "Заказ №1", "metadata": { "email": "buyer@example.com", "order_id": "1" } }' ``` Python: ```python import os import requests SHOP_ID = os.environ["AVANTPAY_SHOP_ID"] SECRET_KEY = os.environ["AVANTPAY_SECRET_KEY"] def create_payment(order_id: str, email: str, amount: str, return_url: str, idempotence_key: str) -> dict: response = requests.post( "https://api.avantpay.io/v3/payments", auth=(SHOP_ID, SECRET_KEY), headers={"Idempotence-Key": idempotence_key}, json={ "amount": {"value": amount, "currency": "RUB"}, "capture": True, "confirmation": {"type": "redirect", "return_url": return_url}, "description": f"Заказ №{order_id}", # email покупателя обязателен; покупатель из Telegram — f"{telegram_id}@telegram.org" "metadata": {"email": email, "order_id": order_id}, }, timeout=30, ) if response.status_code != 200: raise RuntimeError(f"AvantPay: HTTP {response.status_code}: {response.text}") payment = response.json() # Повтор с тем же ключом может вернуть платёж, который не создался до конца (canceled, без ссылки): # тогда создайте новый платёж с новым ключом идемпотентности if payment["status"] != "pending" or "confirmation_url" not in payment.get("confirmation", {}): raise RuntimeError(f"AvantPay: платёж {payment['id']} в статусе {payment['status']}, ссылки на оплату нет") return payment # покупателя — на ["confirmation"]["confirmation_url"] ``` Node.js: ```js const AUTH = "Basic " + Buffer.from(`${process.env.AVANTPAY_SHOP_ID}:${process.env.AVANTPAY_SECRET_KEY}`).toString("base64"); export async function createPayment({ orderId, email, amount, returnUrl, idempotenceKey }) { const response = await fetch("https://api.avantpay.io/v3/payments", { method: "POST", headers: { Authorization: AUTH, "Idempotence-Key": idempotenceKey, "Content-Type": "application/json", }, body: JSON.stringify({ amount: { value: amount, currency: "RUB" }, capture: true, confirmation: { type: "redirect", return_url: returnUrl }, description: `Заказ №${orderId}`, // email покупателя обязателен; покупатель из Telegram — `${telegramId}@telegram.org` metadata: { email, order_id: orderId }, }), signal: AbortSignal.timeout(30000), }); const text = await response.text(); if (response.status !== 200) { throw new Error(`AvantPay: HTTP ${response.status}: ${text}`); } const payment = JSON.parse(text); // Повтор с тем же ключом может вернуть платёж, который не создался до конца (canceled, без ссылки): // тогда создайте новый платёж с новым ключом идемпотентности if (payment.status !== "pending" || !payment.confirmation?.confirmation_url) { throw new Error(`AvantPay: платёж ${payment.id} в статусе ${payment.status}, ссылки на оплату нет`); } return payment; // покупателя — на .confirmation.confirmation_url } ``` PHP: ```php true, CURLOPT_USERPWD => getenv('AVANTPAY_SHOP_ID') . ':' . getenv('AVANTPAY_SECRET_KEY'), CURLOPT_HTTPHEADER => [ 'Idempotence-Key: ' . $idempotenceKey, 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'amount' => ['value' => $amount, 'currency' => 'RUB'], 'capture' => true, 'confirmation' => ['type' => 'redirect', 'return_url' => $returnUrl], 'description' => 'Заказ №' . $orderId, // email покупателя обязателен; покупатель из Telegram — $telegramId . '@telegram.org' 'metadata' => ['email' => $email, 'order_id' => $orderId], ], JSON_UNESCAPED_UNICODE), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($response === false || $httpCode !== 200) { throw new RuntimeException('AvantPay: HTTP ' . $httpCode . ': ' . $response); } $payment = json_decode($response, true); // Повтор с тем же ключом может вернуть платёж, который не создался до конца (canceled, без ссылки): // тогда создайте новый платёж с новым ключом идемпотентности if ($payment['status'] !== 'pending' || empty($payment['confirmation']['confirmation_url'])) { throw new RuntimeException('AvantPay: платёж ' . $payment['id'] . ' в статусе ' . $payment['status'] . ', ссылки на оплату нет'); } return $payment; // покупателя — на ['confirmation']['confirmation_url'] } ``` Что в запросе: | Что | Что передать | |---|---| | `Idempotence-Key` | Ключ идемпотентности: любая уникальная строка до 64 символов, лучше UUID. Создайте его до запроса и сохраните в заказе. Если ответ не пришёл, повторите запрос с тем же ключом — второй платёж не появится. [Подробнее](https://avantpay.io/developers/api/#idempotence) | | `amount.value` | Сумма строкой с двумя знаками после точки. | | `amount.currency` | `RUB`. | | `metadata.email` | **Обязательно.** Email покупателя, до 254 символов. Покупатель из Telegram — `@telegram.org`. | | `capture` | Указывается `true`. | | `confirmation.type` | `redirect` — покупатель перейдёт на страницу оплаты AvantPay. | | `confirmation.return_url` | Куда вернуть покупателя после оплаты или отказа от неё. Укажите адрес страницы заказа: AvantPay не добавляет к нему параметры. | | `description` | Необязательно. Описание до 128 символов. Его видит покупатель на странице оплаты. | | `metadata.order_id` | Необязательно. Ваш номер заказа. Вернётся в объекте платежа и в уведомлениях. | Способ оплаты здесь не передан: покупатель выберет СБП или карту на странице оплаты. Все поля запроса — в справочнике: [Создать платёж](https://avantpay.io/developers/api/#create_payment). Ответ — объект платежа со статусом `pending`: ```json { "id": "2f9b6c1e-4d7a-4c3b-9e8f-1a2b3c4d5e6f", "status": "pending", "paid": false, "amount": { "value": "100.00", "currency": "RUB" }, "description": "Заказ №1", "recipient": { "account_id": "33661" }, "created_at": "2026-10-18T10:15:30.123Z", "expires_at": "2026-10-18T10:35:30.123Z", "confirmation": { "type": "redirect", "confirmation_url": "https://avantpay.io/link/Aq91c7d", "return_url": "https://example.com/orders/1" }, "test": false, "refundable": false, "metadata": { "email": "buyer@example.com", "order_id": "1" } } ``` Сохраните `id` платежа в своём заказе. По нему вы проверите платёж, когда придёт уведомление. ### Шаг 3. Отправьте покупателя на страницу оплаты Перенаправьте покупателя на адрес из `confirmation.confirmation_url`. Например, ответьте браузеру кодом HTTP 302 с этим адресом в заголовке `Location` или покажите кнопку «Оплатить» с этой ссылкой. Покупатель должен заплатить до `expires_at` — это 20 минут после создания платежа. Потом платёж отменится. После оплаты или отказа от неё AvantPay откроет у покупателя `return_url` — ровно тот адрес, что вы передали, без параметров. Поэтому указывайте в `return_url` адрес страницы заказа, например `https://example.com/orders/1`. На этой странице возьмите `id` платежа из заказа и запросите платёж (шаг 4). Возврат на `return_url` не означает, что платёж оплачен. ### Шаг 4. Узнайте результат Используйте оба способа: уведомления и запрос платежа. #### Уведомления Когда платёж оплатят или отменят, AvantPay отправит на адрес из шага 1 POST-запрос с JSON. Здесь покупатель оплатил картой: ```json { "type": "notification", "event": "payment.succeeded", "object": { "id": "2f9b6c1e-4d7a-4c3b-9e8f-1a2b3c4d5e6f", "status": "succeeded", "paid": true, "amount": { "value": "100.00", "currency": "RUB" }, "income_amount": { "value": "97.50", "currency": "RUB" }, "description": "Заказ №1", "recipient": { "account_id": "33661", "gateway_id": "6b1f3c0a-58e2-4f7d-9a41-0c2d7e5b8f93" }, "payment_method": { "type": "bank_card", "id": "2f9b6c1e-4d7a-4c3b-9e8f-1a2b3c4d5e6f", "saved": false, "status": "inactive", "title": "Банковская карта" }, "captured_at": "2026-10-18T10:17:02.418Z", "created_at": "2026-10-18T10:15:30.123Z", "expires_at": "2026-10-18T10:35:30.123Z", "confirmation": { "type": "redirect", "confirmation_url": "https://avantpay.io/link/Aq91c7d", "return_url": "https://example.com/orders/1" }, "test": false, "refundable": false, "metadata": { "email": "buyer@example.com", "order_id": "1" } } } ``` Что сделать, когда пришло уведомление: 1. Возьмите `object.id`. 2. Запросите этот платёж через API (запрос ниже). Решение принимайте по ответу API, а не по телу уведомления. Ответ 404 — платежа нет у магазина: ответьте 200 и ничего не делайте. 3. Если `status` равен `succeeded` и `amount` равен сумме заказа — отметьте заказ оплаченным атомарно (например, `UPDATE … WHERE paid_payment_id IS NULL`) и выполните его. Если заказ уже отмечен — ничего не делайте. Возврат покупателя на `return_url` приходит одновременно с уведомлением, поэтому без атомарности заказ выполнится дважды. 4. Ответьте на уведомление кодом HTTP 200 не позже чем через 10 секунд. Если не ответить, AvantPay будет повторять уведомление. Подробнее и готовые обработчики на Python, Node.js и PHP — на странице [Уведомления](https://avantpay.io/developers/webhooks/). #### Запрос платежа cURL: ```bash curl https://api.avantpay.io/v3/payments/2f9b6c1e-4d7a-4c3b-9e8f-1a2b3c4d5e6f \ -u "$AVANTPAY_SHOP_ID:$AVANTPAY_SECRET_KEY" ``` Python: ```python import os from typing import Optional import requests SHOP_ID = os.environ["AVANTPAY_SHOP_ID"] SECRET_KEY = os.environ["AVANTPAY_SECRET_KEY"] def get_payment(payment_id: str) -> Optional[dict]: """Платёж из API; None — у магазина нет такого платежа (404)""" response = requests.get( f"https://api.avantpay.io/v3/payments/{payment_id}", auth=(SHOP_ID, SECRET_KEY), timeout=30, ) if response.status_code == 404: return None if response.status_code != 200: raise RuntimeError(f"AvantPay: HTTP {response.status_code}: {response.text}") return response.json() ``` Node.js: ```js const AUTH = "Basic " + Buffer.from(`${process.env.AVANTPAY_SHOP_ID}:${process.env.AVANTPAY_SECRET_KEY}`).toString("base64"); // Платёж из API; null — у магазина нет такого платежа (404) export async function getPayment(paymentId) { const response = await fetch(`https://api.avantpay.io/v3/payments/${encodeURIComponent(paymentId)}`, { headers: { Authorization: AUTH }, signal: AbortSignal.timeout(30000), }); if (response.status === 404) return null; const text = await response.text(); if (response.status !== 200) { throw new Error(`AvantPay: HTTP ${response.status}: ${text}`); } return JSON.parse(text); } ``` PHP: ```php getenv('AVANTPAY_SHOP_ID') . ':' . getenv('AVANTPAY_SECRET_KEY'), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode === 404) { return null; } if ($response === false || $httpCode !== 200) { throw new RuntimeException('AvantPay: HTTP ' . $httpCode . ': ' . $response); } return json_decode($response, true); } ``` Ответ — объект платежа в текущем состоянии, такой же, как в `object` уведомления. Запрашивайте платёж: - когда пришло уведомление; - когда покупатель вернулся на `return_url`; - когда запрос на создание платежа закончился ошибкой 500 или без ответа; - раз в несколько минут для сверки: уведомление может не дойти ([как сверяться](https://avantpay.io/developers/webhooks/#reconciliation)). #### Что делать по статусу | `status` | Что значит | Что делать | |---|---|---| | `pending` | Покупатель ещё не заплатил. | Ждать. Заказ не выполнять. | | `succeeded` | Оплачен. | Сверить `amount` с суммой заказа и выполнить заказ один раз. | | `canceled` | Не оплачен: истёк срок, банк отказал или сработала проверка. | Заказ не выполнять. Чтобы покупатель заплатил снова, создать новый платёж с новым ключом идемпотентности. | Все подробности — на странице [Статусы платежа](https://avantpay.io/developers/statuses/). ### Готово Интеграция работает, если: - платёж создаётся с email покупателя, а покупатель попадает на страницу оплаты; - после оплаты ваш сервер получает `payment.succeeded`, проверяет платёж запросом и выполняет заказ один раз; - после отмены ваш сервер получает `payment.canceled` и не выполняет заказ; - покупатель, вернувшийся на `return_url`, видит правильный результат. Перед запуском пройдите [чеклист](https://avantpay.io/developers/checklist/). --- ## Справочник API Страница: https://avantpay.io/developers/api/ Все методы и объекты API для приёма платежей — на одной странице, подряд. Меню показывает, где вы сейчас, а адрес страницы меняется на ссылку раздела: её можно скопировать и отправить. Адрес API — `https://api.avantpay.io/v3`. Примеры берут ID магазина и секретный ключ из переменных окружения `AVANTPAY_SHOP_ID` и `AVANTPAY_SECRET_KEY`. Язык примеров выберите в любом блоке кода — он запомнится на всех страницах. ### Аутентификация Адрес API — `https://api.avantpay.io/v3`. Пути методов добавляются к нему: `https://api.avantpay.io/v3/payments`. Запросы — только по HTTPS. Отправляйте запросы только со своего сервера. Не вызывайте API из браузера или мобильного приложения: в их код пришлось бы положить секретный ключ, а там его может прочитать кто угодно. Браузер такой запрос и не выполнит: API не разрешает запросы со страниц сайтов (CORS выключен). API использует HTTP Basic: - логин — **ID магазина**, например `33661`; - пароль — **секретный ключ**. В cURL это параметр `-u`. Без cURL передайте заголовок `Authorization` со словом `Basic` и строкой `:<секретный ключ>` в Base64. Почти все HTTP-библиотеки собирают этот заголовок сами, если передать логин и пароль. Ключи OAuth (`Authorization: Bearer …`) AvantPay не поддерживает: такой запрос получит 401. #### Где взять ID магазина и секретный ключ 1. Войдите в [личный кабинет](https://avantpay.io/lk/). 2. Откройте **Ещё → Разработчикам → API-ключ** ([открыть сразу](https://avantpay.io/lk/?path=/api/key)). На телефоне вместо «Ещё» — «Меню». 3. ID магазина и адрес API — в блоке «Авторизация запросов». 4. Нажмите **Создать ключ**. Ключ показывается целиком только один раз. Скопируйте его сразу. Если раздела «Разработчикам» нет, попросите владельца магазина добавить вас в разделе «Пользователи» с ролью «Разработчик». #### Как хранить ключ - Храните ключ в переменных окружения или в хранилище секретов сервера. - Не кладите ключ в код, в репозиторий, в логи и в письма. - Если ключ мог утечь, нажмите **Перевыпустить** в том же разделе. Старый ключ сразу перестанет работать. Замените ключ на сервере. - Тем же ключом AvantPay подписывает уведомления. После перевыпуска уведомления подписываются новым ключом. #### Права ключа В блоке «Права ключа» видно, что можно делать этим ключом. Для создания платежей нужно право «Платежи». - Права включает поддержка AvantPay. В личном кабинете их можно только посмотреть. Чтобы включить право, напишите на support@avantpay.io. - Без права «Платежи» запрос `POST /v3/payments` получит ошибку 403 `forbidden`. - Запросы `GET` (получить платёж, список платежей) работают с любым действующим ключом магазина. #### Лимит запросов - Не больше 50 запросов в секунду на магазин. Сверх лимита API ответит 429 `too_many_requests`. - В ответе 429 есть поле `retry_after` — через сколько миллисекунд повторить, и заголовок `Retry-After` — то же в секундах. - Повторяйте запрос на создание платежа после 429 с тем же `Idempotence-Key`. #### Ошибки аутентификации | HTTP | `code` | Когда | Что делать | |---|---|---|---| | 401 | `invalid_credentials` | Нет заголовка `Authorization`, неверный ID магазина или ключ, ключ перевыпущен или удалён, заголовок `Bearer`. `parameter` — `Authorization`. | Проверьте ID магазина и ключ. Если ключ потерян — перевыпустите его. | | 403 | `forbidden` | У ключа нет права «Платежи» или магазин не активен. | Напишите в поддержку: support@avantpay.io. | | 429 | `too_many_requests` | С вашего IP-адреса за минуту пришло больше 20 запросов с неверными ключами. До конца этой минуты API отвечает 429 на все запросы с этого адреса. | Исправьте ключи и повторите через `retry_after` миллисекунд. | Проверка ключей: запрос возвращает последний платёж магазина и ничего не меняет. ```bash curl "https://api.avantpay.io/v3/payments?limit=1" \ -u "$AVANTPAY_SHOP_ID:$AVANTPAY_SECRET_KEY" ``` Ответ 200 — ключи верные. Ответ 401 `invalid_credentials` — неверный ID магазина или ключ: ```json { "type": "error", "id": "0d8e4c1a-6b2f-4e3d-9a7c-5f1b2e3d4c5a", "code": "invalid_credentials", "description": "Invalid credentials: use HTTP Basic authentication with the shop ID as the username and the API secret key as the password", "parameter": "Authorization" } ``` Заголовок `Authorization` без cURL: ```text Authorization: Basic MzM2NjE6dGVzdF9zZWNyZXRfa2V5 ``` Здесь `MzM2NjE6dGVzdF9zZWNyZXRfa2V5` — это Base64 от строки `33661:test_secret_key`. Ответ 429 — больше 50 запросов в секунду: ```json { "type": "error", "id": "5a1c3e2b-7d4f-4a6b-8c9d-0e1f2a3b4c5d", "code": "too_many_requests", "description": "Too many requests: the shop exceeded the request rate limit. Retry later with the same Idempotence-Key", "retry_after": 1000 } ``` ### Идемпотентность Ключ идемпотентности — заголовок `Idempotence-Key` — защищает от двойных платежей. Если запрос на создание платежа не получил ответа, его можно повторить с тем же ключом. AvantPay вернёт уже созданный платёж, а второй не создаст. | Вопрос | Ответ | |---|---| | Где нужен | `POST /v3/payments` — создание платежа. Обязателен. | | Значение | Любая строка до 64 символов. Лучше всего — UUID версии 4, например `7f1c9a52-3d4b-4e8f-a1b2-c3d4e5f6a7b8`. | | Без заголовка | Ошибка 400 `invalid_request`, `"parameter": "Idempotence-Key"`. | | В GET-запросах | Не нужен. Если передать, API его не учтёт. | | Сколько действует | 24 часа. Повтор с тем же ключом в течение 24 часов вернёт тот же платёж. Позже — ошибка 400. | API принимает и заголовок `Idempotency-Key` с тем же смыслом. Передавайте только один из двух. Оба заголовка с разными значениями — ошибка 400. #### Правила 1. Для каждой новой попытки оплаты создавайте новый ключ. 2. Создайте ключ до первого запроса и сохраните его в заказе. Тогда при повторе, даже после перезапуска сервера, вы возьмёте тот же ключ. 3. Повторяйте запрос с тем же ключом и тем же телом, если ответа нет, запрос оборвался по таймауту, пришёл ответ 500, 429 или 202. Повторяйте 1–2 минуты. Потом покажите покупателю «Платёж создаётся» и продолжайте повторять в фоне с тем же ключом, но не дольше 24 часов. 4. Никогда не используйте один ключ для двух разных платежей. Тот же ключ с другим телом запроса (например, с другой суммой) — ошибка 400, второй платёж не создастся. 5. Повтор с тем же ключом позже чем через 24 часа — ошибка 400. Чтобы узнать, чем кончился тот запрос, запросите платёж по `id` из заказа. Если `id` в заказе нет (ответ так и не пришёл), найдите платёж в [списке платежей](https://avantpay.io/developers/api/#get_payments_list) за время создания: у него в `metadata.order_id` ваш номер заказа. #### Что ответит API | Ситуация | Ответ | Что делать | |---|---|---| | Новый ключ | 200, новый платёж | Сохранить `id` платежа. | | Тот же ключ и то же тело, первый запрос выполнен, прошло меньше 24 часов | 200, тот же платёж | Ничего: второго платежа нет. Текущий статус узнавайте запросом `GET /v3/payments/{payment_id}`. | | Тот же ключ, первый запрос ещё выполняется | API ждёт его до 25 секунд и отвечает 200 с тем же платежом. Если не дождался — 202, тело в примерах. | При 202 подождать `retry_after` миллисекунд и повторить с тем же ключом. | | Тот же ключ, первый запрос сорвался после того, как платёж был записан | 200, тот же платёж со `"status": "canceled"` и без `confirmation_url`: платёж не создался до конца | Не отправлять покупателя на оплату. Создать новый платёж с новым ключом. | | Тот же ключ, другое тело | 400 `invalid_request`, `"parameter": "Idempotence-Key"` | Для нового платежа — новый ключ. | | Тот же ключ, прошло больше 24 часов | 400 `invalid_request`, `"parameter": "Idempotence-Key"` | Найти платёж по `id` из заказа. Для новой попытки оплаты — новый ключ. | | Нет ключа, ключ длиннее 64 символов или два заголовка с разными значениями | 400 `invalid_request`, `"parameter": "Idempotence-Key"` | Передать один ключ до 64 символов. | #### Если ответа нет Если запрос на создание платежа закончился таймаутом, обрывом связи или ответом 500, результат неизвестен: платёж мог создаться. Не создавайте платёж с новым ключом. Повторите тот же запрос с тем же ключом: придёт уже созданный платёж или он создастся сейчас. Проверьте `status` в ответе на повтор. `pending` — отправьте покупателя на `confirmation.confirmation_url`. `canceled` — платёж не создался до конца: создайте новый с новым ключом. Выполните эту команду дважды. Второй ответ вернёт тот же платёж с тем же `id`. ```bash curl https://api.avantpay.io/v3/payments \ -X POST \ -u "$AVANTPAY_SHOP_ID:$AVANTPAY_SECRET_KEY" \ -H "Idempotence-Key: 5b8e2f3a-6c1d-4e7f-9a0b-1c2d3e4f5a6b" \ -H "Content-Type: application/json" \ -d '{ "amount": { "value": "100.00", "currency": "RUB" }, "capture": true, "confirmation": { "type": "redirect", "return_url": "https://example.com/orders/2" }, "description": "Заказ №2", "metadata": { "email": "buyer@example.com", "order_id": "2" } }' ``` Ответ 202 — запрос с этим ключом ещё выполняется: ```json { "type": "processing", "description": "Request accepted", "retry_after": 2000 } ``` Ответ 400 на тот же ключ с другим телом: ```json { "type": "error", "id": "9c3b7a1e-2f4d-4b6a-8e5c-1d2f3a4b5c6d", "code": "invalid_request", "description": "Idempotence key duplicated: this key was already used with a different request body. Use a new Idempotence-Key for a new request", "parameter": "Idempotence-Key" } ``` ### Формат запросов и ответов #### Запросы - Тело POST-запроса — JSON с заголовком `Content-Type: application/json`. Без этого заголовка API ответит 415. - Тело — не больше 5 МБ. Больше — ответ 413. Запрос на создание платежа обычно меньше 1 КБ. - Поля, которых нет в справочнике, API обычно отбрасывает без ошибки. Поэтому проверяйте имена полей: поле с опечаткой тоже отбросится. Некоторые поля дают ошибку 400: например, `receipt` (чеки AvantPay не отправляет) или `order_id` в корне запроса (номер заказа передавайте в `metadata.order_id`). - Ставьте таймаут HTTP-клиента не меньше 30 секунд. Если ответа нет, повторите запрос с тем же `Idempotence-Key`. - Заголовок `X-Request-Id` можно передать свой: латинские буквы, цифры и символы `_`, `.`, `:`, `-`, до 128 символов. Иначе API создаст его сам. #### Ответы - Ответ — всегда JSON с заголовком `Content-Type: application/json; charset=utf-8`. - Успешный ответ — сам объект, без обёрток. Например, объект платежа. - Ошибка — объект с полями `type`, `id`, `code`, `description` и иногда `parameter`. Все коды — в разделе [Ошибки](https://avantpay.io/developers/api/#errors). - Если у поля нет значения, поля нет в ответе. Значения `null` в ответах не бывает. - В ответе могут появиться новые поля. Код не должен падать на незнакомом поле. - В каждом ответе есть заголовок `X-Request-Id`. Пишите его в свои логи: по нему поддержка найдёт запрос. #### Сумма Сумма — объект из двух строк: `value` и `currency`. - `value` — строка, рубли и копейки через точку. Не больше двух знаков после точки: `"100.00"`, `"1500.50"`. Без пробелов и разделителей тысяч. - Число (`100`) API тоже примет, но передавайте строку: с числом легко потерять копейки. - Не передавайте копейки отдельно и не умножайте сумму на 100. - Больше двух знаков после точки — ошибка 400. - `currency` — `"RUB"`. - В ответах `value` — всегда строка с двумя знаками после точки. #### Дата и время Даты — в формате ISO 8601, по UTC, с миллисекундами и буквой `Z` на конце: `2026-10-18T10:15:30.123Z`. В запросах передавайте даты в том же формате. #### metadata `metadata` — ваши данные к платежу: email покупателя, номер заказа, ID покупателя и т. п. В запросе на создание платежа `metadata` обязательна, и в ней обязателен ключ `email` — email покупателя. Покупатель из Telegram — `@telegram.org`. - Не больше 16 ключей. - Имя ключа — до 32 символов. - Значение — строка до 512 символов. Число или `true`/`false` API сохранит строкой: `1042` вернётся как `"1042"`. - Ключ со значением `null` API не сохраняет. - Массив или вложенный объект в значении — ошибка 400. - `metadata` возвращается в объекте платежа и в уведомлениях. - Не храните в `metadata` секреты и данные карт. Сумма: ```json { "value": "100.00", "currency": "RUB" } ``` `metadata` с email покупателя и номером заказа: ```json { "email": "buyer@example.com", "order_id": "123", "customer_id": "1042" } ``` Покупатель из Telegram: ```json { "email": "123456789@telegram.org", "order_id": "124" } ``` ### Ошибки Если запрос не выполнен, API отвечает кодом HTTP 400 или выше и объектом ошибки. Объект ошибки приходит на любой ответ 4xx и 5xx, в том числе на неверный адрес (404), неверный метод HTTP (405) и запрос без JSON (415). #### Объект ошибки | Поле | Тип | Есть всегда | Описание | |---|---|---|---| | `type` | string | да | Всегда `error`. Пример: `"error"`. | | `id` | string | да | ID ошибки. Укажите его, если пишете в поддержку. Пример: `"6daac9fa-342d-4264-91c5-b5eafd1a0010"`. | | `code` | string | да | Код ошибки из таблицы ниже. По нему решайте, что делать. Пример: `"invalid_request"`. | | `description` | string | да | Что не так и как исправить. На английском. Пишите в логи, покупателю не показывайте. Пример: `"Invalid request"`. | | `parameter` | string | нет | Какое поле, заголовок или параметр неверны. Например: `amount.value`, `metadata.email`, `Idempotence-Key`, `Authorization`, `payment_id`, `limit`. Пример: `"metadata.email"`. | | `retry_after` | integer | нет | Через сколько миллисекунд повторить запрос. Есть у ответов 429. Пример: `1000`. | #### Коды | HTTP | `code` | Когда | Что делать | |---|---|---|---| | 200 | — | Запрос выполнен. Создание платежа — тоже 200. | — | | 202 | — | Запрос с этим ключом идемпотентности ещё выполняется. Тело: `{"type": "processing", "description": "Request accepted", "retry_after": 2000}`. | Подождать `retry_after` миллисекунд и повторить с тем же `Idempotence-Key`. | | 400 | `invalid_request` | Неверный запрос: нет поля или заголовка (например, `metadata.email`), неверный формат, сумма вне лимитов, ключ идемпотентности уже использован с другим телом, тело не JSON. | Исправить запрос по `parameter` и `description`. Без исправления не повторять. | | 400 | `not_supported` | Передано поле или значение, которого нет в API (например, `receipt` или `"capture": false`), или вызван метод, которого нет. | Убрать поле или не вызывать метод. Без исправления не повторять. | | 401 | `invalid_credentials` | Нет заголовка `Authorization`, неверный ID магазина или секретный ключ, заголовок `Bearer`. `parameter` — `Authorization`. | Проверить ID магазина и ключ. Без исправления не повторять. | | 403 | `forbidden` | У ключа нет права «Платежи», магазин не активен или сценарий `qr` не включён для магазина. | Написать в поддержку: support@avantpay.io. | | 404 | `not_found` | Платежа с таким `id` нет у вашего магазина (`parameter` — `payment_id`). Или такого адреса нет. | Проверить `id` и адрес. | | 405 | `invalid_request` | Метод HTTP не подходит для этого адреса. Например, `PUT /v3/payments`. Допустимые методы — в заголовке `Allow`. | Исправить метод. | | 413 | `invalid_request` | Тело запроса больше 5 МБ. | Уменьшить тело. | | 415 | `invalid_request` | У POST-запроса нет заголовка `Content-Type: application/json`. | Передать JSON с этим заголовком. | | 429 | `too_many_requests` | Больше 50 запросов в секунду от магазина или много запросов с неверными ключами с одного IP-адреса. | Подождать `retry_after` миллисекунд (заголовок `Retry-After` — в секундах) и повторить с тем же `Idempotence-Key`. | | 500 | `internal_server_error` | Сбой у AvantPay. Результат запроса неизвестен: платёж мог создаться. | Повторить с тем же `Idempotence-Key` или проверить платёж запросом GET. | Другой код 5xx или ответ не в JSON обрабатывайте как 500. #### Когда повторять запрос | Ответ | Повторять? | |---|---| | 400, 401, 403, 404, 405, 413, 415 | Нет. Сначала исправьте запрос, ключи или настройки. Исправленный запрос на создание платежа отправляйте с новым ключом идемпотентности. | | 202, 429 | Да, через `retry_after` миллисекунд, с тем же `Idempotence-Key`. | | 500, таймаут, обрыв связи | Да, с тем же `Idempotence-Key` и тем же телом. Повторяйте 1–2 минуты, потом покажите покупателю «Платёж создаётся» и продолжайте в фоне с тем же ключом, но не дольше 24 часов. | Не создавайте платёж с новым ключом, пока не знаете, чем кончился прежний запрос. Иначе покупатель может заплатить дважды. Ошибка 400 — в запросе нет email покупателя: ```json { "type": "error", "id": "6daac9fa-342d-4264-91c5-b5eafd1a0010", "code": "invalid_request", "description": "metadata.email is required: pass the buyer's email address, for a Telegram buyer use @telegram.org", "parameter": "metadata.email" } ``` Ошибка 400 — поле, которого нет в API: ```json { "type": "error", "id": "1f4e2a7c-8b3d-4c6e-9a5f-0d1c2b3a4e5f", "code": "not_supported", "description": "receipt is not supported: AvantPay does not register fiscal receipts (54-FZ), remove receipt from the request", "parameter": "receipt" } ``` Ошибка 429: ```json { "type": "error", "id": "5a1c3e2b-7d4f-4a6b-8c9d-0e1f2a3b4c5d", "code": "too_many_requests", "description": "Too many requests: the shop exceeded the request rate limit. Retry later with the same Idempotence-Key", "retry_after": 1000 } ``` ### Объект платежа Объект платежа — это платёж и всё, что о нём известно: сумма, статус, способ оплаты, ссылка на оплату, email покупателя. API возвращает этот объект: - в ответе на [создание платежа](https://avantpay.io/developers/api/#create_payment) — `POST /v3/payments`; - в ответе на [запрос платежа](https://avantpay.io/developers/api/#get_payment) — `GET /v3/payments/{payment_id}`; - в поле `items` [списка платежей](https://avantpay.io/developers/api/#get_payments_list) — `GET /v3/payments`; - в поле `object` [уведомления](https://avantpay.io/developers/api/#notification_object). #### Правила чтения объекта - Если у поля нет значения, поля нет в объекте. Значения `null` не бывает. Перед чтением необязательного поля проверьте, что оно есть. - В объекте могут появиться новые поля. Код не должен падать на незнакомом поле или незнакомом значении. - Выполняйте заказ, только если `status` равен `succeeded`, а `amount.value` и `amount.currency` равны сумме и валюте заказа ([правила](https://avantpay.io/developers/statuses/#rules)). - Суммы — строки с двумя знаками после точки: `"100.00"`. Даты — ISO 8601 по UTC с миллисекундами: `"2026-10-18T10:15:30.123Z"`. #### Поля | Поле | Тип | Есть всегда | Описание | |---|---|---|---| | `id` | string | да | ID платежа, UUID из 36 символов. Сохраните его в заказе: по нему вы получите платёж. Пример: `"2f9b6c1e-4d7a-4c3b-9e8f-1a2b3c4d5e6f"`. | | `status` | string | да | Статус платежа: `pending`, `succeeded` или `canceled`. Что делать в каждом — на странице [Статусы платежа](https://avantpay.io/developers/statuses/). Пример: `"succeeded"`. | | `paid` | boolean | да | `true`, если платёж оплачен (`status` равен `succeeded`). Иначе `false`. Пример: `true`. | | `amount` | object | да | Сумма платежа — ровно та, что вы передали при создании. Покупатель платит эту сумму. | | `amount.value` | string | да | Сумма строкой с двумя знаками после точки. Пример: `"1000.00"`. | | `amount.currency` | string | да | Валюта: `RUB`. Пример: `"RUB"`. | | `income_amount` | object | нет | Сколько получит магазин: `amount` минус комиссия AvantPay. Бывает только у платежей со статусом `succeeded`. Может не быть и у них, например пока AvantPay проверяет оплату. Не решайте по этому полю, оплачен ли платёж. | | `income_amount.value` | string | да | Сумма строкой с двумя знаками после точки. Пример: `"975.00"`. | | `income_amount.currency` | string | да | Валюта: `RUB`. Пример: `"RUB"`. | | `description` | string | нет | Описание, которое вы передали при создании. Есть, только если вы его передали. Пример: `"Заказ №123"`. | | `recipient` | object | да | Получатель платежа — ваш магазин. | | `recipient.account_id` | string | да | ID магазина строкой. Пример: `"33661"`. | | `recipient.gateway_id` | string | нет | ID счёта в AvantPay, на который зачисляются деньги. Появляется, когда выбран способ оплаты. Для интеграции не нужен. Пример: `"6b1f3c0a-58e2-4f7d-9a41-0c2d7e5b8f93"`. | | `payment_method` | object | нет | Способ оплаты. Появляется, когда способ выбран: сразу, если вы передали `payment_method_data`, иначе — когда покупатель выберет способ на странице оплаты. Если магазину подключены другие способы, `type` может быть и другим — на незнакомом значении код не должен падать. Поля зависят от `payment_method.type`. Варианты: `sbp` — СБП, поля: `type`, `id`, `saved`, `status`, `title`; `bank_card` — банковская карта, поля: `type`, `id`, `saved`, `status`, `title`; `crypto` — криптовалюта, поля: `type`, `id`, `saved`, `status`, `title`. Поля каждого варианта — в строках ниже с пометкой «Вариант». | | `payment_method.type` | string | да | Вариант `sbp` (СБП): поле есть, когда `payment_method.type` — `sbp`. Значение — `sbp`. Пример: `"sbp"`. | | `payment_method.id` | string | да | Вариант `sbp` (СБП): поле есть, когда `payment_method.type` — `sbp`. ID способа оплаты. Всегда равен `id` платежа: способы оплаты не сохраняются. Пример: `"2f9b6c1e-4d7a-4c3b-9e8f-1a2b3c4d5e6f"`. | | `payment_method.saved` | boolean | да | Вариант `sbp` (СБП): поле есть, когда `payment_method.type` — `sbp`. Сохранён ли способ для следующих платежей. Всегда `false`. Пример: `false`. | | `payment_method.status` | string | да | Вариант `sbp` (СБП): поле есть, когда `payment_method.type` — `sbp`. Статус способа оплаты. Всегда `inactive`: способ не сохранён. Пример: `"inactive"`. | | `payment_method.title` | string | нет | Вариант `sbp` (СБП): поле есть, когда `payment_method.type` — `sbp`. Название способа для людей. Пример: `"СБП"`. | | `payment_method.type` | string | да | Вариант `bank_card` (банковская карта): поле есть, когда `payment_method.type` — `bank_card`. Значение — `bank_card`. Пример: `"bank_card"`. | | `payment_method.id` | string | да | Вариант `bank_card` (банковская карта): поле есть, когда `payment_method.type` — `bank_card`. ID способа оплаты. Всегда равен `id` платежа: способы оплаты не сохраняются. Пример: `"2f9b6c1e-4d7a-4c3b-9e8f-1a2b3c4d5e6f"`. | | `payment_method.saved` | boolean | да | Вариант `bank_card` (банковская карта): поле есть, когда `payment_method.type` — `bank_card`. Сохранён ли способ для следующих платежей. Всегда `false`. Пример: `false`. | | `payment_method.status` | string | да | Вариант `bank_card` (банковская карта): поле есть, когда `payment_method.type` — `bank_card`. Статус способа оплаты. Всегда `inactive`: способ не сохранён. Пример: `"inactive"`. | | `payment_method.title` | string | нет | Вариант `bank_card` (банковская карта): поле есть, когда `payment_method.type` — `bank_card`. Название способа для людей. Пример: `"Банковская карта"`. | | `payment_method.type` | string | да | Вариант `crypto` (криптовалюта): поле есть, когда `payment_method.type` — `crypto`. Значение — `crypto`. Пример: `"crypto"`. | | `payment_method.id` | string | да | Вариант `crypto` (криптовалюта): поле есть, когда `payment_method.type` — `crypto`. ID способа оплаты. Всегда равен `id` платежа: способы оплаты не сохраняются. Пример: `"2f9b6c1e-4d7a-4c3b-9e8f-1a2b3c4d5e6f"`. | | `payment_method.saved` | boolean | да | Вариант `crypto` (криптовалюта): поле есть, когда `payment_method.type` — `crypto`. Сохранён ли способ для следующих платежей. Всегда `false`. Пример: `false`. | | `payment_method.status` | string | да | Вариант `crypto` (криптовалюта): поле есть, когда `payment_method.type` — `crypto`. Статус способа оплаты. Всегда `inactive`: способ не сохранён. Пример: `"inactive"`. | | `payment_method.title` | string | нет | Вариант `crypto` (криптовалюта): поле есть, когда `payment_method.type` — `crypto`. Название способа для людей. Пример: `"Криптовалюта"`. | | `captured_at` | string | нет | Когда платёж оплачен. Бывает только у платежей со статусом `succeeded`. Пример: `"2026-10-18T10:17:02.418Z"`. | | `created_at` | string | да | Когда платёж создан. Пример: `"2026-10-18T10:15:30.123Z"`. | | `expires_at` | string | да | До какого момента покупатель может заплатить: 20 минут после создания платежа. Потом платёж станет `canceled`, но не в ту же секунду: сначала AvantPay проверит, не прошла ли оплата. Обычно `canceled` приходит в течение 10 минут после `expires_at`, в редких случаях — до 2 часов. Пример: `"2026-10-18T10:35:30.123Z"`. | | `confirmation` | object | да | Как покупатель платит — сценарий, который вы передали при создании. Поля зависят от `confirmation.type`. Варианты: `redirect` — страница оплаты, поля: `type`, `confirmation_url`, `return_url`; `qr` — QR-код СБП, поля: `type`, `confirmation_data`. Поля каждого варианта — в строках ниже с пометкой «Вариант». | | `confirmation.type` | string | да | Вариант `redirect` (страница оплаты): поле есть, когда `confirmation.type` — `redirect`. `redirect` — страница оплаты AvantPay. Пример: `"redirect"`. | | `confirmation.confirmation_url` | string | нет | Вариант `redirect` (страница оплаты): поле есть, когда `confirmation.type` — `redirect`. Ссылка на страницу оплаты. Отправьте покупателя по ней, только если `status` равен `pending`. Может не быть у платежа, который отменён при создании. Пример: `"https://avantpay.io/link/Aq91c7d"`. | | `confirmation.return_url` | string | нет | Вариант `redirect` (страница оплаты): поле есть, когда `confirmation.type` — `redirect`. Адрес возврата покупателя, который вы передали. Пример: `"https://example.com/orders/123"`. | | `confirmation.type` | string | да | Вариант `qr` (QR-код СБП): поле есть, когда `confirmation.type` — `qr`. `qr` — QR-код СБП. Пример: `"qr"`. | | `confirmation.confirmation_data` | string | нет | Вариант `qr` (QR-код СБП): поле есть, когда `confirmation.type` — `qr`. Ссылка для QR-кода СБП. Сделайте из неё QR-код, на телефоне ссылку можно просто открыть. Сразу после создания здесь может быть ссылка на страницу оплаты AvantPay (`https://avantpay.io/link/…`): QR-код из неё откроет страницу оплаты в браузере телефона. Потом значение может смениться на ссылку СБП (`https://qr.nspk.ru/…`). Показывайте QR-код из последнего полученного объекта. Пример: `"https://qr.nspk.ru/AD10006M8KHABCDEFG"`. | | `test` | boolean | да | Тестовый ли платёж. Всегда `false`: тестового режима нет. Пример: `false`. | | `refunded_amount` | object | нет | Сколько денег вернули покупателю. Есть, только если был возврат. Вернуть деньги можно только в личном кабинете. Статус после возврата остаётся `succeeded`. | | `refunded_amount.value` | string | да | Сумма строкой с двумя знаками после точки. Пример: `"300.00"`. | | `refunded_amount.currency` | string | да | Валюта: `RUB`. Пример: `"RUB"`. | | `refundable` | boolean | да | Можно ли вернуть платёж через API. Всегда `false`: вернуть деньги можно только в личном кабинете. Пример: `false`. | | `metadata` | object | да | Ваши данные из запроса на создание: email покупателя, номер заказа и другие ключи, которые вы передали. Значения — строки. | | `metadata.email` | string | да | Email покупателя, который вы передали. Пример: `"buyer@example.com"`. | | `metadata.order_id` | string | нет | Номер заказа, если вы его передали. Пример: `"123"`. | | `cancellation_details` | object | нет | Кто отменил платёж и почему. Бывает только у платежей со статусом `canceled`. | | `cancellation_details.party` | string | да | Кто отменил: `merchant`, `avantpay` или `payment_network`. Что значит каждое — на странице [Статусы платежа](https://avantpay.io/developers/statuses/#cancellation). Пример: `"avantpay"`. | | `cancellation_details.reason` | string | да | Причина отмены. Все причины — на странице [Статусы платежа](https://avantpay.io/developers/statuses/#reasons). Пример: `"expired_on_confirmation"`. | Оплачен: Покупатель оплатил картой: ```json { "id": "2f9b6c1e-4d7a-4c3b-9e8f-1a2b3c4d5e6f", "status": "succeeded", "paid": true, "amount": { "value": "1000.00", "currency": "RUB" }, "income_amount": { "value": "975.00", "currency": "RUB" }, "description": "Заказ №123", "recipient": { "account_id": "33661", "gateway_id": "6b1f3c0a-58e2-4f7d-9a41-0c2d7e5b8f93" }, "payment_method": { "type": "bank_card", "id": "2f9b6c1e-4d7a-4c3b-9e8f-1a2b3c4d5e6f", "saved": false, "status": "inactive", "title": "Банковская карта" }, "captured_at": "2026-10-18T10:17:02.418Z", "created_at": "2026-10-18T10:15:30.123Z", "expires_at": "2026-10-18T10:35:30.123Z", "confirmation": { "type": "redirect", "confirmation_url": "https://avantpay.io/link/Aq91c7d", "return_url": "https://example.com/orders/123" }, "test": false, "refundable": false, "metadata": { "email": "buyer@example.com", "order_id": "123" } } ``` Новый: Только что созданный платёж. Способ оплаты не передан, поэтому нет `payment_method` и `recipient.gateway_id`: ```json { "id": "e5b1c9d3-7a2f-4e8b-9c6d-1f0a2b3c4d5e", "status": "pending", "paid": false, "amount": { "value": "500.00", "currency": "RUB" }, "description": "Заказ №127", "recipient": { "account_id": "33661" }, "created_at": "2026-10-18T11:02:10.517Z", "expires_at": "2026-10-18T11:22:10.517Z", "confirmation": { "type": "redirect", "confirmation_url": "https://avantpay.io/link/Kd73pQe", "return_url": "https://example.com/orders/127" }, "test": false, "refundable": false, "metadata": { "email": "123456789@telegram.org", "order_id": "127" } } ``` Отменён: Покупатель не заплатил до `expires_at`: ```json { "id": "8d0e4b52-7c19-4a6e-b3f1-5e2a9c7d1b40", "status": "canceled", "paid": false, "amount": { "value": "500.00", "currency": "RUB" }, "description": "Заказ №125", "recipient": { "account_id": "33661" }, "created_at": "2026-10-18T12:00:00.000Z", "expires_at": "2026-10-18T12:20:00.000Z", "confirmation": { "type": "redirect", "confirmation_url": "https://avantpay.io/link/Zt5mW2r", "return_url": "https://example.com/orders/125" }, "test": false, "refundable": false, "metadata": { "email": "buyer@example.com", "order_id": "125" }, "cancellation_details": { "party": "avantpay", "reason": "expired_on_confirmation" } } ``` Возврат: Покупатель заплатил по СБП, часть денег ему вернули в личном кабинете. Статус остался `succeeded`: ```json { "id": "c4a8e2f1-9b3d-4f6a-8e1c-7d2b5a9f3e60", "status": "succeeded", "paid": true, "amount": { "value": "250.00", "currency": "RUB" }, "income_amount": { "value": "243.75", "currency": "RUB" }, "description": "Заказ №122", "recipient": { "account_id": "33661", "gateway_id": "6b1f3c0a-58e2-4f7d-9a41-0c2d7e5b8f93" }, "payment_method": { "type": "sbp", "id": "c4a8e2f1-9b3d-4f6a-8e1c-7d2b5a9f3e60", "saved": false, "status": "inactive", "title": "СБП" }, "captured_at": "2026-10-17T18:41:09.004Z", "created_at": "2026-10-17T18:39:55.871Z", "expires_at": "2026-10-17T18:59:55.871Z", "confirmation": { "type": "redirect", "confirmation_url": "https://avantpay.io/link/Pq28vXn", "return_url": "https://example.com/orders/122" }, "test": false, "refunded_amount": { "value": "50.00", "currency": "RUB" }, "refundable": false, "metadata": { "email": "buyer@example.com", "order_id": "122" } } ``` ### Создать платёж ```text POST https://api.avantpay.io/v3/payments ``` Создаёт платёж и возвращает [объект платежа](https://avantpay.io/developers/api/#payment_object) со статусом `pending` и ссылкой на оплату `confirmation.confirmation_url`. > **Email покупателя обязателен.** > > Передайте его в `metadata.email`. Покупатель из Telegram — `@telegram.org`, например `123456789@telegram.org`. #### Заголовки | Заголовок | Обязательно | Значение | |---|---|---| | `Authorization` | да | HTTP Basic: ID магазина и секретный ключ. В cURL — `-u "$AVANTPAY_SHOP_ID:$AVANTPAY_SECRET_KEY"`. | | `Idempotence-Key` | да | Ключ идемпотентности: уникальная строка до 64 символов, лучше UUID. Новый для каждой попытки оплаты, тот же при повторе запроса. [Подробнее](https://avantpay.io/developers/api/#idempotence) | | `Content-Type` | да | `application/json` | #### Параметры запроса | Поле | Тип | Обязательно | Описание | |---|---|---|---| | `amount` | object | да | Сумма платежа. Покупатель заплатит ровно её. Комиссию AvantPay платит магазин: она вычитается из этой суммы. | | `amount.value` | string | да | Сумма строкой, рубли и копейки через точку. Больше нуля, не больше двух знаков после точки. Должна укладываться в лимиты способов оплаты — они есть в личном кабинете: **Ещё → Способы оплаты**. Пример: `"1000.00"`. | | `amount.currency` | string | да | Валюта: `RUB`. Пример: `"RUB"`. | | `capture` | boolean | да | Указывается `true`. Пример: `true`. | | `confirmation` | object | да | Как покупатель будет платить. Поля зависят от `confirmation.type`. Варианты: `redirect` — страница оплаты, поля: `type`, `return_url`; `qr` — QR-код СБП, поля: `type`, `return_url`. Поля каждого варианта — в строках ниже с пометкой «Вариант». | | `confirmation.type` | string | да | Вариант `redirect` (страница оплаты): поле есть, когда `confirmation.type` — `redirect`. `redirect` — покупатель переходит на страницу оплаты AvantPay. Пример: `"redirect"`. | | `confirmation.return_url` | string | да | Вариант `redirect` (страница оплаты): поле есть, когда `confirmation.type` — `redirect`. Куда вернуть покупателя после оплаты или отказа от неё. Адрес `http://` или `https://`, до 2048 символов. AvantPay открывает его как есть, без параметров, поэтому укажите в нём свой заказ. Пример: `"https://example.com/orders/123"`. | | `confirmation.type` | string | да | Вариант `qr` (QR-код СБП): поле есть, когда `confirmation.type` — `qr`. `qr` — вы показываете покупателю QR-код СБП. Только вместе с `"payment_method_data": {"type": "sbp"}` ([подробнее](https://avantpay.io/developers/api/#create_payment-qr)). Пример: `"qr"`. | | `confirmation.return_url` | string | да | Вариант `qr` (QR-код СБП): поле есть, когда `confirmation.type` — `qr`. Куда вернуть покупателя после оплаты или отказа от неё. Адрес `http://` или `https://`, до 2048 символов. AvantPay открывает его как есть, без параметров, поэтому укажите в нём свой заказ. Пример: `"https://example.com/orders/123"`. | | `description` | string | нет | Описание платежа, до 128 символов. Его видит покупатель на странице оплаты и вы в личном кабинете. Пример: `"Заказ №123"`. | | `payment_method_data` | object | нет | Способ оплаты. Если не передать, покупатель выберет способ на странице оплаты. Способ должен быть включён у магазина. Поля зависят от `payment_method_data.type`. Варианты: `sbp` — СБП, поля: `type`; `bank_card` — банковская карта, поля: `type`; конкретный способ (`type` — ID), поля: `type`. Поля каждого варианта — в строках ниже с пометкой «Вариант». | | `payment_method_data.type` | string | да | Вариант `sbp` (СБП): поле есть, когда `payment_method_data.type` — `sbp`. Значение — `sbp`. Пример: `"sbp"`. | | `payment_method_data.type` | string | да | Вариант `bank_card` (банковская карта): поле есть, когда `payment_method_data.type` — `bank_card`. Значение — `bank_card`. Пример: `"bank_card"`. | | `payment_method_data.type` | string | да | Вариант «конкретный способ»: ID способа оплаты магазина, до 64 символов. Нужен, только если у магазина включено несколько способов одного типа: тогда на `sbp` или `bank_card` API ответит 400 и перечислит ID в `description`. Пример: `"sbp-2"`. | | `metadata` | object | да | Ваши данные к платежу. Обязателен ключ `email`. Всего до 16 ключей, имя ключа до 32 символов, значение — строка до 512 символов. Вернутся в объекте платежа и в уведомлениях. Подробнее — в разделе [metadata](https://avantpay.io/developers/api/#format-metadata). | | `metadata.email` | string | да | Email покупателя, до 254 символов. Покупатель из Telegram — `@telegram.org`, например `123456789@telegram.org`. Нет email или адрес неверный — ошибка 400 `invalid_request`, `parameter` — `metadata.email`. Пример: `"buyer@example.com"`. | | `metadata.order_id` | string | нет | Ваш номер заказа. Вернётся в объекте платежа и в уведомлениях. API не проверяет, что номер уникален: от двойного платежа защищает ключ идемпотентности. Пример: `"123"`. | #### Варианты ##### Покупатель выбирает способ сам Не передавайте `payment_method_data`. На странице оплаты покупатель увидит все способы, которые включены у магазина. В ответе не будет `payment_method` и `recipient.gateway_id` — они появятся, когда покупатель выберет способ. ##### Сразу СБП или карта Передайте `"payment_method_data": {"type": "sbp"}` или `"payment_method_data": {"type": "bank_card"}`. Покупатель сразу попадёт на оплату этим способом. ```bash curl https://api.avantpay.io/v3/payments \ -X POST \ -u "$AVANTPAY_SHOP_ID:$AVANTPAY_SECRET_KEY" \ -H "Idempotence-Key: 9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b" \ -H "Content-Type: application/json" \ -d '{ "amount": { "value": "1500.00", "currency": "RUB" }, "capture": true, "confirmation": { "type": "redirect", "return_url": "https://example.com/orders/126" }, "description": "Заказ №126", "payment_method_data": { "type": "sbp" }, "metadata": { "email": "123456789@telegram.org", "order_id": "126" } }' ``` Ответ: ```json { "id": "c7e2a9f4-1b3d-4c5e-8f6a-9b0c1d2e3f4a", "status": "pending", "paid": false, "amount": { "value": "1500.00", "currency": "RUB" }, "description": "Заказ №126", "recipient": { "account_id": "33661", "gateway_id": "6b1f3c0a-58e2-4f7d-9a41-0c2d7e5b8f93" }, "payment_method": { "type": "sbp", "id": "c7e2a9f4-1b3d-4c5e-8f6a-9b0c1d2e3f4a", "saved": false, "status": "inactive", "title": "СБП" }, "created_at": "2026-10-18T11:30:00.000Z", "expires_at": "2026-10-18T11:50:00.000Z", "confirmation": { "type": "redirect", "confirmation_url": "https://avantpay.io/link/Wm4tR8b", "return_url": "https://example.com/orders/126" }, "test": false, "refundable": false, "metadata": { "email": "123456789@telegram.org", "order_id": "126" } } ``` Здесь покупатель пришёл из Telegram: его email — `123456789@telegram.org`. ##### QR-код СБП Вы показываете покупателю QR-код, он сканирует его телефоном и платит. Правила: - сценарий `qr` работает только с `"payment_method_data": {"type": "sbp"}`; - `return_url` обязателен, как и у `redirect`; - сценарий включает поддержка AvantPay. Если он не включён, API ответит 403 `forbidden`, `"parameter": "confirmation.type"`. ```bash curl https://api.avantpay.io/v3/payments \ -X POST \ -u "$AVANTPAY_SHOP_ID:$AVANTPAY_SECRET_KEY" \ -H "Idempotence-Key: 3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a99" \ -H "Content-Type: application/json" \ -d '{ "amount": { "value": "500.00", "currency": "RUB" }, "capture": true, "confirmation": { "type": "qr", "return_url": "https://example.com/orders/124" }, "description": "Заказ №124", "payment_method_data": { "type": "sbp" }, "metadata": { "email": "buyer@example.com", "order_id": "124" } }' ``` Ответ: ```json { "id": "450d5a05-e377-4946-9e58-2627cae999b4", "status": "pending", "paid": false, "amount": { "value": "500.00", "currency": "RUB" }, "description": "Заказ №124", "recipient": { "account_id": "33661", "gateway_id": "6b1f3c0a-58e2-4f7d-9a41-0c2d7e5b8f93" }, "payment_method": { "type": "sbp", "id": "450d5a05-e377-4946-9e58-2627cae999b4", "saved": false, "status": "inactive", "title": "СБП" }, "created_at": "2026-10-18T11:02:10.517Z", "expires_at": "2026-10-18T11:22:10.517Z", "confirmation": { "type": "qr", "confirmation_data": "https://avantpay.io/link/Vb5nE2k" }, "test": false, "refundable": false, "metadata": { "email": "buyer@example.com", "order_id": "124" } } ``` Сделайте QR-код из строки `confirmation.confirmation_data` и покажите его покупателю. На телефоне эту ссылку можно просто открыть. Сразу после создания в `confirmation_data` может быть ссылка на страницу оплаты AvantPay, как в примере выше: QR-код из неё откроет страницу оплаты в браузере телефона. Потом значение может смениться на ссылку СБП (`https://qr.nspk.ru/…`). Показывайте QR-код из последнего полученного объекта платежа. #### Что сделать после ответа 1. Сохраните `id` платежа в заказе. 2. Если `status` равен `pending` и есть `confirmation.confirmation_url`, отправьте покупателя по этой ссылке. Если повтор запроса вернул `canceled` без ссылки, платёж не создался до конца: создайте новый с новым ключом идемпотентности ([подробнее](https://avantpay.io/developers/api/#idempotence-responses)). 3. Дождитесь [уведомления](https://avantpay.io/developers/webhooks/) и проверьте платёж запросом [`GET /v3/payments/{payment_id}`](https://avantpay.io/developers/api/#get_payment). Если запрос оборвался, пришёл ответ 500, 429 или 202, повторите его с тем же `Idempotence-Key` и тем же телом. Повторяйте 1–2 минуты. Потом покажите покупателю «Платёж создаётся» и продолжайте повторять в фоне с тем же ключом, но не дольше 24 часов. Примеры на Python, Node.js и PHP так и устроены: ключ идемпотентности они получают от вас, а перед отправкой покупателя на оплату проверяют `status` и ссылку. #### Ошибки Формат ошибок и общие коды — в разделе [Ошибки](https://avantpay.io/developers/api/#errors). Ошибки этого метода: | HTTP | `code` | `parameter` | Когда | Что делать | |---|---|---|---|---| | 400 | `invalid_request` | `metadata.email` | Нет `metadata`, нет ключа `email`, он пустой или это не email (больше 254 символов, неверный адрес `…@telegram.org`). | Передать email покупателя. Покупатель из Telegram — `@telegram.org`. | | 400 | `invalid_request` | `Idempotence-Key` | Нет ключа, ключ длиннее 64 символов, переданы `Idempotence-Key` и `Idempotency-Key` с разными значениями, тот же ключ уже использован с другим телом запроса или больше 24 часов назад. | Передать один ключ до 64 символов. Для нового платежа — новый ключ. | | 400 | `invalid_request` | `capture` | Нет `capture` или это не `true`/`false` (например, строка `"true"`). | Передать `"capture": true`. | | 400 | `not_supported` | `capture` | `"capture": false`. | Передать `"capture": true`. | | 400 | `invalid_request` | `amount`, `amount.value` | Нет суммы, сумма не число, больше двух знаков после точки, не больше нуля или вне лимитов способов оплаты. | Исправить сумму. Лимиты — в личном кабинете, **Ещё → Способы оплаты**. | | 400 | `invalid_request` | `amount.currency` | Валюта не `RUB` или у магазина нет способов оплаты в этой валюте. | Передать `"RUB"`. | | 400 | `invalid_request` | — | У магазина не включён ни один способ оплаты. | Написать в поддержку: support@avantpay.io. | | 400 | `invalid_request` | `confirmation`, `confirmation.type`, `confirmation.return_url` | Нет `confirmation`, неизвестный тип, нет `return_url`, `return_url` — не адрес `http://` или `https://` либо длиннее 2048 символов, `qr` без `"payment_method_data": {"type": "sbp"}`. | Передать `return_url`. Для `qr` — ещё способ `sbp`. | | 400 | `not_supported` | `confirmation.type` | Тип, кроме `redirect` и `qr`. | Передать `redirect` или `qr`. | | 400 | `invalid_request` | `payment_method_data.type` | Такого способа нет у магазина, он не работает в рублях, или включено несколько способов этого типа. | Передать `sbp` или `bank_card`, идентификатор способа из `description` либо не передавать `payment_method_data`. | | 400 | `invalid_request` | `description` | Длиннее 128 символов. | Сократить описание. | | 400 | `invalid_request` | `metadata` | `metadata` — не объект, больше 16 ключей, длинное имя ключа или значение, значение — массив или объект. | Привести к правилам из раздела [metadata](https://avantpay.io/developers/api/#format-metadata). | | 400 | `invalid_request` | `metadata.<ключ>` | Для магазина по договору с AvantPay обязательны ещё некоторые ключи `metadata`, а их нет. | Передать ключ, названный в `parameter`. | | 400 | `invalid_request` | `order_id`, `webhook_url`, `expires_at`, `avantpay` | Передано поле, которого в запросе нет. | Номер заказа передать в `metadata.order_id`. Адрес уведомлений указать в личном кабинете. Срок оплаты не передавать: он 20 минут. | | 400 | `not_supported` | имя поля | Передано поле, которого нет в API: `receipt`, `save_payment_method` со значением `true`, `payment_method_id`, `payment_method_data.card` и другие. | Убрать поле из запроса. | | 400 | `invalid_request` | — | Тело запроса — не JSON. | Передать правильный JSON. | | 401 | `invalid_credentials` | `Authorization` | Неверный ID магазина или ключ. | Проверить ключи. | | 403 | `forbidden` | — | У ключа нет права «Платежи» или магазин не активен. | Написать в поддержку: support@avantpay.io. | | 403 | `forbidden` | `confirmation.type` | Сценарий (обычно `qr`) не включён для магазина. | Использовать `redirect` или написать в поддержку. | | 413 | `invalid_request` | — | Тело запроса больше 5 МБ. | Уменьшить тело. | | 415 | `invalid_request` | — | Нет заголовка `Content-Type: application/json`. | Передать заголовок. | | 429 | `too_many_requests` | — | Больше 50 запросов в секунду. | Подождать `retry_after` миллисекунд и повторить с тем же ключом. | | 202 | — | — | Запрос с этим ключом ещё выполняется. Тело: `{"type": "processing", ...}`. | Подождать `retry_after` миллисекунд и повторить с тем же ключом. | | 500 | `internal_server_error` | — | Сбой у AvantPay. Платёж мог создаться. | Повторить с тем же ключом. | Если ответа нет совсем (таймаут, обрыв связи), поступите так же, как при ответе 500: повторите запрос с тем же `Idempotence-Key` и тем же телом. Если повтор вернул `"status": "canceled"`, платёж не создался до конца: создайте новый с новым ключом. cURL: ```bash curl https://api.avantpay.io/v3/payments \ -X POST \ -u "$AVANTPAY_SHOP_ID:$AVANTPAY_SECRET_KEY" \ -H "Idempotence-Key: e1d2c3b4-a5f6-4789-8abc-def012345678" \ -H "Content-Type: application/json" \ -d '{ "amount": { "value": "1000.00", "currency": "RUB" }, "capture": true, "confirmation": { "type": "redirect", "return_url": "https://example.com/orders/123" }, "description": "Заказ №123", "metadata": { "email": "buyer@example.com", "order_id": "123" } }' ``` Python: ```python import os import requests SHOP_ID = os.environ["AVANTPAY_SHOP_ID"] SECRET_KEY = os.environ["AVANTPAY_SECRET_KEY"] def create_payment(order_id: str, email: str, amount: str, return_url: str, idempotence_key: str) -> dict: response = requests.post( "https://api.avantpay.io/v3/payments", auth=(SHOP_ID, SECRET_KEY), headers={"Idempotence-Key": idempotence_key}, json={ "amount": {"value": amount, "currency": "RUB"}, "capture": True, "confirmation": {"type": "redirect", "return_url": return_url}, "description": f"Заказ №{order_id}", # email покупателя обязателен; покупатель из Telegram — f"{telegram_id}@telegram.org" "metadata": {"email": email, "order_id": order_id}, }, timeout=30, ) if response.status_code != 200: raise RuntimeError(f"AvantPay: HTTP {response.status_code}: {response.text}") payment = response.json() # Повтор с тем же ключом может вернуть платёж, который не создался до конца (canceled, без ссылки): # тогда создайте новый платёж с новым ключом идемпотентности if payment["status"] != "pending" or "confirmation_url" not in payment.get("confirmation", {}): raise RuntimeError(f"AvantPay: платёж {payment['id']} в статусе {payment['status']}, ссылки на оплату нет") return payment # покупателя — на ["confirmation"]["confirmation_url"] ``` Node.js: ```js const AUTH = "Basic " + Buffer.from(`${process.env.AVANTPAY_SHOP_ID}:${process.env.AVANTPAY_SECRET_KEY}`).toString("base64"); export async function createPayment({ orderId, email, amount, returnUrl, idempotenceKey }) { const response = await fetch("https://api.avantpay.io/v3/payments", { method: "POST", headers: { Authorization: AUTH, "Idempotence-Key": idempotenceKey, "Content-Type": "application/json", }, body: JSON.stringify({ amount: { value: amount, currency: "RUB" }, capture: true, confirmation: { type: "redirect", return_url: returnUrl }, description: `Заказ №${orderId}`, // email покупателя обязателен; покупатель из Telegram — `${telegramId}@telegram.org` metadata: { email, order_id: orderId }, }), signal: AbortSignal.timeout(30000), }); const text = await response.text(); if (response.status !== 200) { throw new Error(`AvantPay: HTTP ${response.status}: ${text}`); } const payment = JSON.parse(text); // Повтор с тем же ключом может вернуть платёж, который не создался до конца (canceled, без ссылки): // тогда создайте новый платёж с новым ключом идемпотентности if (payment.status !== "pending" || !payment.confirmation?.confirmation_url) { throw new Error(`AvantPay: платёж ${payment.id} в статусе ${payment.status}, ссылки на оплату нет`); } return payment; // покупателя — на .confirmation.confirmation_url } ``` PHP: ```php true, CURLOPT_USERPWD => getenv('AVANTPAY_SHOP_ID') . ':' . getenv('AVANTPAY_SECRET_KEY'), CURLOPT_HTTPHEADER => [ 'Idempotence-Key: ' . $idempotenceKey, 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'amount' => ['value' => $amount, 'currency' => 'RUB'], 'capture' => true, 'confirmation' => ['type' => 'redirect', 'return_url' => $returnUrl], 'description' => 'Заказ №' . $orderId, // email покупателя обязателен; покупатель из Telegram — $telegramId . '@telegram.org' 'metadata' => ['email' => $email, 'order_id' => $orderId], ], JSON_UNESCAPED_UNICODE), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($response === false || $httpCode !== 200) { throw new RuntimeException('AvantPay: HTTP ' . $httpCode . ': ' . $response); } $payment = json_decode($response, true); // Повтор с тем же ключом может вернуть платёж, который не создался до конца (canceled, без ссылки): // тогда создайте новый платёж с новым ключом идемпотентности if ($payment['status'] !== 'pending' || empty($payment['confirmation']['confirmation_url'])) { throw new RuntimeException('AvantPay: платёж ' . $payment['id'] . ' в статусе ' . $payment['status'] . ', ссылки на оплату нет'); } return $payment; // покупателя — на ['confirmation']['confirmation_url'] } ``` Ответ — HTTP 200 и объект платежа: ```json { "id": "3d6f0a8e-5b2c-4e1f-9a7d-2c4b6e8f0a1b", "status": "pending", "paid": false, "amount": { "value": "1000.00", "currency": "RUB" }, "description": "Заказ №123", "recipient": { "account_id": "33661" }, "created_at": "2026-10-18T10:15:30.123Z", "expires_at": "2026-10-18T10:35:30.123Z", "confirmation": { "type": "redirect", "confirmation_url": "https://avantpay.io/link/Aq91c7d", "return_url": "https://example.com/orders/123" }, "test": false, "refundable": false, "metadata": { "email": "buyer@example.com", "order_id": "123" } } ``` Ответ 400 — неверный email покупателя: ```json { "type": "error", "id": "2b7d4e9a-1c3f-4a5b-8d6e-7f0a1b2c3d4e", "code": "invalid_request", "description": "metadata.email must be the buyer's email address (up to 254 characters), for a Telegram buyer use @telegram.org", "parameter": "metadata.email" } ``` ### Получить платёж ```text GET https://api.avantpay.io/v3/payments/{payment_id} ``` Возвращает [объект платежа](https://avantpay.io/developers/api/#payment_object) в текущем состоянии. Это главный способ узнать, оплачен ли платёж. Заголовок `Idempotence-Key` этому методу не нужен. Тела у запроса нет. #### Когда запрашивать - Пришло [уведомление](https://avantpay.io/developers/webhooks/). Решение по заказу принимайте по ответу этого запроса. - Покупатель вернулся на `return_url`. Покажите ему результат по статусу из ответа. - Запрос на создание платежа закончился ошибкой 500 или без ответа. - Для сверки по расписанию: уведомление может не дойти ([как сверяться](https://avantpay.io/developers/webhooks/#reconciliation)). Если опрашиваете платёж, пока покупатель на странице оплаты, делайте это не чаще одного раза в 5 секунд и только пока статус `pending`. #### Параметры пути | Параметр | Тип | Обязательно | Описание | |---|---|---|---| | `payment_id` | string | да | `id` платежа из ответа на его создание. Пример: `2f9b6c1e-4d7a-4c3b-9e8f-1a2b3c4d5e6f`. | #### Что делать по статусу | `status` | Что делать | |---|---| | `pending` | Покупатель ещё не заплатил. Ждите уведомления. Заказ не выполняйте. | | `succeeded` | Оплачен. Сверьте `amount` с суммой заказа и выполните заказ, если ещё не выполнили. | | `canceled` | Не оплачен. Заказ не выполняйте. Для новой попытки оплаты создайте новый платёж с новым ключом идемпотентности. | Подробнее — на странице [Статусы платежа](https://avantpay.io/developers/statuses/). #### Ошибки | HTTP | `code` | Когда | Что делать | |---|---|---|---| | 404 | `not_found` | Платежа с таким `id` нет у вашего магазина. Платёж другого магазина тоже даёт 404. `parameter` — `payment_id`. | Проверить `id`. | | 401 | `invalid_credentials` | Неверный ID магазина или ключ. | Проверить ключи. | | 403 | `forbidden` | Магазин не активен. | Написать в поддержку: support@avantpay.io. | | 429 | `too_many_requests` | Больше 50 запросов в секунду. | Повторить через `retry_after` миллисекунд. | | 500 | `internal_server_error` | Сбой у AvantPay. | Повторить запрос позже. | cURL: ```bash curl https://api.avantpay.io/v3/payments/2f9b6c1e-4d7a-4c3b-9e8f-1a2b3c4d5e6f \ -u "$AVANTPAY_SHOP_ID:$AVANTPAY_SECRET_KEY" ``` Python: ```python import os from typing import Optional import requests SHOP_ID = os.environ["AVANTPAY_SHOP_ID"] SECRET_KEY = os.environ["AVANTPAY_SECRET_KEY"] def get_payment(payment_id: str) -> Optional[dict]: """Платёж из API; None — у магазина нет такого платежа (404)""" response = requests.get( f"https://api.avantpay.io/v3/payments/{payment_id}", auth=(SHOP_ID, SECRET_KEY), timeout=30, ) if response.status_code == 404: return None if response.status_code != 200: raise RuntimeError(f"AvantPay: HTTP {response.status_code}: {response.text}") return response.json() ``` Node.js: ```js const AUTH = "Basic " + Buffer.from(`${process.env.AVANTPAY_SHOP_ID}:${process.env.AVANTPAY_SECRET_KEY}`).toString("base64"); // Платёж из API; null — у магазина нет такого платежа (404) export async function getPayment(paymentId) { const response = await fetch(`https://api.avantpay.io/v3/payments/${encodeURIComponent(paymentId)}`, { headers: { Authorization: AUTH }, signal: AbortSignal.timeout(30000), }); if (response.status === 404) return null; const text = await response.text(); if (response.status !== 200) { throw new Error(`AvantPay: HTTP ${response.status}: ${text}`); } return JSON.parse(text); } ``` PHP: ```php getenv('AVANTPAY_SHOP_ID') . ':' . getenv('AVANTPAY_SECRET_KEY'), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode === 404) { return null; } if ($response === false || $httpCode !== 200) { throw new RuntimeException('AvantPay: HTTP ' . $httpCode . ': ' . $response); } return json_decode($response, true); } ``` Ответ — HTTP 200 и объект платежа. Здесь покупатель оплатил картой: ```json { "id": "2f9b6c1e-4d7a-4c3b-9e8f-1a2b3c4d5e6f", "status": "succeeded", "paid": true, "amount": { "value": "1000.00", "currency": "RUB" }, "income_amount": { "value": "975.00", "currency": "RUB" }, "description": "Заказ №123", "recipient": { "account_id": "33661", "gateway_id": "6b1f3c0a-58e2-4f7d-9a41-0c2d7e5b8f93" }, "payment_method": { "type": "bank_card", "id": "2f9b6c1e-4d7a-4c3b-9e8f-1a2b3c4d5e6f", "saved": false, "status": "inactive", "title": "Банковская карта" }, "captured_at": "2026-10-18T10:17:02.418Z", "created_at": "2026-10-18T10:15:30.123Z", "expires_at": "2026-10-18T10:35:30.123Z", "confirmation": { "type": "redirect", "confirmation_url": "https://avantpay.io/link/Aq91c7d", "return_url": "https://example.com/orders/123" }, "test": false, "refundable": false, "metadata": { "email": "buyer@example.com", "order_id": "123" } } ``` ### Список платежей ```text GET https://api.avantpay.io/v3/payments ``` Возвращает платежи вашего магазина от новых к старым. Можно отфильтровать по дате, статусу и способу оплаты. Список отдаётся страницами. Заголовок `Idempotence-Key` этому методу не нужен. Тела у запроса нет. #### Параметры запроса Все параметры необязательные. Передавайте их в строке запроса, каждый — не больше одного раза. Незнакомые параметры API не учитывает. Если в дате нет часового пояса, это время по UTC. | Параметр | Тип | Обязательно | Описание | |---|---|---|---| | `limit` | integer | нет | Сколько платежей на странице: от 1 до 100. По умолчанию 10. Пример: `10`. | | `cursor` | string | нет | Значение `next_cursor` из предыдущего ответа — чтобы получить следующую страницу. Передавайте как есть. Пример: `MjAyNi0xMC0xN1QxODozOTo1NS44NzFa`. | | `status` | string | нет | Только платежи с этим статусом: `pending`, `succeeded` или `canceled`. Пример: `succeeded`. | | `payment_method` | string | нет | Только платежи этим способом: `sbp` или `bank_card`. Пример: `sbp`. | | `created_at.gte` | string | нет | Созданы в этот момент или позже. Дата ISO 8601. Пример: `2026-10-01T00:00:00.000Z`. | | `created_at.gt` | string | нет | Созданы позже этого момента. Пример: `2026-10-01T00:00:00.000Z`. | | `created_at.lte` | string | нет | Созданы в этот момент или раньше. Пример: `2026-10-31T23:59:59.999Z`. | | `created_at.lt` | string | нет | Созданы раньше этого момента. Пример: `2026-11-01T00:00:00.000Z`. | | `captured_at.gte` | string | нет | Оплачены в этот момент или позже. Пример: `2026-10-01T00:00:00.000Z`. | | `captured_at.gt` | string | нет | Оплачены позже этого момента. Пример: `2026-10-01T00:00:00.000Z`. | | `captured_at.lte` | string | нет | Оплачены в этот момент или раньше. Пример: `2026-10-31T23:59:59.999Z`. | | `captured_at.lt` | string | нет | Оплачены раньше этого момента. Пример: `2026-11-01T00:00:00.000Z`. | Фильтра по номеру заказа нет: запрос с `order_id` получит ошибку 400 `not_supported`. Номер заказа храните в `metadata.order_id`, а платёж получайте по `id`, который сохранили в заказе: [`GET /v3/payments/{payment_id}`](https://avantpay.io/developers/api/#get_payment). #### Ответ | Поле | Тип | Есть всегда | Описание | |---|---|---|---| | `type` | string | да | Всегда `list`. Пример: `"list"`. | | `items` | array | да | Платежи — [объекты платежа](https://avantpay.io/developers/api/#payment_object), от новых к старым. Если ничего не нашлось — пустой массив `[]`. | | `next_cursor` | string | нет | Курсор следующей страницы. Нет на последней странице. Пример: `"MjAyNi0xMC0xN1QxODozOTo1NS44NzFa"`. | #### Как получить все страницы 1. Отправьте запрос с нужными фильтрами. 2. Если в ответе есть `next_cursor`, повторите запрос с теми же фильтрами и параметром `cursor=<значение next_cursor>`. 3. Повторяйте, пока в ответе не будет `next_cursor`. Новые платежи, созданные во время обхода, не сдвигают страницы: платежи не повторяются и не пропадают. Примеры на Python, Node.js и PHP обходят все страницы сами. #### Ошибки | HTTP | `code` | Когда | Что делать | |---|---|---|---| | 400 | `invalid_request` | `limit` — не целое число от 1 до 100, `cursor` — не из `next_cursor`, дата не в формате ISO 8601, `status` — незнакомое значение, параметр передан дважды. Имя параметра — в `parameter`. | Исправить параметр. | | 400 | `not_supported` | Передан `order_id`. `parameter` — `order_id`. | Убрать параметр. Получать платёж по `id` из заказа. | | 401 | `invalid_credentials` | Неверный ID магазина или ключ. | Проверить ключи. | | 403 | `forbidden` | Магазин не активен. | Написать в поддержку: support@avantpay.io. | | 429 | `too_many_requests` | Больше 50 запросов в секунду. | Повторить через `retry_after` миллисекунд. | | 500 | `internal_server_error` | Сбой у AvantPay. | Повторить запрос позже. | Оплаченные платежи с 1 октября 2026 года, по 2 на странице: cURL: ```bash curl "https://api.avantpay.io/v3/payments?status=succeeded&created_at.gte=2026-10-01T00:00:00.000Z&limit=2" \ -u "$AVANTPAY_SHOP_ID:$AVANTPAY_SECRET_KEY" ``` Python: ```python import os from typing import Iterator import requests SHOP_ID = os.environ["AVANTPAY_SHOP_ID"] SECRET_KEY = os.environ["AVANTPAY_SECRET_KEY"] def list_payments(filters: dict) -> Iterator[dict]: """Платежи по фильтрам со всех страниц: list_payments({"status": "succeeded", "created_at.gte": "…"})""" params = {**filters, "limit": 100} while True: response = requests.get( "https://api.avantpay.io/v3/payments", auth=(SHOP_ID, SECRET_KEY), params=params, timeout=30, ) if response.status_code != 200: raise RuntimeError(f"AvantPay: HTTP {response.status_code}: {response.text}") page = response.json() yield from page["items"] if "next_cursor" not in page: return params = {**filters, "limit": 100, "cursor": page["next_cursor"]} ``` Node.js: ```js const AUTH = "Basic " + Buffer.from(`${process.env.AVANTPAY_SHOP_ID}:${process.env.AVANTPAY_SECRET_KEY}`).toString("base64"); // Платежи по фильтрам со всех страниц: for await (const p of listPayments({ status: "succeeded" })) { … } export async function* listPayments(filters = {}) { let cursor; do { const params = new URLSearchParams({ ...filters, limit: "100", ...(cursor && { cursor }) }); const response = await fetch(`https://api.avantpay.io/v3/payments?${params}`, { headers: { Authorization: AUTH }, signal: AbortSignal.timeout(30000), }); const text = await response.text(); if (response.status !== 200) { throw new Error(`AvantPay: HTTP ${response.status}: ${text}`); } const page = JSON.parse(text); yield* page.items; cursor = page.next_cursor; } while (cursor); } ``` PHP: ```php 'succeeded']) as $payment) { … } function listPayments(array $filters): Generator { $cursor = null; do { $params = $filters + ['limit' => 100] + ($cursor !== null ? ['cursor' => $cursor] : []); $ch = curl_init('https://api.avantpay.io/v3/payments?' . http_build_query($params)); curl_setopt_array($ch, [ CURLOPT_USERPWD => getenv('AVANTPAY_SHOP_ID') . ':' . getenv('AVANTPAY_SECRET_KEY'), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($response === false || $httpCode !== 200) { throw new RuntimeException('AvantPay: HTTP ' . $httpCode . ': ' . $response); } $page = json_decode($response, true); foreach ($page['items'] as $payment) { yield $payment; } $cursor = $page['next_cursor'] ?? null; } while ($cursor !== null); } ``` Ответ — HTTP 200 и объект списка: ```json { "type": "list", "items": [ { "id": "2f9b6c1e-4d7a-4c3b-9e8f-1a2b3c4d5e6f", "status": "succeeded", "paid": true, "amount": { "value": "1000.00", "currency": "RUB" }, "income_amount": { "value": "975.00", "currency": "RUB" }, "description": "Заказ №123", "recipient": { "account_id": "33661", "gateway_id": "6b1f3c0a-58e2-4f7d-9a41-0c2d7e5b8f93" }, "payment_method": { "type": "bank_card", "id": "2f9b6c1e-4d7a-4c3b-9e8f-1a2b3c4d5e6f", "saved": false, "status": "inactive", "title": "Банковская карта" }, "captured_at": "2026-10-18T10:17:02.418Z", "created_at": "2026-10-18T10:15:30.123Z", "expires_at": "2026-10-18T10:35:30.123Z", "confirmation": { "type": "redirect", "confirmation_url": "https://avantpay.io/link/Aq91c7d", "return_url": "https://example.com/orders/123" }, "test": false, "refundable": false, "metadata": { "email": "buyer@example.com", "order_id": "123" } }, { "id": "c4a8e2f1-9b3d-4f6a-8e1c-7d2b5a9f3e60", "status": "succeeded", "paid": true, "amount": { "value": "250.00", "currency": "RUB" }, "income_amount": { "value": "243.75", "currency": "RUB" }, "description": "Заказ №122", "recipient": { "account_id": "33661", "gateway_id": "6b1f3c0a-58e2-4f7d-9a41-0c2d7e5b8f93" }, "payment_method": { "type": "sbp", "id": "c4a8e2f1-9b3d-4f6a-8e1c-7d2b5a9f3e60", "saved": false, "status": "inactive", "title": "СБП" }, "captured_at": "2026-10-17T18:41:09.004Z", "created_at": "2026-10-17T18:39:55.871Z", "expires_at": "2026-10-17T18:59:55.871Z", "confirmation": { "type": "redirect", "confirmation_url": "https://avantpay.io/link/Pq28vXn", "return_url": "https://example.com/orders/122" }, "test": false, "refunded_amount": { "value": "50.00", "currency": "RUB" }, "refundable": false, "metadata": { "email": "buyer@example.com", "order_id": "122" } } ], "next_cursor": "MjAyNi0xMC0xN1QxODozOTo1NS44NzFafGM0YThlMmYxLTliM2QtNGY2YS04ZTFjLTdkMmI1YTlmM2U2MA" } ``` Ответ 400 — фильтра по номеру заказа нет: ```json { "type": "error", "id": "4c2e8a1f-6b3d-4f5a-9e7c-0b1d2e3f4a5b", "code": "not_supported", "description": "order_id is not supported: save the payment id in your order and get the payment with GET /v3/payments/{payment_id}", "parameter": "order_id" } ``` ### Объект уведомления Уведомление — POST-запрос от AvantPay на ваш сервер, когда платёж оплачен или отменён. Адрес и события задаются в личном кабинете: **Ещё → Разработчикам → HTTP-уведомления**. Как включить, обработать и проверить уведомление — на странице [Уведомления](https://avantpay.io/developers/webhooks/). Запрос приходит с заголовком `Content-Type: application/json` и телом из трёх полей: | Поле | Тип | Есть всегда | Описание | |---|---|---|---| | `type` | string | да | Всегда `notification`. Пример: `"notification"`. | | `event` | string | да | Событие: `payment.succeeded` или `payment.canceled`. Пример: `"payment.succeeded"`. | | `object` | object | да | [Объект платежа](https://avantpay.io/developers/api/#payment_object) на момент события — такой же, как в ответе на `GET /v3/payments/{payment_id}`. | #### События | `event` | В личном кабинете | Когда приходит | |---|---|---| | `payment.succeeded` | Платёж оплачен | Платёж перешёл в статус `succeeded`. Может прийти и после `payment.canceled` — это [поздняя оплата](https://avantpay.io/developers/statuses/#late-payment). | | `payment.canceled` | Платёж отменён | Платёж перешёл в статус `canceled`: истёк срок оплаты, банк отказал или сработала проверка. | Если пришло уведомление с другим `event`, ответьте 200 и ничего не делайте. #### Заголовки | Заголовок | Что в нём | |---|---| | `Signature-Timestamp` | Время отправки, Unix-время в секундах. | | `Signature` | Подпись: `v1=` и HMAC-SHA256 строки `.<тело запроса>` секретным ключом API в шестнадцатеричном виде. Как проверить — в разделе [Подпись](https://avantpay.io/developers/webhooks/#signature). | Ответьте кодом 200 не позже чем через 10 секунд. Иначе AvantPay повторит уведомление ([расписание повторов](https://avantpay.io/developers/webhooks/#retries)). Платёж отменён: покупатель не заплатил вовремя. ```json { "type": "notification", "event": "payment.canceled", "object": { "id": "8d0e4b52-7c19-4a6e-b3f1-5e2a9c7d1b40", "status": "canceled", "paid": false, "amount": { "value": "500.00", "currency": "RUB" }, "description": "Заказ №125", "recipient": { "account_id": "33661" }, "created_at": "2026-10-18T12:00:00.000Z", "expires_at": "2026-10-18T12:20:00.000Z", "confirmation": { "type": "redirect", "confirmation_url": "https://avantpay.io/link/Zt5mW2r", "return_url": "https://example.com/orders/125" }, "test": false, "refundable": false, "metadata": { "email": "buyer@example.com", "order_id": "125" }, "cancellation_details": { "party": "avantpay", "reason": "expired_on_confirmation" } } } ``` Заголовки запроса: ```http POST /avantpay/webhook HTTP/1.1 Host: example.com Content-Type: application/json Signature-Timestamp: 1792324800 Signature: v1=07181d18b6de6cfe0c882fd02f7e40c8c0e735f19dc67062c39f57c8d75c995b ``` Пример уведомления `payment.succeeded` — в [быстром старте](https://avantpay.io/developers/quick-start/). --- ## Статусы платежа Страница: https://avantpay.io/developers/statuses/ Статус — в поле `status` [объекта платежа](https://avantpay.io/developers/api/#payment_object). Статусов три. | `status` | Что значит | Что делать магазину | Может измениться? | |---|---|---|---| | `pending` | Платёж создан. Покупатель ещё не заплатил. | Ждать. Заказ не выполнять. Если покупатель хочет вернуться к оплате до `expires_at`, отправить его на тот же `confirmation_url` — только если сумма заказа не менялась. Если менялась — создать новый платёж с новым ключом идемпотентности. | Да: станет `succeeded` или `canceled`. | | `succeeded` | Покупатель заплатил. `paid` равно `true`. | Сверить `amount` с суммой заказа и выполнить заказ один раз. | Нет. Возврат денег статус не меняет. | | `canceled` | Платёж не оплачен: истёк срок, банк отказал или платёж остановила проверка. Причина — в `cancellation_details`. | Заказ не выполнять. Чтобы покупатель заплатил снова, создать новый платёж с новым ключом идемпотентности. Связь заказа с этим платежом не удалять. | Редко: может стать `succeeded` — см. [поздняя оплата](https://avantpay.io/developers/statuses/#late-payment). | ### Как меняется статус ```text pending ──► succeeded │ ▲ │ │ поздняя оплата (редко) └──────► canceled ``` - Новый платёж всегда в статусе `pending`. - Из `pending` платёж переходит в `succeeded` или в `canceled`. - `succeeded` — окончательный статус. - `canceled` почти всегда окончательный. Исключение — поздняя оплата. ### Правила для кода 1. Выполняйте заказ только при `status` = `succeeded`. Не смотрите для этого на `paid`, `income_amount` или факт возврата покупателя на сайт. 2. Перед выполнением заказа сверьте `amount.value` и `amount.currency` с суммой заказа. Если сумма заказа изменилась после создания платежа (покупатель поменял корзину, открыл заказ в двух вкладках), создайте новый платёж с новым ключом идемпотентности. Платёж на старую сумму не засчитывайте; если его оплатят — верните деньги в личном кабинете. 3. Узнавайте статус запросом [`GET /v3/payments/{payment_id}`](https://avantpay.io/developers/api/#get_payment). Тело уведомления может быть устаревшим. 4. Выполняйте заказ ровно один раз. Отмечайте заказ оплаченным атомарно: условным `UPDATE` (например, `UPDATE orders SET paid_payment_id = … WHERE id = … AND paid_payment_id IS NULL`) или с блокировкой строки. Уведомление и возврат покупателя на `return_url` приходят одновременно: проверка «не выполнен — выполнить» без атомарности выполнит заказ дважды. 5. При `canceled` не выполняйте заказ, но и не удаляйте связь заказа с `id` платежа: платёж ещё может стать `succeeded`. 6. Не полагайтесь только на уведомления: если ваш сервер был недоступен дольше 24 часов, они не придут. Раз в несколько минут сверяйтесь с API — как, написано в разделе [Если уведомление не пришло](https://avantpay.io/developers/webhooks/#reconciliation). 7. Незнакомый статус считайте как `pending`: ждите и запросите платёж позже. ### Поздняя оплата: canceled → succeeded Иногда покупатель начинает оплату до `expires_at`, а банк подтверждает её позже. К этому моменту платёж уже `canceled`. Деньги покупателя пришли, поэтому платёж становится `succeeded`, и приходит уведомление `payment.succeeded` — даже если раньше приходило `payment.canceled`. Что делать: - выполните заказ, если он ещё актуален; - если заказ уже не актуален (товар продан, покупатель создал новый платёж и оплатил его), верните деньги покупателю в личном кабинете. Чтобы не пропустить позднюю оплату, обрабатывайте `payment.succeeded` для любого платежа заказа, даже отменённого, и не удаляйте связь заказа с `id` платежа. Сумму сверяйте так же: платёж на старую сумму заказа не засчитывайте, а деньги за него верните. Поэтому `canceled` — не всегда окончательный статус. Не снимайте резерв товара и не удаляйте заказ только потому, что пришло `payment.canceled`, если не готовы вернуть деньги при поздней оплате. ### Проверка оплаты в AvantPay Иногда AvantPay проверяет оплату вручную. Деньги покупателя уже пришли, поэтому платёж — `succeeded`, `paid` равно `true`, приходит `payment.succeeded`. Пока идёт проверка, в объекте нет `income_amount`. Выполняйте заказ как обычно — по статусу `succeeded`. Вопросы по такому платежу — в поддержку: support@avantpay.io, укажите `id` платежа. ### Кто отменил платёж У платежа со статусом `canceled` есть поле `cancellation_details`: ```json { "party": "avantpay", "reason": "expired_on_confirmation" } ``` `party` — кто отменил: | `party` | Кто | |---|---| | `merchant` | Магазин. Сейчас не встречается: отменить платёж через API нельзя. | | `avantpay` | AvantPay: истёк срок оплаты, платёж остановила проверка безопасности или его не удалось создать. | | `payment_network` | Банк покупателя, платёжная система или СБП. | Не стройте логику заказа на `party` и `reason`: для решения достаточно `status`. Эти поля нужны, чтобы объяснить покупателю, что случилось. ### Причины отмены `reason` — почему отменён платёж. Что бы ни было в `reason`, заказ не выполняйте. Если покупатель хочет заплатить снова, создайте новый платёж с новым ключом идемпотентности. | `reason` | Кто ставит | Что случилось | Что сказать покупателю | |---|---|---|---| | `3d_secure_failed` | `payment_network` | Покупатель не прошёл проверку 3-D Secure: ввёл неверный код или не дождался его. | Попробуйте оплатить ещё раз. Если не получится — обратитесь в свой банк. | | `call_issuer` | `payment_network` | Банк покупателя отказал без объяснения причины. | Обратитесь в свой банк или оплатите другим способом. | | `canceled_by_merchant` | `merchant` | Платёж отменил магазин. Сейчас не встречается. | — | | `card_expired` | `payment_network` | Истёк срок действия карты. | Оплатите другой картой или другим способом. | | `country_forbidden` | `payment_network` | Картой этой страны здесь заплатить нельзя. | Оплатите картой другого банка или другим способом. | | `expired_on_confirmation` | `avantpay`, `payment_network` | Покупатель не заплатил вовремя: прошёл `expires_at` или истёк срок QR-кода или ссылки у банка. | Время на оплату вышло. Начните оплату заново. | | `fraud_suspected` | `avantpay`, `payment_network` | Платёж остановлен из-за подозрения в мошенничестве. | Оплатите другим способом. | | `general_decline` | `avantpay`, `payment_network` | Причина не уточняется. | Попробуйте ещё раз или оплатите другим способом. | | `insufficient_funds` | `payment_network` | Не хватает денег на счёте покупателя. | Пополните счёт или оплатите другим способом. | | `internal_timeout` | `payment_network` | Банк или платёжная система не ответили вовремя. | Попробуйте оплатить ещё раз. | | `invalid_card_number` | `payment_network` | Неверный номер карты. | Проверьте номер карты и оплатите ещё раз. | | `invalid_csc` | `payment_network` | Неверный код CVV2/CVC2. | Проверьте код и оплатите ещё раз. | | `issuer_unavailable` | `payment_network` | Банк покупателя недоступен. | Попробуйте позже или оплатите другим способом. | | `payment_method_limit_exceeded` | `payment_network` | Исчерпан лимит платежей для способа оплаты или карты. | Попробуйте позже, на другую сумму или другим способом. | | `payment_method_restricted` | `payment_network` | Операции этим способом запрещены, например карта заблокирована. | Обратитесь в свой банк или оплатите другим способом. | Если придёт значение, которого нет в таблице, обработайте его как `general_decline`. ### Статусы в личном кабинете В личном кабинете статусов больше. Вот как они соответствуют статусам API: | В личном кабинете | `status` в API | |---|---| | Не оплачен, В обработке | `pending` | | Оплачен, Возврат в обработке, Возвращён частично, Возвращён | `succeeded` | | Ошибка оплаты, Просрочен, Отменён | `canceled` | --- ## Уведомления Страница: https://avantpay.io/developers/webhooks/ Уведомление — это POST-запрос от AvantPay на ваш сервер. Он приходит, когда платёж оплачен или отменён. Так вы узнаёте о результате без постоянных запросов к API. Тело, события и заголовки уведомления — в справочнике: [Объект уведомления](https://avantpay.io/developers/api/#notification_object). ### Как включить 1. Войдите в [личный кабинет](https://avantpay.io/lk/). 2. Откройте **Ещё → Разработчикам → HTTP-уведомления** ([открыть сразу](https://avantpay.io/lk/?path=/api/webhook)). На телефоне вместо «Ещё» — «Меню». 3. Нажмите **Изменить настройки**. 4. Укажите адрес, на который присылать уведомления, например `https://example.com/avantpay/webhook`. 5. Отметьте события «Платёж оплачен» и «Платёж отменён». Нажмите **Сохранить**. Адрес уведомлений задаётся только в личном кабинете: через API адрес и события не настраиваются. Адрес запоминается в платеже при создании. Поэтому включите уведомления до первого платежа: платежи, созданные, пока адрес не был указан, уведомлений не получат никогда — их результат узнавайте запросом [`GET /v3/payments/{payment_id}`](https://avantpay.io/developers/api/#get_payment). Если поменять адрес в личном кабинете, уведомления о платежах, созданных раньше, придут на прежний адрес. ### Требования к адресу - Только `https://`. - Доменное имя, не IP-адрес и не `localhost`. - Порт 443 или 8443. - Действующий сертификат от общепризнанного центра сертификации (например, Let's Encrypt), TLS 1.2 или новее. С самоподписанным или просроченным сертификатом уведомление не доставится. ### Какие уведомления приходят Для приёма платежей нужны два события: `payment.succeeded` (платёж оплачен) и `payment.canceled` (платёж отменён). Подробнее — в справочнике: [События](https://avantpay.io/developers/api/#notification_object-events). В личном кабинете есть и другие события. Для приёма платежей они не нужны: включайте только эти два. Если пришло уведомление с другим `event`, ответьте 200 и ничего не делайте. Уведомления приходят и о счетах, которые выставлены в личном кабинете, — даже если события выключены. У таких платежей в `metadata.order_id` — номер счёта вида `BI…`, а ещё в `metadata` есть служебные ключи AvantPay. Поэтому заказ ищите только по `id` платежа. Если платёж из уведомления не связан ни с одним вашим заказом или запрос платежа вернул 404, ответьте 200 и ничего не делайте. Не отвечайте 500: тогда AvantPay будет повторять уведомление сутки. ### Как обработать уведомление 1. Прочитайте тело запроса как есть, байтами, до разбора JSON. Это нужно для проверки подписи. 2. Проверьте подпись и время из заголовков `Signature` и `Signature-Timestamp` ([как](https://avantpay.io/developers/webhooks/#signature)). Не сошлось — ответьте 401 и ничего не делайте. 3. Разберите JSON. Если `event` не `payment.succeeded` и не `payment.canceled`, ответьте 200 и ничего не делайте. 4. Запросите платёж: `GET https://api.avantpay.io/v3/payments/{object.id}`. Дальше решайте по ответу этого запроса, а не по телу уведомления. Ответ 404 — такого платежа у магазина нет: ответьте 200 и ничего не делайте. 5. Найдите заказ по `id` платежа. Заказа нет (например, это счёт из личного кабинета) — ответьте 200 и ничего не делайте. 6. Если `status` равен `succeeded`, сверьте `amount.value` и `amount.currency` с суммой заказа. Совпали — отметьте заказ оплаченным атомарно и выполните его. Не совпали — заказ не выполняйте, деньги за этот платёж верните в личном кабинете ([почему](https://avantpay.io/developers/statuses/#rules)). 7. Если `status` равен `canceled` — не выполняйте заказ. 8. Ответьте кодом 200. Тело ответа не важно. Правила: - Отвечайте не позже чем через 10 секунд. Если выполнение заказа долгое, поставьте его в очередь и сразу ответьте 200. - Одно и то же уведомление может прийти несколько раз. Повторная обработка не должна ничего менять. - Уведомления могут прийти не по порядку. Поэтому статус берите из запроса платежа. - Отмечайте заказ оплаченным атомарно: условным `UPDATE … WHERE paid_payment_id IS NULL` или с блокировкой строки. Уведомление и возврат покупателя на `return_url` приходят одновременно, и проверка «не выполнен — выполнить» без атомарности выполнит заказ дважды. - Если не смогли обработать уведомление (например, API не ответил), ответьте 500. AvantPay пришлёт уведомление ещё раз. ### Ответ и повторы Уведомление доставлено, если ваш сервер ответил кодом 200 (или любым другим кодом 2xx) не позже чем через 10 секунд. Если ответ другой, ответа нет или соединение не установилось, AvantPay повторяет уведомление примерно через 1, 5, 15 и 30 минут, через 1, 2, 4, 8 и 16 часов и в последний раз через 24 часа после первой попытки. Всего 11 попыток. В повторе — то же тело: объект платежа на момент события. Подпись и `Signature-Timestamp` у каждой попытки свои. Текущее состояние платежа узнавайте запросом `GET /v3/payments/{payment_id}`. ### Если уведомление не пришло Уведомление может не дойти: истёк сертификат, сервер лежал дольше 24 часов, адрес не был указан, когда создавали платёж. Тогда оплаченный заказ сам не выполнится. Поэтому сверяйтесь с API по расписанию: 1. Раз в несколько минут запрашивайте оплаченные платежи: `GET /v3/payments?status=succeeded&captured_at.gte=<время прошлой сверки минус 10 минут>`, все страницы ([список платежей](https://avantpay.io/developers/api/#get_payments_list)). Запас в 10 минут — чтобы не пропустить платёж на границе. 2. Каждый платёж обрабатывайте тем же кодом, что и уведомление: найти заказ, сверить сумму, атомарно отметить оплаченным, выполнить. Уже выполненные заказы код пропустит. 3. Для отдельного заказа, у которого платёж давно `pending` и прошёл `expires_at`, можно запросить платёж напрямую: `GET /v3/payments/{payment_id}`. Так вы не пропустите и [позднюю оплату](https://avantpay.io/developers/statuses/#late-payment): у неё тоже есть `captured_at`. ### Проверка подлинности Уведомление может подделать кто угодно, кто знает ваш адрес. Поэтому не верьте телу уведомления на слово. #### Главный способ: запросите платёж Запросите платёж по `object.id` с вашими ключами: `GET /v3/payments/{payment_id}`. Ответ приходит от API AvantPay, поэтому ему можно доверять. Если такого платежа у вашего магазина нет, API ответит 404 — тогда уведомление поддельное: ответьте на него 200 и ничего не делайте. Этого способа достаточно. #### Дополнительно: подпись В каждом уведомлении есть заголовки: - `Signature-Timestamp` — время отправки, Unix-время в секундах. - `Signature` — подпись: `v1=` и HMAC-SHA256 в шестнадцатеричном виде. Подписывается строка `.<тело запроса>`: время, точка и тело запроса как оно пришло (байты, без разбора и повторной сборки JSON). Ключ HMAC — ваш секретный ключ API. Как проверить: 1. Склейте значение `Signature-Timestamp`, точку и тело запроса. 2. Посчитайте HMAC-SHA256 этой строки секретным ключом и добавьте впереди `v1=`. 3. Сравните с `Signature` функцией сравнения за постоянное время. 4. Отклоните уведомление, если `Signature-Timestamp` отличается от текущего времени больше чем на 5 минут (300 секунд). Так чужой не сможет прислать вам старое уведомление повторно. Подписывается текущим секретным ключом магазина. После перевыпуска ключа — новым ключом, в том числе в повторах старых уведомлений. Если ключа у магазина нет, заголовков подписи нет. Функция проверки: Python: ```python import hashlib import hmac import time from typing import Optional TOLERANCE_SECONDS = 300 def verify_signature(raw_body: bytes, signature: Optional[str], timestamp: Optional[str], secret_key: str) -> bool: if not signature or not timestamp or not (timestamp.isascii() and timestamp.isdigit()): return False if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS: return False signed = timestamp.encode() + b"." + raw_body expected = "v1=" + hmac.new(secret_key.encode(), signed, hashlib.sha256).hexdigest() return hmac.compare_digest(signature.encode(), expected.encode()) ``` Node.js: ```js import crypto from "node:crypto"; const TOLERANCE_SECONDS = 300; function verifySignature(rawBody, signature, timestamp, secretKey) { if (typeof signature !== "string" || typeof timestamp !== "string" || !/^\d+$/.test(timestamp)) { return false; } if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false; const expected = "v1=" + crypto .createHmac("sha256", secretKey) .update(`${timestamp}.`) .update(rawBody) .digest("hex"); return ( signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)) ); } ``` PHP: ```php 300) { return false; } $expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secretKey); return hash_equals($expected, $signature); } ``` Сравнивайте подпись функцией сравнения за постоянное время, как в примерах: `hmac.compare_digest`, `timingSafeEqual`, `hash_equals`. Проверьте свою функцию на этом примере. Тело запроса: ```json {"type":"notification","event":"payment.succeeded","object":{"id":"2f9b6c1e-4d7a-4c3b-9e8f-1a2b3c4d5e6f","status":"succeeded","paid":true,"amount":{"value":"1000.00","currency":"RUB"}}} ``` `Signature-Timestamp` равен `1792324800`. С ключом `test_secret_key` подпись равна `v1=07181d18b6de6cfe0c882fd02f7e40c8c0e735f19dc67062c39f57c8d75c995b`. Время в примере старое, поэтому проверка времени отклонит его: для самопроверки временно подставьте вместо текущего времени `1792324800`. #### Дополнительно: IP-адреса AvantPay AvantPay отправляет уведомления только с этих IP-адресов: - 79.141.79.167 - 31.129.44.152 ### Пример обработчика Готовые обработчики: проверяют подпись, запрашивают платёж и выполняют заказ один раз. Замените `fulfill_order` (`fulfillOrder`) своей функцией: она находит заказ, сверяет сумму и атомарно отмечает заказ оплаченным. На 404 обработчики отвечают 200: такого платежа у магазина нет. Python — на Flask, Node.js — на Express, PHP — без фреймворка. Python: ```python import hashlib import hmac import os import time from typing import Optional import requests from flask import Flask, abort, request SHOP_ID = os.environ["AVANTPAY_SHOP_ID"] SECRET_KEY = os.environ["AVANTPAY_SECRET_KEY"] app = Flask(__name__) TOLERANCE_SECONDS = 300 def verify_signature(raw_body: bytes, signature: Optional[str], timestamp: Optional[str], secret_key: str) -> bool: if not signature or not timestamp or not (timestamp.isascii() and timestamp.isdigit()): return False if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS: return False signed = timestamp.encode() + b"." + raw_body expected = "v1=" + hmac.new(secret_key.encode(), signed, hashlib.sha256).hexdigest() return hmac.compare_digest(signature.encode(), expected.encode()) def get_payment(payment_id: str) -> Optional[dict]: """Платёж из API; None — у магазина нет такого платежа (404)""" response = requests.get( f"https://api.avantpay.io/v3/payments/{payment_id}", auth=(SHOP_ID, SECRET_KEY), timeout=8, ) if response.status_code == 404: return None response.raise_for_status() return response.json() def fulfill_order(payment: dict) -> None: # 1. Найдите заказ по payment["id"]. Заказа нет (например, счёт из личного кабинета) — ничего не делайте. # 2. Сверьте payment["amount"]["value"] и payment["amount"]["currency"] с суммой заказа. Не совпали — не выполняйте заказ. # 3. Отметьте заказ оплаченным атомарно, например: # UPDATE orders SET paid_payment_id = %s WHERE id = %s AND paid_payment_id IS NULL # Изменилась строка — выполните заказ. Не изменилась — заказ уже выполнен, ничего не делайте. pass @app.post("/avantpay/webhook") def avantpay_webhook(): # Тело — «сырыми» байтами: подпись считается по нему как есть raw_body = request.get_data() signature = request.headers.get("Signature") timestamp = request.headers.get("Signature-Timestamp") if not verify_signature(raw_body, signature, timestamp, SECRET_KEY): abort(401) notification = request.get_json(force=True) if notification.get("event") not in ("payment.succeeded", "payment.canceled"): return "", 200 try: payment = get_payment(notification["object"]["id"]) except requests.RequestException: return "", 500 # AvantPay пришлёт уведомление ещё раз # None — платежа нет у магазина (404): ответ 200, ничего не делаем if payment is not None and payment["status"] == "succeeded": fulfill_order(payment) return "", 200 ``` Node.js: ```js import crypto from "node:crypto"; import express from "express"; const SHOP_ID = process.env.AVANTPAY_SHOP_ID; const SECRET_KEY = process.env.AVANTPAY_SECRET_KEY; const AUTH = "Basic " + Buffer.from(`${SHOP_ID}:${SECRET_KEY}`).toString("base64"); const TOLERANCE_SECONDS = 300; function verifySignature(rawBody, signature, timestamp, secretKey) { if (typeof signature !== "string" || typeof timestamp !== "string" || !/^\d+$/.test(timestamp)) { return false; } if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false; const expected = "v1=" + crypto .createHmac("sha256", secretKey) .update(`${timestamp}.`) .update(rawBody) .digest("hex"); return ( signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)) ); } // Платёж из API; null — у магазина нет такого платежа (404) async function getPayment(paymentId) { const response = await fetch( `https://api.avantpay.io/v3/payments/${encodeURIComponent(paymentId)}`, { headers: { Authorization: AUTH }, signal: AbortSignal.timeout(8000) }, ); if (response.status === 404) return null; if (!response.ok) throw new Error(`AvantPay API: HTTP ${response.status}`); return response.json(); } async function fulfillOrder(payment) { // 1. Найдите заказ по payment.id. Заказа нет (например, счёт из личного кабинета) — ничего не делайте. // 2. Сверьте payment.amount.value и payment.amount.currency с суммой заказа. Не совпали — не выполняйте заказ. // 3. Отметьте заказ оплаченным атомарно, например: // UPDATE orders SET paid_payment_id = $1 WHERE id = $2 AND paid_payment_id IS NULL // Изменилась строка — выполните заказ. Не изменилась — заказ уже выполнен, ничего не делайте. } const app = express(); // Тело — «сырыми» байтами: подпись считается по нему как есть app.post( "/avantpay/webhook", express.raw({ type: "application/json" }), async (req, res) => { if (!verifySignature(req.body, req.get("Signature"), req.get("Signature-Timestamp"), SECRET_KEY)) { return res.sendStatus(401); } const notification = JSON.parse(req.body.toString("utf8")); if (!["payment.succeeded", "payment.canceled"].includes(notification.event)) { return res.sendStatus(200); } try { const payment = await getPayment(notification.object.id); // payment === null — платежа нет у магазина (404): ответ 200, ничего не делаем if (payment?.status === "succeeded") await fulfillOrder(payment); res.sendStatus(200); } catch (error) { console.error(error); res.sendStatus(500); // AvantPay пришлёт уведомление ещё раз } }, ); app.listen(3000); ``` PHP: ```php 300) { return false; } $expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secretKey); return hash_equals($expected, $signature); } function fulfillOrder(array $payment): void { // 1. Найдите заказ по $payment['id']. Заказа нет (например, счёт из личного кабинета) — ничего не делайте. // 2. Сверьте $payment['amount']['value'] и $payment['amount']['currency'] с суммой заказа. Не совпали — не выполняйте заказ. // 3. Отметьте заказ оплаченным атомарно, например: // UPDATE orders SET paid_payment_id = ? WHERE id = ? AND paid_payment_id IS NULL // Изменилась строка — выполните заказ. Не изменилась — заказ уже выполнен, ничего не делайте. } // Тело — как пришло: подпись считается по нему до json_decode $rawBody = file_get_contents('php://input'); $signature = $_SERVER['HTTP_SIGNATURE'] ?? null; $timestamp = $_SERVER['HTTP_SIGNATURE_TIMESTAMP'] ?? null; if (!verifySignature($rawBody, $signature, $timestamp, $secretKey)) { http_response_code(401); exit; } $notification = json_decode($rawBody, true); $event = $notification['event'] ?? ''; if ($event !== 'payment.succeeded' && $event !== 'payment.canceled') { http_response_code(200); exit; } $ch = curl_init('https://api.avantpay.io/v3/payments/' . rawurlencode($notification['object']['id'])); curl_setopt_array($ch, [ CURLOPT_USERPWD => $shopId . ':' . $secretKey, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 8, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode === 404) { http_response_code(200); // платежа нет у магазина: ничего не делаем exit; } if ($response === false || $httpCode !== 200) { http_response_code(500); // AvantPay пришлёт уведомление ещё раз exit; } $payment = json_decode($response, true); if ($payment['status'] === 'succeeded') { fulfillOrder($payment); } http_response_code(200); ``` --- ## Проверка интеграции Страница: https://avantpay.io/developers/testing/ Тестового режима и тестового магазина у AvantPay нет. Все платежи настоящие, поле `test` всегда `false`. Поэтому проверяйте интеграцию так: сначала всё, что можно проверить без денег, потом один настоящий платёж на минимальную сумму. Деньги за него можно вернуть в личном кабинете. Все запросы и уведомления видны в личном кабинете: **Ещё → Разработчикам → Логи запросов**. Там же видно, что ответил ваш сервер на уведомление. ### 1. Ключи ```bash curl "https://api.avantpay.io/v3/payments?limit=1" \ -u "$AVANTPAY_SHOP_ID:$AVANTPAY_SECRET_KEY" ``` Ответ 200 — ключи верные. Ответ 401 — неверный ID магазина или ключ. ### 2. Платёж без оплаты Проверяет создание платежа, приём уведомления `payment.canceled` и запрос платежа. Денег не нужно. 1. Создайте платёж через свою интеграцию. Или командами ниже: первая строка создаёт ключ идемпотентности (работает в Linux и macOS). ```bash KEY=$(uuidgen) curl https://api.avantpay.io/v3/payments \ -X POST \ -u "$AVANTPAY_SHOP_ID:$AVANTPAY_SECRET_KEY" \ -H "Idempotence-Key: $KEY" \ -H "Content-Type: application/json" \ -d '{ "amount": { "value": "100.00", "currency": "RUB" }, "capture": true, "confirmation": { "type": "redirect", "return_url": "https://example.com/orders/test" }, "description": "Проверка интеграции", "metadata": { "email": "buyer@example.com", "order_id": "test" } }' ``` 2. Откройте `confirmation.confirmation_url` и убедитесь, что страница оплаты открывается. Не платите. 3. Дождитесь уведомления `payment.canceled`. Срок оплаты — 20 минут после создания, он в `expires_at`. Платёж отменяется не в ту же секунду после `expires_at`: сначала AvantPay проверяет, не прошла ли оплата. Обычно `canceled` приходит в течение 10 минут после `expires_at`, в редких случаях — до 2 часов. 4. Проверьте: - ваш сервер получил уведомление, проверил подпись и ответил 200; - запрос `GET /v3/payments/{payment_id}` вернул `"status": "canceled"`; - заказ не выполнен. ### 3. Повтор запроса Отправьте тот же запрос на создание платежа ещё раз — с тем же `Idempotence-Key` и тем же телом. Если создавали платёж командами выше, повторите только команду `curl`, без строки с `KEY`. В ответе должен быть тот же `id`. Значит, повтор после таймаута не создаст второй платёж. ### 4. Повтор уведомления Временно сделайте так, чтобы ваш сервер ответил на уведомление кодом 500. Примерно через минуту AvantPay пришлёт его ещё раз. Убедитесь, что повторное уведомление не выполняет заказ второй раз. ### 5. Настоящий платёж 1. Узнайте минимальную сумму в личном кабинете: **Ещё → Способы оплаты**. 2. Создайте платёж на эту сумму и оплатите его сами — СБП или картой. 3. Проверьте: - пришло уведомление `payment.succeeded`, ваш сервер ответил 200; - запрос `GET /v3/payments/{payment_id}` вернул `"status": "succeeded"`; - заказ выполнен один раз, сумма платежа сверена с суммой заказа; - покупатель вернулся на `return_url` и увидел правильный результат. 4. Верните деньги в личном кабинете: найдите платёж в разделе платежей и оформите возврат. ### Если что-то не работает | Что видно | Что проверить | |---|---| | 401 `invalid_credentials` | ID магазина и ключ. Не перевыпускали ли ключ. Запросы должны идти на `https://api.avantpay.io/v3`. | | 403 `forbidden` | Включено ли у ключа право «Платежи». Если нет — напишите в поддержку. | | 400 `invalid_request` | Поле из `parameter`. Описание — в `description`. Таблица ошибок — в справочнике: [Создать платёж](https://avantpay.io/developers/api/#create_payment-errors). | | 400, `"parameter": "metadata.email"` | В запросе нет email покупателя или адрес неверный. Покупатель из Telegram — `@telegram.org`. | | 400, `"parameter": "capture"` | В запросе нет `"capture": true`. | | 400 `not_supported` | В запросе поле, которого нет в API, например `receipt`. Уберите его. | | 404 на любой запрос | Адрес. Правильно: `https://api.avantpay.io/v3/payments`. | | Уведомления не приходят | Адрес и события в **Ещё → Разработчикам → HTTP-уведомления**. Адрес — `https://`, порт 443 или 8443, сертификат действующий. Запись об отправке — в **Логах запросов**. Платежи, созданные до того, как указан адрес, уведомлений не получат: проверьте их запросом `GET /v3/payments/{payment_id}`. | | Сервер отклоняет уведомления | Подпись считается по строке `.<тело>`, тело — как оно пришло, до разбора JSON. Часы сервера идут верно (допуск — 5 минут). | В письме в поддержку (support@avantpay.io) укажите `id` платежа, `id` ошибки из ответа и значение заголовка `X-Request-Id`. --- ## Чеклист перед запуском Страница: https://avantpay.io/developers/checklist/ Пройдите каждый пункт. Если пункт не выполнен, интеграция может потерять деньги или выполнить заказ без оплаты. ### Ключи и безопасность - [ ] Запросы к API идут только с вашего сервера. В браузере и мобильном приложении секретного ключа нет. - [ ] Секретный ключ хранится в переменных окружения или хранилище секретов, а не в коде и не в репозитории. - [ ] Секретный ключ не пишется в логи. - [ ] У ключа включено право «Платежи» (**Ещё → Разработчикам → API-ключ**). - [ ] Используется адрес `https://api.avantpay.io/v3`. ### Создание платежа - [ ] В каждом запросе `POST /v3/payments` есть заголовок `Idempotence-Key`. - [ ] Ключ идемпотентности создаётся один раз на попытку оплаты и сохраняется в заказе до запроса. - [ ] При таймауте, ответе 500, 429 или 202 запрос повторяется с тем же ключом и тем же телом. - [ ] В каждом запросе есть email покупателя в `metadata.email`. Для покупателя из Telegram — `@telegram.org`. - [ ] В каждом запросе есть `"capture": true`. - [ ] Сумма передаётся строкой с двумя знаками после точки: `"100.00"`. Валюта — `"RUB"`. - [ ] Номер заказа передаётся в `metadata.order_id`. - [ ] `return_url` ведёт на страницу заказа, например `https://example.com/orders/123`: AvantPay не добавляет к нему параметры. - [ ] Нет полей, которых нет в справочнике: `receipt`, `save_payment_method`, `payment_method_id` и других ([ошибки](https://avantpay.io/developers/api/#create_payment-errors)). - [ ] `id` платежа сохраняется в заказе. - [ ] Таймаут HTTP-клиента — не меньше 30 секунд. ### Результат оплаты - [ ] Покупатель перенаправляется на `confirmation.confirmation_url`. - [ ] Возврат покупателя на `return_url` не считается оплатой: статус проверяется запросом `GET /v3/payments/{payment_id}`. - [ ] Заказ выполняется только при `status` = `succeeded`. - [ ] Перед выполнением заказа `amount.value` и `amount.currency` сверяются с суммой заказа. Если сумма заказа изменилась, создаётся новый платёж с новым ключом, а старый не засчитывается. - [ ] Заказ выполняется ровно один раз, даже если `succeeded` пришёл несколько раз: заказ отмечается оплаченным атомарно (условный `UPDATE` или блокировка строки). - [ ] Покупатель уходит на оплату, только если `status` равен `pending` и есть `confirmation_url`. Повтор с тем же ключом вернул `canceled` — создаётся новый платёж с новым ключом. - [ ] При `canceled` заказ не выполняется, связь заказа с платежом сохраняется. - [ ] Если `canceled` сменился на `succeeded`, заказ выполняется или деньги возвращаются в личном кабинете. - [ ] Код не падает на незнакомых полях и значениях в объекте платежа. ### Уведомления - [ ] Адрес уведомлений указан в **Ещё → Разработчикам → HTTP-уведомления** до первого платежа: `https://`, порт 443 или 8443, действующий сертификат. - [ ] Включены события «Платёж оплачен» и «Платёж отменён». - [ ] Обработчик запрашивает платёж через API и решает по ответу API, а не по телу уведомления. - [ ] Обработчик отвечает 200 не позже чем через 10 секунд. Долгая работа — после ответа. - [ ] На незнакомое событие обработчик отвечает 200 и ничего не делает. На платёж, которого нет в заказах (счёт из личного кабинета), и на ответ 404 — тоже 200. - [ ] Раз в несколько минут сервер сверяется с API: запрашивает оплаченные платежи и выполняет заказы, по которым не пришло уведомление ([как](https://avantpay.io/developers/webhooks/#reconciliation)). - [ ] Если подпись проверяется — по строке `.<тело>` с телом как оно пришло, до разбора JSON, и уведомления старше 5 минут отклоняются. ### Проверка - [ ] Платёж без оплаты прошёл до `canceled`, уведомление получено ([как проверить](https://avantpay.io/developers/testing/)). - [ ] Повтор запроса с тем же ключом вернул тот же платёж. - [ ] Повтор уведомления не выполнил заказ второй раз. - [ ] Один настоящий платёж на минимальную сумму прошёл до `succeeded`, заказ выполнен один раз, деньги возвращены в личном кабинете. - [ ] В логах вашего сервера сохраняются `id` платежа, `id` ошибки и заголовок `X-Request-Id` — чтобы разбираться с поддержкой. --- ## Частые вопросы Страница: https://avantpay.io/developers/faq/ ### Нужна ли библиотека или SDK? Нет. Подойдёт любой HTTP-клиент. Примеры на cURL, Python, Node.js и PHP — в [справочнике API](https://avantpay.io/developers/api/#create_payment) и в [быстром старте](https://avantpay.io/developers/quick-start/). ### Обязательно ли передавать email покупателя? Да. Передавайте email покупателя в `metadata.email` в каждом запросе на создание платежа. Без него или с неверным адресом API ответит 400 `invalid_request`, `"parameter": "metadata.email"`. ### Где взять email покупателя из Telegram? Если покупатель пришёл из Telegram и email у вас нет, передайте `@telegram.org`: числовой ID пользователя Telegram (`123456789@telegram.org`) или его имя пользователя без `@` в начале. ### Как связать платёж с заказом? Передайте номер заказа в `metadata.order_id` и сохраните `id` платежа в заказе сразу после ответа. Номер заказа вернётся в объекте платежа и в уведомлениях, а платёж получайте по `id`: `GET /v3/payments/{payment_id}`. Фильтра по номеру заказа в списке платежей нет. ### Почему ошибка «capture is required»? В запросе нет `"capture": true`. В AvantPay платёж списывается сразу после оплаты, поэтому `capture` обязателен и всегда равен `true`. `"capture": false` тоже даёт ошибку 400. ### Покупатель вернулся на return_url. Платёж оплачен? Не обязательно. Покупатель возвращается и после оплаты, и после отказа. Запросите платёж `GET /v3/payments/{payment_id}` и смотрите на `status`. ### Как на странице return_url понять, о каком платеже речь? AvantPay открывает `return_url` как есть, без параметров. Поэтому укажите в нём адрес страницы заказа, например `https://example.com/orders/123`. На этой странице возьмите `id` платежа из заказа и запросите платёж. ### Нужно ли передавать способ оплаты? Нет. Без `payment_method_data` покупатель выберет СБП или карту на странице оплаты. ### Как оплатить заказ ещё раз, если платёж отменён? Создайте новый платёж с новым ключом идемпотентности. Номер заказа в `metadata.order_id` можно оставить тем же. ### Можно ли отменить платёж? Нет. Неоплаченный платёж отменится сам после `expires_at` — через 20 минут после создания платежа. Если заказ больше не нужен, просто не показывайте покупателю ссылку на оплату. Запрос `/cancel` ответит 400 `not_supported`. ### Можно ли изменить сумму платежа? Нет. Создайте новый платёж с новой суммой и новым ключом идемпотентности. Старый платёж не засчитывайте: если его всё же оплатят, верните деньги в личном кабинете. Поэтому перед выполнением заказа сверяйте `amount` платежа с суммой заказа. ### Есть ли двухстадийные платежи (холдирование)? Нет. Деньги списываются сразу после оплаты. Передавайте `"capture": true`. Запрос `/capture` ответит 400 `not_supported`. ### Как вернуть деньги покупателю? В личном кабинете. Через API вернуть деньги нельзя. ### Есть ли тестовый режим? Нет. Проверьте интеграцию без оплаты и одним настоящим платежом на минимальную сумму, потом верните деньги в личном кабинете. Как — на странице [Проверка интеграции](https://avantpay.io/developers/testing/). ### AvantPay отправляет чеки 54-ФЗ? Нет. Не передавайте `receipt`: запрос с ним получит ошибку 400 `not_supported`. ### Можно ли сохранить карту и списывать без покупателя? Нет. Сохранения способов оплаты и автоплатежей нет. ### В какой валюте принимать платежи? В рублях: `"currency": "RUB"`. ### Какая минимальная и максимальная сумма? Зависит от способа оплаты. Лимиты — в личном кабинете: **Ещё → Способы оплаты**. Если сумма вне лимитов, API ответит 400 с `"parameter": "amount.value"`. ### Пришло payment.canceled, а потом payment.succeeded. Что это? Поздняя оплата: банк подтвердил оплату после отмены платежа. Деньги пришли. Выполните заказ или верните деньги в личном кабинете. Подробнее — в разделе [Поздняя оплата](https://avantpay.io/developers/statuses/#late-payment). ### Одно уведомление пришло два раза. Это ошибка? Нет, так бывает. Обрабатывайте уведомления так, чтобы повтор ничего не менял: не выполняйте заказ второй раз. ### Уведомления не приходят. Что проверить? Адрес и события в **Ещё → Разработчикам → HTTP-уведомления**. Адрес должен быть `https://`, на порту 443 или 8443, с действующим сертификатом. Попытки отправки видны в **Ещё → Разработчикам → Логи запросов**. Платежи, созданные до того, как указан адрес, уведомлений не получат никогда: проверьте их запросом `GET /v3/payments/{payment_id}`. Чтобы не пропускать оплаты, сверяйтесь с API по расписанию — [как](https://avantpay.io/developers/webhooks/#reconciliation). ### Пришло уведомление о платеже, которого нет в моих заказах. Что это? Например, счёт, выставленный в личном кабинете: о таких платежах уведомления приходят тоже. Ответьте 200 и ничего не делайте. Так же — если запрос платежа вернул 404. ### Ответ 401 invalid_credentials. Что не так? Неверный ID магазина или секретный ключ. Возможно, ключ перевыпустили. Проверьте их в **Ещё → Разработчикам → API-ключ** и проверьте адрес: `https://api.avantpay.io/v3`. ### Ответ 403 forbidden. Что не так? У ключа нет права «Платежи» или магазин не активен. Напишите в поддержку: support@avantpay.io. ### Ответ 404 на любой запрос. Что не так? Скорее всего, адрес. Правильно: `https://api.avantpay.io/v3/payments` — `payments` во множественном числе, версия `v3`. ### Ответ 429 too_many_requests. Что не так? Больше 50 запросов в секунду от магазина. Подождите `retry_after` миллисекунд и повторите с тем же `Idempotence-Key`. ### Ответ 500 или нет ответа. Платёж создан? Неизвестно. Повторите тот же запрос с тем же `Idempotence-Key`: придёт созданный платёж или он создастся сейчас. Не создавайте платёж с новым ключом, иначе покупатель может заплатить дважды. Если повтор вернул `"status": "canceled"`, платёж не создался до конца: вот тогда создайте новый с новым ключом. ### Повторил запрос с тем же ключом, но с другой суммой — пришла ошибка 400. Почему? Один ключ идемпотентности — один платёж. Тот же ключ с другим телом запроса API отклоняет: `"description": "Idempotence key duplicated…"`. Для нового платежа нужен новый ключ. ### Повторил запрос с тем же ключом через два дня — пришла ошибка 400. Почему? Повтор с тем же ключом работает 24 часа. Позже API отвечает 400, а не прежним платежом. Чтобы узнать, чем кончился тот запрос, запросите платёж по `id` из заказа. ### Можно ли вызывать API из браузера или мобильного приложения? Нет. Для запроса нужен секретный ключ, а в браузере и приложении его может прочитать кто угодно. Браузер такой запрос и не выполнит: CORS выключен. Вызывайте API со своего сервера. ### Где посмотреть запросы к API? В личном кабинете: **Ещё → Разработчикам → Логи запросов**. Там видны запросы и уведомления с ответами. ### Куда писать, если ничего не помогло? support@avantpay.io. Укажите `id` платежа, `id` ошибки из ответа и значение заголовка `X-Request-Id`. --- ## Интеграция с помощью ИИ Страница: https://avantpay.io/developers/ai/ Эту документацию удобно отдавать ИИ-ассистенту: ChatGPT, Claude, Cursor, GitHub Copilot и другим. ### Документация для ИИ | Файл | Что внутри | |---|---| | https://avantpay.io/developers/llms-full.txt | Вся документация одним файлом в формате Markdown: поля, примеры, ошибки, уведомления, чеклист. Лучший вариант для ассистента. | | https://avantpay.io/developers/llms.txt | Оглавление по стандарту llms.txt со ссылками на страницы и разделы справочника. | | Страница + `.md` | Любая страница в формате Markdown. Например, https://avantpay.io/developers/api.md — справочник API. | Файлы собираются из страниц этого сайта при каждом обновлении документации, поэтому всегда совпадают с ним. Если ассистент не умеет открывать ссылки, скачайте https://avantpay.io/developers/llms-full.txt и приложите файл к сообщению. ### Готовое задание Скопируйте текст, допишите в первой строке свой язык и фреймворк и отправьте ассистенту. ```text Подключи приём платежей AvantPay к моему проекту. Проект: <язык, фреймворк, где хранятся заказы>. Документация: https://avantpay.io/developers/llms-full.txt. Прочитай её целиком до того, как писать код. Используй только методы, поля и значения из этой документации. Если чего-то в документации нет, не придумывай — спроси меня. Что нужно сделать: 1. Создание платежа: POST https://api.avantpay.io/v3/payments, HTTP Basic (ID магазина и секретный ключ из переменных окружения AVANTPAY_SHOP_ID и AVANTPAY_SECRET_KEY). Поля: amount (value — строка с двумя знаками после точки, currency — RUB), capture = true (обязательно), confirmation (type = redirect, return_url — адрес страницы этого заказа), description, metadata.email — email покупателя (обязательно; покупатель из Telegram — @telegram.org), metadata.order_id — номер заказа. Не передавай receipt и другие поля, которых нет в разделе «Создать платёж» справочника. 2. Ключ идемпотентности (заголовок Idempotence-Key): новый UUID на каждую попытку оплаты. Сохрани его в заказе до запроса. При таймауте, ответе 500, 429 или 202 повтори запрос с тем же ключом и тем же телом: 1–2 минуты, потом покажи покупателю «Платёж создаётся» и продолжай в фоне с тем же ключом, но не дольше 24 часов. Сохрани id платежа из ответа в заказе. 3. Перенаправление покупателя на confirmation.confirmation_url — только если status = pending и ссылка есть. Если повтор с тем же ключом вернул status = canceled, платёж не создался до конца: создай новый платёж с новым ключом. 4. Обработчик уведомлений (POST на мой адрес): проверь подпись из заголовков Signature и Signature-Timestamp по строке «.<сырое тело запроса>» и отклони уведомление старше 5 минут; на незнакомый event ответь 200 и ничего не делай; запроси платёж GET https://api.avantpay.io/v3/payments/{id} и решай по ответу API; на ответ 404 и на платёж, которого нет в моих заказах (например, счёт из личного кабинета), ответь 200 и ничего не делай; при status = succeeded сверь amount.value и amount.currency с суммой заказа и отметь заказ оплаченным атомарно (условный UPDATE … WHERE paid_payment_id IS NULL или блокировка строки), потом выполни заказ ровно один раз; ответь 200 не позже чем через 10 секунд; если не удалось обработать — ответь 500. 5. Страница возврата (return_url): AvantPay открывает её без параметров. Найди заказ по адресу страницы, возьми из заказа id платежа, запроси платёж GET https://api.avantpay.io/v3/payments/{id} и покажи покупателю результат по status. Если выполняешь заказ и здесь — той же атомарной функцией, что и обработчик уведомлений: возврат покупателя и уведомление приходят одновременно. 6. Сверка по расписанию: раз в несколько минут запрашивай GET https://api.avantpay.io/v3/payments?status=succeeded&captured_at.gte=<время прошлой сверки минус 10 минут> по всем страницам и обрабатывай каждый платёж тем же кодом, что и уведомление. Уведомление может не дойти. 7. Учти: статус canceled может смениться на succeeded; одно уведомление может прийти несколько раз и не по порядку; незнакомые поля в ответах не должны ломать код; повторная оплата того же заказа — новый платёж с новым ключом идемпотентности; если сумма заказа изменилась после создания платежа, создай новый платёж с новым ключом, а платёж на старую сумму не засчитывай. 8. Запросы к API — только с сервера. Секретный ключ не должен попасть в браузер, мобильное приложение, логи или репозиторий. В конце перечисли, что я должен настроить в личном кабинете AvantPay (адрес уведомлений — до первого платежа), и как проверить интеграцию по разделу «Проверка интеграции» документации. ``` ### Что проверить в коде от ассистента Пройдите [чеклист перед запуском](https://avantpay.io/developers/checklist/). Чаще всего ассистенты ошибаются так: - не передают email покупателя в `metadata.email` — платёж не создаётся, ошибка 400; - не передают `"capture": true`; - передают `receipt` — чеки AvantPay не отправляет, запрос получит ошибку 400; - выполняют заказ, когда покупатель вернулся на `return_url`, без проверки статуса; - не сверяют `amount` платежа с суммой заказа: после смены корзины заказ выполняется по старому платежу на меньшую сумму; - проверяют «заказ не выполнен», а потом выполняют его без атомарной отметки: уведомление и возврат покупателя выполняют заказ дважды; - выполняют заказ только по уведомлению, без сверки по расписанию; - отвечают 500 на уведомление о платеже, которого нет в заказах, или на ответ 404 — уведомление повторяется сутки; - ждут на странице возврата параметр с `id` платежа — его там нет; - создают новый ключ идемпотентности при каждом повторе запроса; - передают сумму числом (`100`) вместо строки (`"100.00"`); - считают подпись только по телу, без `Signature-Timestamp`; - считают `canceled` окончательным статусом; - выполняют заказ повторно на повторное уведомление.