# EGN Pay EGN Pay — единый платёжный сервис экосистемы EGN. Ваш сервис создаёт платёж одним HTTP-запросом, получает ссылку `https://pay.egnetwork.ru/p/pay_…` и отправляет на неё пользователя. На странице оплаты пользователь видит сумму и QR-код СБП (на телефоне — кнопку «Оплатить в приложении банка»), при необходимости — кнопку оплаты картой. После оплаты ваш сервис получает подписанный вебхук, а пользователь возвращается на `return_url`. Ключи терминала Т-Банка, подписи запросов, статусы и повторы уведомлений живут только здесь — в сервисах их больше нет. ## Как это работает ```text Ваш сервис EGN Pay Т-Банк / НСПК │ POST /api/v1/payments │ │ │ ─────────────────────────▶ │ Init + GetQr │ │ { payment_url, id } │ ────────────────────────────────▶ │ │ ◀───────────────────────── │ │ │ │ │ │ 302 → payment_url │ пользователь платит по QR (СБП) │ │ ──────────── пользователь ─▶ ───────────────────────────────▶ │ │ │ уведомление CONFIRMED │ │ │ ◀──────────────────────────────── │ │ webhook payment.succeeded │ │ │ ◀───────────────────────── │ │ │ пользователь → return_url │ │ ``` ## Быстрый старт ### 1. Получите ключи Администратор создаёт сервис в [админке](https://pay.egnetwork.ru/admin) и выдаёт вашему отделу доступ. Войдите в админку через EGN ID, откройте сервис и выпустите ключи: - `egn_test_…` — тестовый ключ (деньги не списываются), - `egn_live_…` — боевой ключ, - `whsec_…` — секрет для проверки подписи вебхуков. ### 2. Создайте платёж ```bash curl https://pay.egnetwork.ru/api/v1/payments \ -H "Authorization: Bearer egn_test_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \ -H "Content-Type: application/json" \ -d '{ "amount": 49900, "order_id": "order-1042", "description": "Подписка Pro на 1 месяц", "return_url": "https://example.egnetwork.ru/billing/done", "customer": { "email": "user@example.com" }, "metadata": { "user_id": "u_123" } }' ``` `amount` — в **копейках**: `49900` = 499,00 ₽. Ответ (`201 Created`): ```json { "id": "pay_8fK2mQ1xZbY7cV3nT0aR", "object": "payment", "livemode": false, "status": "pending", "amount": 49900, "payment_url": "https://pay.egnetwork.ru/p/pay_8fK2mQ1xZbY7cV3nT0aR", "order_id": "order-1042", "expires_at": "2026-10-27T13:00:00.000Z" } ``` ### 3. Отправьте пользователя на `payment_url` Сделайте редирект (HTTP 302) или откройте ссылку в WebApp/боте. ### 4. Примите вебхук На `webhook_url` сервиса придёт `POST` с событием `payment.succeeded`. Проверьте подпись `X-EGN-Signature` (см. [Вебхуки](/docs/webhooks)) и выдайте пользователю товар. **Выдавайте товар только по вебхуку или по `GET /api/v1/payments/{id}`**, а не по факту возврата на `return_url` — URL возврата может открыть кто угодно. ## Разделы - [Аутентификация](/docs/authentication) — ключи, режимы test/live - [Платежи](/docs/payments) — создание, статусы, отмена, возвраты, чеки 54-ФЗ - [Страница оплаты](/docs/payment-page) — что видит пользователь, параметры возврата - [Вебхуки](/docs/webhooks) — события, подпись, повторы - [Тестирование](/docs/testing) — тестовый режим и симуляция оплаты - [Ошибки](/docs/errors) — коды ошибок - [SDK](/docs/sdk) — однофайловый TypeScript-клиент - [Для ИИ-агентов](/docs/agents) — `llms.txt`, OpenAPI, Markdown-версии страниц --- # Аутентификация Каждый запрос к `https://pay.egnetwork.ru/api/v1/*` подписывается секретным ключом сервиса в заголовке `Authorization`: ```http Authorization: Bearer egn_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX ``` ## Ключи | Префикс | Режим | Что происходит | |---|---|---| | `egn_live_` | боевой | Реальные платежи через терминал Т-Банка | | `egn_test_` | тестовый | Тестовый терминал Т-Банка или встроенный эмулятор. Деньги не списываются | Ключ определяет и сервис, и режим. Платежи разных режимов изолированы: test-ключ не видит live-платежи и наоборот. Поле `livemode` в каждом объекте показывает режим. - Ключ — 32 символа после префикса, храните его только на бэкенде (переменная окружения, secret manager). Никогда не отдавайте его в браузер или мобильное приложение. - В базе EGN Pay хранится только SHA-256 хэш ключа, восстановить его нельзя — только перевыпустить. - Перевыпуск (`rotate`) сразу отзывает предыдущий ключ этого режима. ## Проверка ключа ```bash curl https://pay.egnetwork.ru/api/v1/me -H "Authorization: Bearer $EGN_PAY_KEY" ``` ```json { "object": "merchant", "id": "mch_3hJ9…", "name": "Emerald Games", "slug": "emerald", "livemode": false, "provider": "emulator", "webhook_url": "https://emerald.example/api/egn-pay/webhook", "allowed_return_hosts": ["emerald.example"] } ``` `provider` — `tbank` (запросы идут в Т-Банк) или `emulator` (тестовый режим без тестового терминала). ## Ошибки аутентификации | HTTP | `code` | Причина | |---|---|---| | 401 | `unauthorized` | Нет заголовка, неверный формат, ключ не найден или отозван | | 403 | `merchant_disabled` | Сервис отключён администратором | --- # Платежи Все суммы — **целые числа в копейках**. Валюта — только `RUB`. ## Создать платёж `POST https://pay.egnetwork.ru/api/v1/payments` | Поле | Тип | Обяз. | Описание | |---|---|---|---| | `amount` | integer | да | Сумма в копейках, от `100` (1 ₽) до `1000000000` (10 млн ₽) | | `order_id` | string | да | Ваш ID заказа, до 64 символов `[A-Za-z0-9._:-]`. Уникален в пределах сервиса и режима — это ключ идемпотентности | | `description` | string | да | Что оплачивает пользователь, до 140 символов. Показывается на странице оплаты и в банке | | `return_url` | string (URL) | нет | Куда вернуть пользователя после оплаты. `https`, хост должен быть в `allowed_return_hosts` сервиса (если список задан) | | `webhook_url` | string (URL) | нет | Переопределить URL вебхука для этого платежа (только `https`). По умолчанию — URL из настроек сервиса | | `methods` | `("sbp" \| "card")[]` | нет | Способы оплаты. По умолчанию `["sbp","card"]`. `["sbp"]` — только QR | | `expires_in` | integer | нет | Сколько секунд платёж ждёт оплаты: `300`…`604800`. По умолчанию `3600` | | `customer.email` | string | одно из двух | Email покупателя. На него банк отправит чек 54-ФЗ | | `customer.phone` | string | одно из двух | Телефон покупателя `+79991234567`. На него банк отправит чек 54-ФЗ | | `receipt` | object | нет | Чек 54-ФЗ, см. ниже | | `metadata` | object | нет | До 20 пар `string → string` (ключ ≤ 40, значение ≤ 500 символов). Возвращается в объекте и вебхуках | Ответ: `201 Created` с [объектом платежа](#объект-платежа). Если платёж с этим `order_id` уже есть и сумма совпадает — `200 OK` с существующим платежом (повтор безопасен). ```js const res = await fetch("https://pay.egnetwork.ru/api/v1/payments", { method: "POST", headers: { Authorization: `Bearer ${process.env.EGN_PAY_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ amount: 49900, order_id: `order-${order.id}`, description: "Подписка Pro на 1 месяц", return_url: "https://example.egnetwork.ru/billing/done", customer: { email: user.email }, metadata: { user_id: String(user.id) }, }), }); if (!res.ok) throw new Error((await res.json()).error.message); const payment = await res.json(); return Response.redirect(payment.payment_url, 302); ``` ```python import os, requests r = requests.post( "https://pay.egnetwork.ru/api/v1/payments", headers={"Authorization": f"Bearer {os.environ['EGN_PAY_KEY']}"}, json={ "amount": 49900, "order_id": f"order-{order.id}", "description": "Подписка Pro на 1 месяц", "return_url": "https://example.egnetwork.ru/billing/done", "customer": {"email": user.email}, }, timeout=20, ) r.raise_for_status() payment_url = r.json()["payment_url"] ``` ### Идемпотентность `order_id` гарантирует, что двойной клик или ретрай после таймаута не создаст второй платёж: | Ситуация | Ответ | |---|---| | `order_id` новый | `201`, новый платёж | | `order_id` есть, сумма та же, платёж `pending`/`succeeded`/… | `200`, существующий платёж | | `order_id` есть, сумма другая | `409 order_id_conflict` | | `order_id` есть, платёж `failed`/`expired`/`canceled` | `409 order_closed` — создайте платёж с новым `order_id`, например `order-1042-2` | ## Получить платёж `GET https://pay.egnetwork.ru/api/v1/payments/{id}` Используйте как источник правды, если вебхук не дошёл, или для проверки при возврате пользователя на `return_url`. ## Список платежей `GET https://pay.egnetwork.ru/api/v1/payments?order_id=&status=&limit=20&starting_after=` | Параметр | Описание | |---|---| | `order_id` | Найти платёж по вашему ID заказа | | `status` | Фильтр по статусу | | `limit` | 1…100, по умолчанию 20 | | `starting_after` | ID последнего платежа предыдущей страницы | ```json { "object": "list", "data": [ { "id": "pay_…" } ], "has_more": false } ``` ## Отменить платёж `POST https://pay.egnetwork.ru/api/v1/payments/{id}/cancel` — только для `pending`. После отмены QR перестаёт работать, приходит вебхук `payment.canceled`. ## Вернуть деньги `POST https://pay.egnetwork.ru/api/v1/payments/{id}/refund` ```json { "amount": 10000 } ``` `amount` необязателен — без него возвращается весь остаток. Можно делать несколько частичных возвратов. Статус станет `partially_refunded` или `refunded`, придёт вебхук с тем же именем. `refunded_amount` показывает сумму всех возвратов. ## Статусы | Статус | Финальный | Значение | |---|---|---| | `pending` | нет | Ожидает оплаты | | `succeeded` | — | Оплачен. Можно выдавать товар | | `failed` | да | Банк отклонил оплату | | `expired` | да | Истёк `expires_in`, оплаты не было | | `canceled` | да | Отменён через API или админку | | `partially_refunded` | нет | Оплачен, часть суммы возвращена | | `refunded` | да | Вся сумма возвращена | ```text pending ──▶ succeeded ──▶ partially_refunded ──▶ refunded │ └──────────────────────────────▶ refunded ├──▶ failed ├──▶ expired └──▶ canceled ``` Редкий случай: если банк подтвердит оплату уже после того, как платёж стал `expired`/`failed`, статус сменится на `succeeded` и придёт вебхук `payment.succeeded`. Деньги важнее таймеров — обработайте такой вебхук как обычную оплату. ## Чеки 54-ФЗ Если к терминалу подключена онлайн-касса, Т-Банк ждёт чек. Два варианта: **Автоматически.** Если администратор выбрал систему налогообложения (админка → Терминал), EGN Pay сам сформирует чек из одной позиции: `description`, `amount`, предмет расчёта «услуга», способ «полный расчёт». Чек уходит на `customer.email` или `customer.phone` — поэтому одно из этих полей обязательно в каждом платеже. Без контакта запрос вернёт `422 validation_error`. **Явно** — передайте `receipt`: ```json { "amount": 59800, "order_id": "order-77", "description": "Заказ №77", "customer": { "email": "user@example.com" }, "receipt": { "taxation": "usn_income", "items": [ { "name": "Подписка Pro", "price": 49900, "quantity": 1, "tax": "none" }, { "name": "Доп. слот", "price": 9900, "quantity": 1, "tax": "none", "payment_object": "service" } ] } } ``` | Поле | Описание | |---|---| | `taxation` | `osn`, `usn_income`, `usn_income_outcome`, `envd`, `esn`, `patent` | | `email` / `phone` | Куда отправить чек. По умолчанию из `customer` | | `items[].name` | Название, до 128 символов | | `items[].price` | Цена за единицу, копейки | | `items[].quantity` | Количество | | `items[].amount` | Стоимость позиции. По умолчанию `price × quantity` | | `items[].tax` | `none`, `vat0`, `vat5`, `vat7`, `vat10`, `vat20`, `vat22`, `vat105`, `vat107`, `vat110`, `vat120`, `vat122` | | `items[].payment_method` | По умолчанию `full_payment` | | `items[].payment_object` | По умолчанию `service` | Сумма `items[].amount` должна равняться `amount`, иначе `422 receipt_mismatch`. ## Объект платежа ```json { "id": "pay_8fK2mQ1xZbY7cV3nT0aR", "object": "payment", "livemode": true, "status": "succeeded", "amount": 49900, "refunded_amount": 0, "currency": "RUB", "description": "Подписка Pro на 1 месяц", "order_id": "order-1042", "methods": ["sbp", "card"], "customer": { "email": "user@example.com", "phone": null }, "metadata": { "user_id": "u_123" }, "payment_url": "https://pay.egnetwork.ru/p/pay_8fK2mQ1xZbY7cV3nT0aR", "return_url": "https://example.egnetwork.ru/billing/done", "expires_at": "2026-10-27T13:00:00.000Z", "paid_at": "2026-10-27T12:03:41.512Z", "created_at": "2026-10-27T12:00:00.000Z", "updated_at": "2026-10-27T12:03:41.512Z" } ``` --- # Страница оплаты `payment_url` = `https://pay.egnetwork.ru/p/{id}`. Страница открывается без авторизации — ID платежа длинный и случайный, его нельзя подобрать. Не публикуйте ссылку на платёж в открытых местах. ## Что видит пользователь - Название сервиса, сумму и `description`. - **Компьютер** — QR-код СБП. Пользователь сканирует его камерой телефона или в приложении банка. - **Телефон** — кнопку «Оплатить в приложении банка». Она открывает ссылку НСПК `https://qr.nspk.ru/…`, система предлагает выбрать банк. QR можно показать по кнопке. - **«Оплатить картой»**, если в `methods` есть `card`: открывается защищённая форма банка-эквайера. После оплаты пользователь возвращается на ту же страницу. - Таймер до `expires_at`. Страница опрашивает статус каждые 3 секунды и сразу показывает результат. ## Возврат в сервис Когда платёж оплачен, страница через 5 секунд переходит на `return_url` (или сразу по кнопке «Вернуться в сервис»). К URL добавляются параметры: ```text https://example.egnetwork.ru/billing/done?payment_id=pay_8fK2mQ1xZbY7cV3nT0aR&status=succeeded ``` | Параметр | Значение | |---|---| | `payment_id` | ID платежа | | `status` | Статус на момент перехода: `succeeded`, `expired`, `failed`, `canceled`, `pending` (пользователь нажал «Отменить и вернуться») | > **Не доверяйте `status` из URL.** Его может подставить кто угодно. На странице возврата запросите `GET /api/v1/payments/{payment_id}` со своим ключом или дождитесь вебхука. Без `return_url` страница просто показывает «Оплачено». ## Встраивание - **Редирект** (рекомендуется): `302 Location: payment_url`. - **Telegram WebApp / бот**: откройте `payment_url` через `openLink` или inline-кнопку с URL. Внутри iframe открывать не нужно — приложения банков не всегда запускаются из iframe. - **Своя кнопка в мобильном приложении**: откройте `payment_url` во внешнем браузере или SFSafariViewController/Custom Tabs. --- # Вебхуки Когда статус платежа меняется, EGN Pay отправляет `POST` на `webhook_url` платежа или, если он не указан, на `webhook_url` сервиса. ## События | `type` | Когда | |---|---| | `payment.succeeded` | Платёж оплачен | | `payment.failed` | Банк отклонил оплату | | `payment.expired` | Время на оплату истекло | | `payment.canceled` | Платёж отменён | | `payment.partially_refunded` | Частичный возврат (приходит на каждый возврат) | | `payment.refunded` | Возвращена вся сумма | ## Формат ```http POST /api/egn-pay/webhook HTTP/1.1 Content-Type: application/json User-Agent: EGN-Pay-Webhooks/1.0 X-EGN-Event: payment.succeeded X-EGN-Delivery: evt_Q2w9… X-EGN-Signature: t=1793095421,v1=5f1c…e9 ``` ```json { "id": "evt_Q2w9sLk3VbN8mXc1Hj4R", "type": "payment.succeeded", "created_at": "2026-10-27T12:03:41.600Z", "livemode": true, "data": { "payment": { "id": "pay_8fK2mQ1xZbY7cV3nT0aR", "object": "payment", "status": "succeeded", "amount": 49900, "order_id": "order-1042", "metadata": { "user_id": "u_123" } } } } ``` `data.payment` — полный [объект платежа](/docs/payments#объект-платежа) на момент события. ## Ответ Ответьте любым кодом `2xx` в течение 10 секунд. Тело ответа не важно. Всё остальное (`3xx`, `4xx`, `5xx`, таймаут) считается неудачей. Редиректы не выполняются. Тяжёлую работу (выдачу товара, письма) лучше ставить в очередь и отвечать сразу. ## Повторы После неудачи доставка повторяется через: 1 мин, 5 мин, 30 мин, 2 ч, 6 ч, 24 ч, 24 ч. Это 8 попыток примерно за 2,5 суток. Администратор может переотправить вебхук вручную в админке. Вебхуки могут прийти **повторно** или **не по порядку**. Поэтому: - Дедуплицируйте по `id` события (`X-EGN-Delivery`) или делайте выдачу товара идемпотентной по `data.payment.id`. - Ориентируйтесь на `data.payment.status`, а не на порядок прихода. В сомнительных случаях запросите `GET /api/v1/payments/{id}`. ## Проверка подписи `X-EGN-Signature: t=,v1=`, где ```text v1 = HMAC-SHA256(key = webhook_secret, message = t + "." + сырое_тело_запроса) ``` 1. Возьмите **сырое** тело запроса, до JSON-парсинга. 2. Посчитайте HMAC и сравните с `v1` за постоянное время. 3. Отклоните запрос, если `t` отличается от текущего времени больше чем на 5 минут (защита от повтора). ### Node.js / Next.js ```ts import { createHmac, timingSafeEqual } from "node:crypto"; export function verifyEgnPay(rawBody: string, header: string | null, secret: string) { if (!header) return false; const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2))); const t = Number(parts.t); if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"); return expected.length === parts.v1?.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1)); } // app/api/egn-pay/webhook/route.ts export async function POST(req: Request) { const raw = await req.text(); if (!verifyEgnPay(raw, req.headers.get("x-egn-signature"), process.env.EGN_PAY_WEBHOOK_SECRET!)) { return new Response("bad signature", { status: 400 }); } const event = JSON.parse(raw); if (event.type === "payment.succeeded") { await grantOrder(event.data.payment.order_id, event.data.payment.id); // идемпотентно! } return new Response("ok"); } ``` ### Python (FastAPI) ```python import hmac, hashlib, time, os from fastapi import FastAPI, Request, HTTPException app = FastAPI() SECRET = os.environ["EGN_PAY_WEBHOOK_SECRET"].encode() def verify(raw: bytes, header: str | None) -> bool: if not header: return False parts = dict(p.split("=", 1) for p in header.split(",")) t = int(parts.get("t", 0)) if abs(time.time() - t) > 300: return False expected = hmac.new(SECRET, f"{t}.".encode() + raw, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts.get("v1", "")) @app.post("/egn-pay/webhook") async def webhook(request: Request): raw = await request.body() if not verify(raw, request.headers.get("x-egn-signature")): raise HTTPException(400, "bad signature") event = await request.json() ... return {"ok": True} ``` ### PHP ```php $raw = file_get_contents('php://input'); parse_str(str_replace(',', '&', $_SERVER['HTTP_X_EGN_SIGNATURE'] ?? ''), $sig); $expected = hash_hmac('sha256', $sig['t'] . '.' . $raw, getenv('EGN_PAY_WEBHOOK_SECRET')); if (abs(time() - (int)$sig['t']) > 300 || !hash_equals($expected, $sig['v1'] ?? '')) { http_response_code(400); exit('bad signature'); } $event = json_decode($raw, true); ``` ### Go ```go func verify(raw []byte, header, secret string) bool { var t, v1 string for _, p := range strings.Split(header, ",") { kv := strings.SplitN(p, "=", 2) if len(kv) == 2 && kv[0] == "t" { t = kv[1] } if len(kv) == 2 && kv[0] == "v1" { v1 = kv[1] } } ts, err := strconv.ParseInt(t, 10, 64) if err != nil || math.Abs(float64(time.Now().Unix()-ts)) > 300 { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(t + ".")) mac.Write(raw) return hmac.Equal([]byte(hex.EncodeToString(mac.Sum(nil))), []byte(v1)) } ``` --- # Тестирование Используйте ключ `egn_test_…`. Тестовые платежи полностью повторяют боевой поток: страница оплаты, QR, смена статусов, вебхуки, возвраты. Деньги при этом не списываются. ## Что стоит за test-режимом | Настройка EGN Pay | `provider` в `/api/v1/me` | Поведение | |---|---|---| | Задан тестовый терминал Т-Банка (`…DEMO`) | `tbank` | Платёж создаётся в Т-Банке. Оплату эмулирует метод `SbpPayTest`, статус приходит обычным уведомлением банка | | Тестовый терминал не задан | `emulator` | Т-Банк не вызывается. QR содержит фиктивную ссылку НСПК, статус меняется сразу | ## Симуляция оплаты На странице тестового платежа есть блок «Тестовый платёж» с кнопками: - **Симулировать оплату** — платёж станет `succeeded`, придёт `payment.succeeded`. - **Отказ** — платёж станет `failed`, придёт `payment.failed`. То же самое через API, например в автотестах: ```bash curl -X POST https://pay.egnetwork.ru/api/public/payments/pay_…/simulate \ -H "Content-Type: application/json" \ -d '{"outcome":"success"}' # success | rejected | expired ``` Для live-платежей метод вернёт `403 live_payment`. ## Чек-лист перед запуском - [ ] Платёж создаётся, `payment_url` открывается - [ ] Вебхук доходит, подпись проверяется, ответ `2xx` - [ ] Повторный вебхук не выдаёт товар второй раз - [ ] Повторный `POST /payments` с тем же `order_id` не создаёт дубль - [ ] Страница `return_url` проверяет статус через API, а не по параметру `status` - [ ] `payment.expired` и `payment.failed` корректно закрывают заказ - [ ] Возврат (`/refund`) отражается у вас в системе - [ ] Заменили `egn_test_` на `egn_live_` --- # Ошибки Ошибка — это HTTP-код не из `2xx` и тело вида: ```json { "error": { "code": "validation_error", "message": "Некорректные параметры запроса", "details": { "issues": [{ "path": "amount", "message": "минимум 100 копеек (1 ₽)" }] } } } ``` Ветвитесь по `error.code`. `message` написан для человека и может меняться. | HTTP | `code` | Что делать | |---|---|---| | 400 | `invalid_json` | Тело не JSON | | 401 | `unauthorized` | Проверьте ключ и заголовок `Authorization` | | 403 | `merchant_disabled` | Сервис отключён, обратитесь к администратору | | 403 | `live_payment` | Симуляция для live-платежа запрещена | | 404 | `not_found` | Платёж не найден (или он из другого режима или сервиса) | | 409 | `order_id_conflict` | Платёж с этим `order_id` уже есть с другой суммой | | 409 | `order_closed` | Платёж с этим `order_id` уже завершён без оплаты, нужен новый `order_id` | | 409 | `not_cancelable` | Отменить можно только `pending` | | 409 | `not_refundable` | Вернуть можно только `succeeded` / `partially_refunded` | | 409 | `not_pending` | Платёж уже не ожидает оплаты | | 422 | `validation_error` | Ошибки полей в `details.issues` (в т.ч. не передан `customer.email` / `customer.phone`) | | 422 | `invalid_return_url` | `return_url` должен быть `https` | | 422 | `return_url_not_allowed` | Хост `return_url` не в `allowed_return_hosts` сервиса | | 422 | `receipt_mismatch` | Сумма позиций чека не равна `amount` | | 422 | `receipt_contact_required` | Для чека нужен email или телефон | | 422 | `invalid_refund_amount` | Сумма возврата больше остатка | | 500 | `internal_error` | Ошибка у нас. Повторите позже | | 502 | `provider_error` | Т-Банк вернул ошибку, код в `details.provider_code`. Платёж **не** создан, можно повторить с тем же `order_id` | | 503 | `provider_not_configured` | Боевой терминал не настроен в EGN Pay | ## Ретраи Повторять безопасно: - `POST /payments` с тем же `order_id`: дубля не будет; - любые `GET`; - `502`, `500` и сетевые таймауты, лучше с экспоненциальной задержкой. Не повторяйте `4xx` без исправления запроса. --- # SDK для TypeScript Один файл без зависимостей. Работает в Node.js 18+, Bun, Deno, Next.js (включая edge). Скачайте его в проект: ```bash curl -o src/lib/egn-pay.ts https://pay.egnetwork.ru/sdk/egn-pay.ts ``` Типы в SDK совпадают с [OpenAPI](https://pay.egnetwork.ru/openapi.json). Обновить SDK можно той же командой. ## Создать платёж ```ts import { EgnPay, EgnPayError } from "@/lib/egn-pay"; const pay = new EgnPay(process.env.EGN_PAY_KEY!); try { const payment = await pay.payments.create({ amount: 49900, order_id: `order-${order.id}`, description: "Подписка Pro на 1 месяц", return_url: "https://example.egnetwork.ru/billing/done", customer: { email: user.email }, metadata: { user_id: String(user.id) }, }); redirect(payment.payment_url); } catch (err) { if (err instanceof EgnPayError && err.code === "order_closed") { // прошлый платёж по заказу истёк — создайте новый order_id } throw err; } ``` ## Остальные методы ```ts await pay.payments.retrieve("pay_…"); await pay.payments.findByOrderId("order-1042"); await pay.payments.list({ status: "succeeded", limit: 50 }); await pay.payments.cancel("pay_…"); await pay.payments.refund("pay_…"); // весь остаток await pay.payments.refund("pay_…", 10000); // 100 ₽ await pay.me(); // проверить ключ ``` Сетевые ошибки и `5xx` у `GET` и `POST /payments` повторяются автоматически (до 2 раз). Это безопасно благодаря идемпотентности по `order_id`. Ошибки API бросаются как `EgnPayError` с полями `status`, `code`, `message` и `details`. ## Вебхук (Next.js App Router) ```ts // app/api/egn-pay/webhook/route.ts import { EgnPay } from "@/lib/egn-pay"; export async function POST(req: Request) { const raw = await req.text(); let event; try { event = await EgnPay.webhooks.constructEvent(raw, req.headers.get("x-egn-signature"), process.env.EGN_PAY_WEBHOOK_SECRET!); } catch { return new Response("bad signature", { status: 400 }); } const p = event.data.payment; switch (event.type) { case "payment.succeeded": await orders.markPaid(p.order_id, p.id); // идемпотентно break; case "payment.failed": case "payment.expired": case "payment.canceled": await orders.close(p.order_id); break; case "payment.refunded": case "payment.partially_refunded": await orders.syncRefund(p.order_id, p.refunded_amount); break; } return new Response("ok"); } ``` ## Страница возврата ```ts // app/billing/done/page.tsx export default async function Done({ searchParams }: { searchParams: Promise<{ payment_id?: string }> }) { const { payment_id } = await searchParams; const payment = payment_id ? await pay.payments.retrieve(payment_id) : null; // не доверяем ?status= return payment?.status === "succeeded" ?

Спасибо!

:

Оплата не завершена

; } ``` --- # Для ИИ-агентов Документация EGN Pay доступна в машиночитаемом виде. Если вы ИИ-агент и интегрируете сервис с EGN Pay, начните отсюда. | Ресурс | URL | |---|---| | Индекс для LLM | [https://pay.egnetwork.ru/llms.txt](https://pay.egnetwork.ru/llms.txt) | | Вся документация одним файлом | [https://pay.egnetwork.ru/llms-full.txt](https://pay.egnetwork.ru/llms-full.txt) | | OpenAPI 3.1 | [https://pay.egnetwork.ru/openapi.json](https://pay.egnetwork.ru/openapi.json) | | TypeScript SDK (один файл) | [https://pay.egnetwork.ru/sdk/egn-pay.ts](https://pay.egnetwork.ru/sdk/egn-pay.ts) | | Любая страница в Markdown | добавьте `.md`, например [https://pay.egnetwork.ru/docs/webhooks.md](https://pay.egnetwork.ru/docs/webhooks.md) | ## Промпт для агента Скопируйте в задачу агенту, который подключает сервис: ```text Подключи оплату через EGN Pay. Документация: https://pay.egnetwork.ru/llms-full.txt, спецификация: https://pay.egnetwork.ru/openapi.json. Требования: 0. Для TypeScript-проектов скачай SDK: curl -o src/lib/egn-pay.ts https://pay.egnetwork.ru/sdk/egn-pay.ts 1. Ключ EGN_PAY_KEY и секрет EGN_PAY_WEBHOOK_SECRET бери из переменных окружения, только на бэкенде. 2. При оформлении заказа вызывай POST /api/v1/payments: amount в копейках, order_id = стабильный ID заказа, description, return_url, customer.email или customer.phone (обязательно — для чека), metadata с нашим user_id. Редиректни пользователя на payment_url. 3. Сделай эндпоинт вебхука: проверь подпись X-EGN-Signature по сырому телу (HMAC-SHA256, "t.body", допуск 5 минут), на payment.succeeded идемпотентно выдай заказ по data.payment.order_id, на failed/expired/canceled закрой заказ, ответь 200. 4. На странице return_url не доверяй параметру status — запроси GET /api/v1/payments/{payment_id}. 5. Покрой тестом на egn_test_ ключе через POST /api/public/payments/{id}/simulate. ``` ## Ключевые факты - Базовый URL API: `https://pay.egnetwork.ru/api/v1` - Аутентификация: `Authorization: Bearer egn_live_…` / `egn_test_…` - Суммы — целые числа в копейках, валюта RUB - Идемпотентность — по `order_id` в пределах сервиса и режима - Вебхуки: `X-EGN-Signature: t=…,v1=hex(HMAC_SHA256(secret, t + "." + body))` - Статусы: `pending`, `succeeded`, `failed`, `expired`, `canceled`, `partially_refunded`, `refunded`