Для розробників

API і webhooks

Ваша система (інтернет-магазин, CRM, облікова програма) створює рахунки в гривнях за API-ключем, веде покупця на сторінку оплати й дізнається про оплату з підписаного webhook. Покупець платить напряму на ваш гаманець: API ніколи не переказує кошти.

Адреса API
https://api.potiq.io
Маршрути без префікса: POST /invoices — це запит на цю адресу плюс /invoices. Лише HTTPS.

Початок роботи

  1. Адміністратор бізнесу відкриває в кабінеті Інтеграції і створює API-ключ. Ключ показується один раз: зберігайте його в секретах вашого сервера, ніколи в браузері чи мобільному застосунку.
  2. На тому ж екрані збережіть адресу вашого приймача webhooks і збережіть його секрет підпису (теж показується один раз).
  3. Створіть рахунок через POST /invoices, відкрийте його paymentUrl і дочекайтеся invoice.paid на приймачі. Доставки та їхні помилки видно на екрані «Інтеграції».

Домовленості

  • JSON через HTTPS, UTF-8. Із тілом запиту надсилайте Content-Type: application/json.
  • Гроші — це десятковий рядок, ніколи не JSON-число: "amountUah": "250.00", "expectedAmount": "6.131000". Суми USDT мають повну точність токена (6 знаків у TRON, 18 у BNB Smart Chain). Не перетворюйте їх на числа з плаваючою комою.
  • Ідентифікатори — UUID. Мережі: TRON_TRC20 (USDT TRC20) і BSC_BEP20 (USDT BEP20).
  • Час — ISO 8601 в UTC: 2026-11-11T09:30:00.000Z.
  • Невідоме поле в тілі запиту відхиляється з 400 VALIDATION_FAILED.

Автентифікація

Кожен запит передає ключ у заголовку X-Api-Key:

X-Api-Key: potiq_…
  • Ключ працює лише з шістьма методами рахунків, описаними нижче.
  • Будь-який інший маршрут відповідає 403 API_KEY_NOT_ALLOWED: гаманці, касири й налаштування лишаються в кабінеті.
  • Невідомий або відкликаний ключ отримує 401 Unauthorized.
  • Запит одночасно з X-Api-Key і Authorization відхиляється (400 VALIDATION_FAILED, код ambiguous_credentials).
  • Ключ належить бізнесу, а не людині, яка його створила, і бачить усі рахунки бізнесу, зокрема створені касирами. Рахунок іншого бізнесу відповідає 404, ніби його немає.
  • Відкличте ключ на екрані «Інтеграції», щойно він міг потрапити в чужі руки, і створіть новий. Ми зберігаємо лише його відбиток.

Методи рахунків

Рахунок — це сума в гривнях, перерахована в точну суму USDT за зафіксованим курсом. Покупець має надіслати рівно expectedAmount на recipientAddress у мережі рахунку до expiresAt.

Створити рахунок

POST/invoices

Фіксує курс, бере гаманець бізнесу за замовчуванням у цій мережі й повертає рахунок із посиланням на оплату. Відправте покупця на paymentUrl або покажіть QR із /qr.svg.

НазваДеПравила
amountUahтілоОбов’язкове. Рядок до 12 цифр і до 2 знаків після крапки, більше за нуль: "250", "250.5", "250.50". Коди: required, invalid_amount.
networkтілоОбов’язкове. TRON_TRC20 або BSC_BEP20; мережа має бути увімкнена в налаштуваннях бізнесу. Код: invalid_network.
walletIdтілоНеобов’язкове. UUID активного гаманця бізнесу в цій мережі замість гаманця за замовчуванням. Код: invalid_wallet.
pointIdтілоНеобов’язкове. UUID точки оплати (каси); у точки щонайбільше один активний рахунок. Код: invalid_point.
noteтілоНеобов’язкове. Номер замовлення чи коментар, видно в кабінеті. Переноси рядків і повторні пробіли стають одним пробілом; до 120 символів, без керівних символів. Коди: too_long, invalid_note.
Idempotency-KeyзаголовокНеобов’язковий, рекомендований. Від 1 до 200 друкованих символів ASCII, наприклад id вашого замовлення. Повтор із тим самим ключем і тим самим тілом повертає вже створений рахунок замість нового; ключ запам’ятовується назавжди. Код: invalid_idempotency_key.

Відповідь: 201 з об’єктом рахунку; 200 з раніше створеним рахунком для повторного Idempotency-Key. Той самий ключ з іншим тілом отримує 409 IDEMPOTENCY_KEY_REUSED.

Помилки, крім загальних: 403 BUSINESS_SUSPENDED, 503 INVOICE_CREATION_PAUSED, 422 NETWORK_DISABLED, 503 NETWORK_PAUSED, 422 NO_DEFAULT_WALLET, 404 WALLET_NOT_FOUND, 404 POINT_NOT_FOUND, 422 POINT_DISABLED, 409 POINT_BUSY, 409 IDEMPOTENCY_KEY_REUSED, 503 RATE_UNAVAILABLE, 503 NO_FREE_AMOUNT

curl
curl -X POST https://api.potiq.io/invoices \
  -H "X-Api-Key: $POTIQ_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042" \
  -d '{"amountUah": "250.00", "network": "TRON_TRC20", "note": "Order 1042"}'

Список рахунків

GET/invoices

Рахунки бізнесу, новіші першими, посторінково.

НазваДеПравила
statusqueryНеобов’язковий. Один статус, наприклад PAID. Код: invalid_status.
networkqueryНеобов’язковий. TRON_TRC20 або BSC_BEP20. Код: invalid_network.
pointIdqueryНеобов’язковий. UUID точки оплати. Код: invalid_point.
fromqueryНеобов’язковий. Створені в цей момент або пізніше, ISO 8601 із Z або зсувом. Код: invalid_date.
toqueryНеобов’язковий. Створені раніше цього моменту (не включно); має бути пізніше за from. Коди: invalid_date, invalid_range.
limitqueryНеобов’язковий. Від 1 до 100, за замовчуванням 20. Код: invalid_limit.
cursorqueryНеобов’язковий. nextCursor попередньої сторінки без змін. Код: invalid_cursor.

Відповідь: 200 з items (об’єкти рахунків) і nextCursor (null на останній сторінці).

curl
curl "https://api.potiq.io/invoices?status=PAID&from=2026-11-01T00:00:00Z&limit=50" \
  -H "X-Api-Key: $POTIQ_API_KEY"

Отримати рахунок

GET/invoices/:id

Рахунок із поточним статусом і пов’язаними переказами в блокчейні.

Відповідь: 200 з об’єктом рахунку.

Помилки, крім загальних: 404 INVOICE_NOT_FOUND

curl
curl https://api.potiq.io/invoices/$INVOICE_ID -H "X-Api-Key: $POTIQ_API_KEY"

Отримати статус

GET/invoices/:id/status

Коротка відповідь для опитування: статус і підтвердження. Краще використовуйте webhooks; опитуйте лише як запасний варіант і не частіше ніж раз на кілька секунд.

Відповідь: 200 з об’єктом статусу.

Помилки, крім загальних: 404 INVOICE_NOT_FOUND

curl
curl https://api.potiq.io/invoices/$INVOICE_ID/status -H "X-Api-Key: $POTIQ_API_KEY"

Скасувати рахунок

POST/invoices/:id/cancel

Можна лише поки рахунок у статусі CREATED або AWAITING_PAYMENT і його expiresAt ще не минув. Повторне скасування вже скасованого рахунку безпечне й дає ту саму відповідь.

Відповідь: 200 з об’єктом рахунку. Якщо платіж уже виявлено, рахунок фінальний або строк минув: 409 INVOICE_NOT_CANCELLABLE.

Помилки, крім загальних: 404 INVOICE_NOT_FOUND, 409 INVOICE_NOT_CANCELLABLE

curl
curl -X POST https://api.potiq.io/invoices/$INVOICE_ID/cancel -H "X-Api-Key: $POTIQ_API_KEY"

Отримати QR-код

GET/invoices/:id/qr.svg

SVG-зображення QR-коду з paymentUrl рахунку для екрана чи чека. Він містить посилання на сторінку оплати, а не адресу гаманця.

Відповідь: 200 з Content-Type: image/svg+xml.

Помилки, крім загальних: 404 INVOICE_NOT_FOUND

curl
curl https://api.potiq.io/invoices/$INVOICE_ID/qr.svg -H "X-Api-Key: $POTIQ_API_KEY" -o invoice-qr.svg

Об’єкти

Рахунок

Цей об’єкт повертає кожен метод рахунків; його ж містять webhooks.

invoice
{
  "id": "0b9f7c1e-6c1d-4d44-9a55-3f1e0e2b7c11",
  "businessId": "5d2c1a7e-1f0b-4c3e-8a9d-2b7e6f4a1c90",
  "businessName": null,
  "pointId": null,
  "pointName": null,
  "note": "Order 1042",
  "status": "AWAITING_PAYMENT",
  "failureReason": null,
  "amountUah": "250.00",
  "network": "TRON_TRC20",
  "recipientAddress": "T… (your wallet)",
  "baseAmount": "6.100000",
  "commissionAmount": null,
  "uniqueAdjustment": "0.031000",
  "expectedAmount": "6.131000",
  "addCommissionToBill": false,
  "rate": { "value": "40.980000", "source": "whitebit", "fetchedAt": "2026-11-11T09:30:00.000Z" },
  "paymentUrl": "https://shop.example.com/i/3f9c…",
  "expiresAt": "2026-11-11T09:45:00.000Z",
  "createdAt": "2026-11-11T09:30:00.000Z",
  "updatedAt": "2026-11-11T09:30:00.000Z",
  "transfers": []
}
ПолеЗначення
idІдентифікатор рахунку.
statusПоточний статус, див. «Статуси».
failureReasonnull, якщо статус не FAILED.
amountUahСума в гривнях, яку ви вказали.
networkTRON_TRC20 або BSC_BEP20.
recipientAddressАдреса вашого гаманця, на яку платить покупець.
baseAmountamountUah, перерахована за зафіксованим курсом, у USDT.
commissionAmountКомісія платформи, додана до суми покупця, або null, якщо її щотижня виставляють бізнесу (налаштування бізнесу).
uniqueAdjustmentНевелика дробова частина, додана, щоб сума цього рахунку відрізнялася від усіх інших відкритих на тому ж гаманці.
expectedAmountТочна сума USDT, яка має надійти. Базова сума плюс комісія (якщо додана) плюс унікальна частина.
addCommissionToBillЧи додано комісію до суми покупця.
rateЗафіксований курс: value (гривень за 1 USDT), source і fetchedAt.
paymentUrlСторінка оплати для покупця. Може бути на піддомені вашого бізнесу.
expiresAtСтрок оплати.
noteВаш коментар зі створення або null.
pointIdТочка оплати або null.
pointNameПоточна назва точки або null.
transfersПерекази в блокчейні, пов’язані з рахунком: txHash, amount, confirmations, status.
createdAtЧас створення.
updatedAtЧас останньої зміни.

Статус

Повертає GET /invoices/:id/status. confirmations — кількість блоків над блоком платежу, requiredConfirmations — скільки потрібно для PAID у цій мережі.

status
{
  "id": "0b9f7c1e-6c1d-4d44-9a55-3f1e0e2b7c11",
  "status": "CONFIRMING",
  "failureReason": null,
  "confirmations": 7,
  "requiredConfirmations": 20,
  "expiresAt": "2026-11-11T09:45:00.000Z",
  "updatedAt": "2026-11-11T09:33:10.000Z"
}

Статуси

Звичайний шлях: CREATED → AWAITING_PAYMENT → PAYMENT_DETECTED → CONFIRMING → PAID. Фінальні статуси: PAID, EXPIRED, CANCELLED, FAILED.

СтатусЗначення
CREATEDСтворюється; у webhook ви його не побачите.
AWAITING_PAYMENTГотовий до оплати, чекає на переказ.
PAYMENT_DETECTEDТочний переказ видно в блокчейні, ще не підтверджений. Може повернутися в AWAITING_PAYMENT, якщо блокчейн його не підтвердить.
CONFIRMINGРахуються підтвердження.
PAIDОплачено: потрібну кількість підтверджень досягнуто.
EXPIREDСтрок минув без точного переказу.
CANCELLEDСкасовано до виявлення платежу.
FAILEDФінальна невдача з failureReason. Для нових рахунків не використовується; тимчасова недоступність вузла блокчейну ніколи не є невдачею.
UNDERPAIDБільше не встановлюється: менший переказ іде на ручний розгляд.
OVERPAIDБільше не встановлюється: більший переказ іде на ручний розгляд.
  • Відвантажуйте товар лише при PAID (webhook invoice.paid). PAYMENT_DETECTED і CONFIRMING — ще не оплата.
  • Автоматично рахунок оплачує лише переказ рівно на expectedAmount. Комісії мережі й біржі сплачує покупець: на ваш гаманець має надійти точна сума.
  • Переказ іншої суми, запізнілий чи неоднозначний іде на ручний розгляд платформою; рахунок тим часом лишається AWAITING_PAYMENT.
  • Якщо після розгляду платформа зарахує запізнілий платіж, рахунок EXPIRED ще може стати PAID: тоді ви отримаєте invoice.paid.
  • Строк оплати задає платформа; беріть його з expiresAt, а не розраховуйте на фіксований час.

Помилки

Помилка має статус і стабільний код у message. Орієнтуйтеся на код, а не на інший текст. Помилка валідації додає issues, кожен із path поля і кодом.

error
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "VALIDATION_FAILED",
  "issues": [{ "path": "amountUah", "message": "invalid_amount" }]
}
СтатусКодЗначення
400VALIDATION_FAILEDТіло, query чи заголовок не пройшли валідацію; див. issues.
401UnauthorizedКлюча немає, або він невідомий чи відкликаний.
403API_KEY_NOT_ALLOWEDМаршрут не є методом рахунків.
403BUSINESS_SUSPENDEDУ бізнесу прострочений рахунок платформи; нові рахунки заблоковані до його оплати.
404INVOICE_NOT_FOUNDТакого рахунку у вашому бізнесі немає.
404WALLET_NOT_FOUNDТакого активного гаманця вашого бізнесу в цій мережі немає.
404POINT_NOT_FOUNDТакої точки оплати у вашому бізнесі немає.
409IDEMPOTENCY_KEY_REUSEDIdempotency-Key уже використано з іншим тілом або іншим ключем чи людиною.
409INVOICE_NOT_CANCELLABLEПлатіж виявлено, рахунок фінальний або строк минув.
409POINT_BUSYУ точки оплати вже є активний рахунок.
422NETWORK_DISABLEDМережу вимкнено в налаштуваннях бізнесу.
422NO_DEFAULT_WALLETУ бізнесу немає гаманця за замовчуванням у цій мережі.
422POINT_DISABLEDТочку оплати вимкнено.
429RATE_LIMITEDЗабагато запитів; зачекайте стільки, скільки вказано в Retry-After.
500INTERNAL_ERRORНеочікуваний збій на нашому боці.
503RATE_UNAVAILABLEКурс зараз недоступний; повторіть пізніше.
503INVOICE_CREATION_PAUSEDПлатформа призупинила створення рахунків на час робіт; повторіть пізніше. Наявні рахунки обробляються як звичайно.
503NETWORK_PAUSEDПлатформа призупинила нові рахунки в цій мережі; повторіть пізніше або оберіть іншу мережу.
503NO_FREE_AMOUNTЗараз немає вільної унікальної суми; повторіть пізніше.
  • 429 і 503 можна повторювати. Для POST /invoices повторюйте з тим самим Idempotency-Key: ви ніколи не отримаєте двох рахунків на одне замовлення.
  • Кожна відповідь має заголовок X-Request-Id. Вказуйте його, коли повідомляєте про проблему.

Ліміти запитів

120 запитів на хвилину на ключ і не більше 600 на хвилину з однієї IP-адреси незалежно від ключа. Понад ліміт відповідь — 429 RATE_LIMITED із заголовком Retry-After у секундах.

Webhooks

При кожній зміні статусу рахунку ми надсилаємо POST на ваш приймач. Подія створюється в тій самій транзакції бази даних, що й зміна, тож жодна не губиться і ми не дублюємо їх.

Приймач

  • Адреса зберігається на екрані Інтеграції: лише https://, порт 443 або 8443, публічний хост (приватні й локальні адреси відхиляються).
  • Секрет підпису whsec_… показується один раз, при першому збереженні. Зберігайте його на своєму сервері.
  • Там же можна будь-коли згенерувати новий секрет; старий одразу перестає підписувати.
  • Вимкнений приймач не отримує нових подій, а недоставлені завершуються помилкою.

Події

ТипНадсилається, коли рахунок стає
invoice.awaiting_paymentAWAITING_PAYMENT: готовий до оплати (одразу після створення).
invoice.payment_detectedPAYMENT_DETECTED: переказ видно.
invoice.confirmingCONFIRMING: рахуються підтвердження.
invoice.paidPAID: єдиний сигнал оплати.
invoice.expiredEXPIRED: строк минув.
invoice.cancelledCANCELLED: скасовано.
invoice.failedFAILED: фінальна невдача.
invoice.underpaidЗарезервовано, не надсилається.
invoice.overpaidЗарезервовано, не надсилається.

Кожен рахунок отримує щонайбільше одну подію кожного типу. Події одного рахунку можуть прийти в будь-якому порядку: порівнюйте occurredAt або запитуйте GET /invoices/:id.

Запит

POST
{
  "id": "6f0f6a7e-0a47-4d6a-9c2c-0d2f6f3b7a90",
  "type": "invoice.paid",
  "occurredAt": "2026-11-11T09:36:12.000Z",
  "data": {
    "previousStatus": "CONFIRMING",
    "invoice": { "id": "0b9f7c1e-…", "status": "PAID", "…": "the invoice object" }
  }
}

data.invoice — об’єкт рахунку на момент першої спроби; кожна повторна спроба надсилає те саме тіло. id — ідентифікатор події, однаковий у всіх спробах. Порядок ключів у тілі не фіксований.

ЗаголовокЗначення
X-Webhook-IdІдентифікатор події (UUID), однаковий у всіх спробах.
X-Webhook-TimestampUnix-час у секундах, коли цю спробу підписано.
X-Webhook-Signaturev1= і підпис, див. нижче.
Content-Typeapplication/json
User-Agentpotiq-webhooks/1

Підпис

HMAC-SHA256 у hex нижнім регістром із префіксом v1=. Ключ — увесь рядок секрету як є, разом із whsec_, у байтах UTF-8: не декодуйте його й не відрізайте префікс. Повідомлення — мітка часу, крапка і точні байти тіла.

X-Webhook-Signature = "v1=" + hex(HMAC_SHA256(secret, timestamp + "." + rawBody))

Перевірте свій код на цьому прикладі:

secret    = whsec_test
timestamp = 1761559200
rawBody   = {"id":"e1"}
signature = v1=d1a0f17f3d38634317582ea30366b93c4c20e87dae498d8282feffda5de354b5

Правила для приймача

  • Перевіряйте підпис по сирих байтах тіла, до будь-якого розбору JSON.
  • Порівнюйте підписи за сталий час.
  • Відхиляйте X-Webhook-Timestamp, що відрізняється від вашого годинника більш ніж на 5 хвилин.
  • Запам’ятовуйте оброблені X-Webhook-Id і пропускайте повтори: доставка може прийти більше одного разу.
  • Відповідайте 2xx протягом 10 секунд, а роботу виконуйте потім (через чергу).
  • Не покладайтеся на порядок подій.

Доставка і повтори

Будь-яка відповідь 2xx — успіх. Усе інше, тайм-аут (10 секунд) чи редирект — невдача, і спроба повторюється через 1 хвилину, 5 хвилин, 30 хвилин, 2 години і 12 годин: 6 спроб приблизно за 15 годин. Після цього доставка позначається невдалою й показується на екрані «Інтеграції»; щоб наздогнати, прочитайте рахунок через GET /invoices/:id.

Приклади

Створити рахунок

Ключ читається зі змінної середовища POTIQ_API_KEY. Id замовлення водночас є Idempotency-Key, тож повтор після тайм-ауту безпечний.

Node.js
// Node.js 18+ (built-in fetch)
const BASE_URL = "https://api.potiq.io";

export async function createInvoice(orderId, amountUah) {
  const res = await fetch(`${BASE_URL}/invoices`, {
    method: "POST",
    headers: {
      "X-Api-Key": process.env.POTIQ_API_KEY,
      "Content-Type": "application/json",
      // The same key for the same order: a retry returns the same invoice.
      "Idempotency-Key": `order-${orderId}`,
    },
    body: JSON.stringify({ amountUah, network: "TRON_TRC20", note: `Order ${orderId}` }),
  });
  const body = await res.json();
  if (!res.ok) {
    // body.message is a stable code, e.g. RATE_UNAVAILABLE.
    throw new Error(`${res.status} ${body.message}, request ${res.headers.get("x-request-id")}`);
  }
  // 201: a new invoice, 200: the invoice created earlier with this Idempotency-Key.
  return body; // send the customer to body.paymentUrl
}

Перевірити webhook

Секрет читається з POTIQ_WEBHOOK_SECRET. Кожен приклад перевірено на прикладі підпису вище.

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

// secret: the whole "whsec_..." string from the cabinet, used as is.
// rawBody: the exact bytes received (a Buffer), never a re-serialised object.
export function verifyWebhook(rawBody, timestamp, signature, secret, now = Date.now()) {
  if (!/^\d+$/.test(timestamp ?? "") || !signature) return false;
  if (Math.abs(now / 1000 - Number(timestamp)) > 300) return false; // 5 minutes
  const expected =
    "v1=" + createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && timingSafeEqual(a, b);
}

// Express: take the raw body for this route.
// app.post("/webhooks/potiq", express.raw({ type: "application/json" }), (req, res) => {
//   const ok = verifyWebhook(req.body, req.get("X-Webhook-Timestamp"),
//     req.get("X-Webhook-Signature"), process.env.POTIQ_WEBHOOK_SECRET);
//   if (!ok) return res.sendStatus(400);
//   const event = JSON.parse(req.body.toString("utf8"));
//   // Skip event.id if it was handled before, queue the work, answer at once.
//   res.sendStatus(200);
// });

Готові приймати USDT?

Створіть акаунт, додайте адресу гаманця й виставте перший рахунок.