Створення платіжної сесії v2
Один USD-заказ, доступні маршрути й однократне виконання.
Сесія — одне замовлення; спроба створюється лише після вибору маршруту. У кабінеті виберіть проєкт і режим, відкрийте Платіжні запити → Створити. JWT власника дозволяє створення без API-ключа за готовності проєкту. Для інтеграції використовуйте секрет проєкту.
Створіть одну сесію на замовлення
До надсилання збережіть тіло запиту, ID замовлення, очікувані проєкт і режим, а також ключ ідемпотентності. Після тайм-ауту чи втрати відповіді повторюйте той самий запит із тим самим ключем.
curl --request POST 'https://api.bramapay.com/v2/payment-sessions' \
--header 'X-Api-Key: YOUR_PROJECT_SECRET_KEY' \
--header 'Idempotency-Key: order-demo-1042-attempt-1' \
--header 'Content-Type: application/json' \
--data '{
"amountUsd": "25.00",
"currency": "USD",
"externalId": "order-demo-1042",
"displayOrderNumber": "1042",
"description": "Example service order",
"expiresInSeconds": 1800
}'| Поле | Контракт |
|---|---|
amountUsd | Додатний десятковий рядок, щонайбільше шість знаків після крапки. Не число з рухомою комою. |
currency | Лише USD. Фіксоване співвідношення обліку стейблкоїнів не є ринковим курсом або гарантією погашення. |
externalId | Обов’язкове поле, 1–200 символів; постійний ідентифікатор замовлення в проєкті й режимі. |
displayOrderNumber | Необов’язковий публічний текст, до 100 кодових точок Unicode. |
description | Необов’язковий публічний текст, до 1000 кодових точок Unicode. Без облікових даних, секретів видачі та персональних даних клієнта. |
expiresInSeconds | Необов’язкове ціле число 1–86400. Типове значення визначається серверною політикою строку рахунку. Отриманий строк фіксований. |
Не передавайте в цей endpoint projectId, mode, ID гаманців, вибраний спосіб оплати й перевизначення URL повернення. Контекст задають API-ключ і налаштування проєкту. Для явного перенесення комісії використовуйте інший контракт: baseAmountUsd, feePayer: "BUYER", pricingVersion: "BUYER_FEE_V1", без amountUsd. Не змішуйте ці варіанти.
Успішна відповідь містить sessionId, externalId, mode/paymentMode, amountUsd, currency, status, expiresAt, checkoutUrl, routes, attempts і, коли застосовно, відомості про ціну та моніторинг. Збережіть зв’язок сесії із замовленням і незмінні цінові параметри. Нова сесія може ще не мати платіжних спроб.
Обробіть відповідь створення
Після успішної відповіді збережіть sessionId, projectId, mode, externalId, amountUsd, currency та знімок розрахунку комісії в замовленні до переходу на checkoutUrl. Нижче неповна вигадана відповідь, а не справжні платіжні реквізити. Можливий routes: []; не вигадуйте маршрут і не просіть переказ, якщо доступних способів немає.
{
"sessionId": "00000000-0000-4000-8000-000000000001",
"projectId": "prj_00000000000000000000000000000000",
"externalId": "order-demo-1042",
"mode": "TEST",
"paymentMode": "TEST",
"amountUsd": "25",
"currency": "USD",
"status": "OPEN",
"checkoutUrl": "https://checkout.example/en/checkout/00000000-0000-4000-8000-000000000001#token=EXAMPLE_NOT_A_REAL_CAPABILITY",
"routes": [],
"attempts": []
}Відкрийте оплату
Спрямуйте покупця на отриманий checkoutUrl без змін. У фрагменті URL міститься токен доступу до сесії: не записуйте повний URL у журнали й аналітику. Не створюйте посилання самостійно й не переносьте токен у query-параметри.
Покупець вибирає доступний спосіб, після чого отримує адресу й точну суму. QR містить лише адресу. Суму слід надіслати одним переказом у зазначеній мережі до завершення строку. Зміна способу й повтори не подовжують строк. Не округлюйте суму, не розділяйте переказ і не просіть доплатити різницю без перевірки.
Перевірте webhook і достовірний статус
Подія оплати сесії — payment_session.paid; для рахунків v1 використовується invoice.paid. integration.test і webhook.test перевіряють з’єднання, а не оплату. Test-подія не має виконувати замовлення Live.
Перевірте підпис v2 на початковому тілі запиту до його розбору для бізнес-обробки. Порядок підписуваних даних:
JSON.stringify([
"vaultless.webhook", 2, eventId, deliveryId, eventType,
timestamp, keyId, attempt, rawBodyAsUtf8String
])Використовуйте HMAC-SHA256, порівняння зі сталим часом, секрет за X-Key-Id і перевірку свіжості часу. Версія — число 2, інші значення конверта — рядки. Перевіряйте X-Idempotency-Key === X-Event-Id. Зберігайте старі нескомпрометовані ключі для раніше створених доставок: ротація не перепідписує їх.
Повний приклад перевірки підпису на Node.js
Передавайте початкові байти HTTP-запиту як rawBody до JSON middleware. У Node HTTP збирайте Buffer-фрагменти з обмеженням 256 КіБ; в Express для цього маршруту встановіть express.raw({type: "application/json", limit: "256kb"}) перед JSON-парсером. Перетворіть назви заголовків на нижній регістр і відхиляйте повторювані заголовки підпису. Вибирайте secret зі свого довіреного списку за x-key-id; невідомий ключ відхиляйте. До перевірки підпису заголовок — лише підказка для пошуку ключа. Синхронізуйте годинник сервера.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyBramaPayWebhook(
rawBody: Buffer,
headers: Record<string, string | undefined>,
secret: string,
) {
const required = (name: string) => {
const value = headers[name.toLowerCase()];
if (!value) throw new Error(`Missing ${name}`);
return value;
};
const version = required("x-webhook-version");
const eventId = required("x-event-id");
const deliveryId = required("x-delivery-id");
const eventType = required("x-event-type");
const timestamp = required("x-timestamp");
const keyId = required("x-key-id");
const attempt = required("x-attempt");
const signature = required("x-signature-sha256");
if (version !== "2" || required("x-idempotency-key") !== eventId)
throw new Error("Invalid webhook envelope");
if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300)
throw new Error("Stale webhook");
if (!/^[0-9a-f]{64}$/.test(signature) || !/^[1-9]\d*$/.test(attempt))
throw new Error("Invalid signature encoding or attempt");
const input = JSON.stringify([
"vaultless.webhook",
2,
eventId,
deliveryId,
eventType,
timestamp,
keyId,
attempt,
rawBody.toString("utf8"),
]);
const expected = Buffer.from(createHmac("sha256", secret).update(input).digest("hex"), "hex");
const actual = Buffer.from(signature, "hex");
if (actual.length !== expected.length || !timingSafeEqual(actual, expected))
throw new Error("Invalid webhook signature");
return { eventId, eventType, keyId };
}Лише після успішної перевірки розберіть JSON, перевірте його структуру й тип події, потім запитайте авторизований статус, як показано нижче. За тимчасової помилки читання статусу або сховища повертайте не 2xx, щоб доставка повторилася. Неправильний підпис відхиляйте без обробки. Перевірений тест з’єднання отримує 2xx, але не запускає видачу товару.
Запитуйте статус із сервера тим самим секретним ключем проєкту:
curl 'https://api.bramapay.com/v2/payment-sessions/SESSION_ID' \
--header 'X-Api-Key: YOUR_PROJECT_SECRET_KEY'Вимагайте status === "PAID"; звірте ID сесії, externalId, проєкт, режим, валюту й очікувану вартість за збереженим ціновим контрактом. Порівнюйте десяткові суми точно. За перенесення комісії налаштуванням проєкту amountUsd інтеграції зберігає вартість замовлення, а baseAmountUsd/payableAmountUsd розкривають складові; явний buyer-контракт має іншу семантику. Вартість замовлення, сума з комісією та точна сума токенів — різні величини.
Рекомендована обробка на боці магазину; це псевдокод вашого застосунку, а не методи API BramaPay:
перевірити raw-підпис, час, тип та ідентифікатор події
знайти збережений зв’язок замовлення із сесією й перевірити статус через API
в одній транзакції бази даних:
вставити (merchant_scope, eventId) з унікальним обмеженням
якщо подію вже оброблено: нічого не виконувати повторно
інакше позначити пов’язане замовлення оплаченим і поставити видачу в чергу один раз
повернути невелику відповідь 2xx після надійного збереженняУсувайте дублікати і за подією, і за виконанням замовлення. Повернення браузера, пошук транзакції, запис для ручної перевірки й неперевірений клієнтський статус недостатні.
Маршрути, сторінка оплати та подія оплати
- Посилання на оплату має вигляд
/{locale}/checkout/{sessionId}#token=…. Сторінка оплати прибирає токен з історії браузера та зберігає його під ключем цієї сесії, щоб перезавантаження та інші вкладки могли його відновити. Якщо сховище браузера заблоковане, для повторного відкриття потрібне початкове повне посилання. - Публічна сторінка оплати викликає
GET /v2/checkout/:idіPOST /v2/checkout/:id/attemptsіз заголовкомX-Checkout-Tokenі тілом на кшталт{ "asset": "USDT", "network": "TRON" }, без cookie та авторизації магазину. Серверній інтеграції магазину ці endpoints не потрібні. - Спроба створюється під час першого вибору маршруту; повторний вибір повертає ті самі адресу, точну суму та строк. Раніше видані спроби лишаються у списку, навіть якщо видача нових маршрутів недоступна.
- Статус сесії не залежить від маршруту, відкритого в покупця: якщо підтвердився маршрут A, а покупець дивиться B, сесія стає
PAID, аwinningAttemptIdвказує на A. - Для спроб сесії видачу замовлення виконують за
payment_session.paid, а не за дочірнімinvoice.paid. Тіло події міститьsessionId,attemptId,invoiceId(спроба-переможець),externalId,projectId,mode,paymentMode,status: "PAID",amountUsd,currency,amountPaid,amountExpected,txHashіchain; у Test також тестову мережу таtest: true. Суми передаються рядками. displayOrderNumberіdescriptionпоказуються покупцю як звичайний текст: HTML і Markdown не інтерпретуються. Без публічного номера замовлення сторінка оплати не підставляє замість ньогоexternalId. Ціни в описі не впливають на суму платежу.- Розділ Платіжні запити в кабінеті обмежений вибраними проєктом і режимом та показує спроби, стан моніторингу, спробу-переможця й класифікацію надходжень. Раніше видані рахунки v1 лишаються в історії старих рахунків; їхні посилання та інтеграції продовжують працювати.
Обробіть помилки та повтори
| Ситуація | Дія інтеграції |
|---|---|
| Тайм-аут, втрата відповіді, загальний 5xx | Результат може бути невідомим. Збережіть початкові тіло й ключ, повторюйте той самий запит. Не створюйте нове замовлення. |
| Конфлікт 409 | Перевірте ключ, тіло, зовнішнє замовлення й готовність проєкту. Не обходьте конфлікт новим ключем. |
| 401 | Перевірте секретний ключ і його проєкт/режим; не публікуйте ключ у діагностиці. |
| 429 | Збільште інтервал та врахуйте доступні відомості про повтор. |
PAYMENT_CREATION_DECLINED | Лише перевірена відповідь із retryable: false, sessionCreated: false, відповідною ідентичністю запиту та квитанцією відмови підтверджує остаточну відмову. Самого HTTP 503 недостатньо. Збережіть квитанцію. |
| Завершення строку, пізній, зайвий або невідповідний переказ | Не просіть платити повторно автоматично. Перевірте статус і зверніться до підтримки з ID, мережею та хешем транзакції. Автоповернення й постійний процес ручного підтвердження не обіцяються. |
Webhooks можуть доставлятися повторно; успішна доставка не гарантується. За цикл виконується до п’яти спроб. Для помилок, що допускають повтор, інтервали становлять 30 секунд, 5 хвилин, 30 хвилин і 2 години. Інші помилки можуть завершити цикл одразу. Невдалу доставку на активний endpoint можна повторити вручну після паузи; повтор не дозволяє видавати замовлення знову. Див. повтори доставки.
Таймер не визначає остаточність результату. Переказ, включений у блок у межах строку, може підтвердитися пізніше. Використовуйте серверні status, monitoringComplete, isFinal; не обіцяйте час підтвердження й не вважайте кожну відповідь про завершення строку остаточною відсутністю оплати.
API повертає status сесії як OPEN, PAID або EXPIRED. Закриття передається окремо в closedAt; CLOSED і CONFIRMING — обчислювані фільтри кабінету, а не значення цього поля API. Перший відповідний підтверджений переказ виграє; додаткові й пізні надходження не дозволяють другу видачу. reviewCandidates — неприписані перекази на адресу, які можуть належати іншим замовленням. Пошук транзакції не означає ручне підтвердження або повернення коштів.