Всё взаимодействие — обычный HTTP с JSON. Базовый адрес:
https://api.aurum.example/api/v1
checkout_url.
Интеграционные запросы подписываются ключом проекта. Передавайте его в заголовке
X-API-Key либо как Authorization: Bearer — принимаются оба.
X-API-Key: aur_live_kNyJo-33JxgnwLUjDirM8M1XY489mwWFieAcLIGpxrM
Все суммы передаются строками и в минимальных
единицах. Это не придирка: у токенов бывает 18 знаков после запятой, и
такое число не помещается в number ни в JavaScript, ни в JSON без
потери точности.
| Валюта | Алиас | Знаков | 1 единица |
|---|---|---|---|
| TRX | TRX | 6 | "1000000" |
| USDT (TRC-20) | USDT_TRON | 6 | "1000000" |
25 USDT записываются как "25000000".
POST /integration/invoices
curl https://api.aurum.example/api/v1/integration/invoices \ -H "X-API-Key: aur_live_…" \ -H "Content-Type: application/json" \ -d '{ "order_id": "A-1042", "description": "Подписка Pro, 1 месяц", "amount": "25000000", "currency_alias": "USDT_TRON" }'
| Поле | Описание |
|---|---|
order_id | Ваш номер заказа. Он же ключ идемпотентности: повтор с тем же значением вернёт уже выставленный счёт и код 200 вместо 201. |
amount | Сумма в минимальных единицах, строкой. |
currency_alias | USDT_TRON или TRX. |
description | Необязательно. Покупатель увидит его на странице оплаты. |
order_id тот же,
второго счёта не появится. Без этого двойной клик по кнопке «оплатить» означал
бы, что покупатель может заплатить дважды.
В ответе придёт checkout_url — страница оплаты, — и
address, если адрес уже готов. Изредка адрес приходит с задержкой в
пару секунд; тогда статус будет PENDING_ADDRESS, и адрес появится
при следующем запросе счёта.
GET /integration/invoices/{uuid}
Интеграция нужна не всем. Если заказов немного или вы только пробуете, счёт можно
выставить руками в личном кабинете — вкладка Счета, кнопка
«Выставить счёт». Получится ровно то же самое: та же ссылка на оплату, тот же
адрес, те же уведомления. Тот же order_id и та же идемпотентность.
Если вы продаёте за доллары, передавайте цену в долларах — мы пересчитаем по курсу и зафиксируем его в счёте. Покупатель, открывший ссылку через час, увидит ту же сумму.
{
"order_id": "A-1043",
"fiat_code": "USD",
"fiat_amount": "25.00",
"currency_alias": "TRX"
}
Указывайте либо amount, либо fiat_amount. Оба сразу —
ошибка: две суммы в одном запросе означают, что магазин сам не знает, сколько
хочет получить.
| Статус | Что означает |
|---|---|
PENDING_ADDRESS | Готовим адрес, обычно пара секунд. |
AWAITING_PAYMENT | Ждём перевод. |
UNDERPAID | Пришло меньше запрошенного. В outstanding — сколько осталось. |
PAID | Оплачен полностью. |
EXPIRED | Время вышло. Перевод, пришедший позже, всё равно будет засчитан. |
CANCELLED | Счёт отменён в кабинете. |
Укажите адрес уведомлений в настройках проекта — только https. Мы
отправим POST с телом:
{
"event": "invoice.paid",
"invoice_uuid": "8702702b-bd52-45f7-…",
"order_id": "A-1042",
"currency_alias": "USDT_TRON",
"amount": "25000000",
"received": "25000000",
"status": "PAID",
"paid_at": "2026-08-27T02:41:12Z"
}
События: invoice.paid и invoice.underpaid.
Ответьте любым кодом 2xx. Всё остальное считается неудачей, и мы
повторим — пятнадцать раз с растущей паузой, суммарно почти сутки. Магазин,
упавший ночью и починенный утром, всё равно получит уведомление.
PAID.
Каждый вебхук приходит с заголовком Aurum-Signature:
Aurum-Signature: t=1787797272,v1=5f3a…c81b
Подпись — HMAC-SHA256 от строки "{t}.{тело запроса}" с секретом
проекта. Секрет виден в кабинете рядом с адресом вебхука.
// Node.js import crypto from 'node:crypto' function verify(rawBody, header, secret) { const parts = Object.fromEntries( header.split(',').map((p) => p.split('=')), ) // Отметка времени защищает от повторной отправки перехваченного уведомления. const age = Math.abs(Date.now() / 1000 - Number(parts.t)) if (age > 300) return false const expected = crypto .createHmac('sha256', secret) .update(parts.t + '.' + rawBody) .digest('hex') // timingSafeEqual, а не ===: обычное сравнение подбирается по времени ответа. return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(parts.v1), ) }
JSON.parse и обратной сериализации: порядок ключей и пробелы
изменятся, и подпись не сойдётся.
Оплата во всплывающем окне поверх вашего сайта — покупатель не уходит с оформления заказа. Подключается одним скриптом.
<script src="https://api.aurum.example/widget.js"></script>
<script>
Aurum.open(checkoutURL, {
onPaid: function (invoice) { location.href = '/thanks' },
onClose: function () {}
})
</script>
Ссылку checkoutURL отдаёт ваш сервер — это checkout_url
из ответа на выставление счёта. Если вставить свой скрипт некуда, хватит разметки:
<a data-aurum="https://api.aurum.example/pay/<invoice_uuid>">Оплатить криптой</a>
onPaid. Это сообщение из браузера
покупателя, и подделать его может кто угодно — достаточно открыть консоль.
Отгружайте по вебхуку с подписью; onPaid нужен только чтобы
показать человеку, что можно идти дальше.
Виджет не выставляет счета и не знает вашего ключа. Ключ в браузере — это ключ у всех, кто открыл страницу: им можно выставлять счета от вашего имени и читать чужие платежи.
Тонкий клиент для сервера магазина: выставление счетов и проверка подписи уведомлений. Зависимостей нет, нужен Node 18+.
const { Aurum, verifyWebhook } = require('@aurum/node')
const aurum = new Aurum({ apiKey: process.env.AURUM_API_KEY, baseURL: 'https://api.aurum.example' })
const invoice = await aurum.createInvoice({
order_id: 'A-1042',
currency_alias: 'USDT_TRON',
amount: '12500000',
})
Проверка подписи требует сырого тела запроса: подпись считается
по байтам, и тело, прошедшее через JSON.parse и обратно, отличается
пробелами и порядком полей.
app.post('/webhooks/aurum', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifyWebhook({ secret, header: req.get('Aurum-Signature'), body: req.body })) {
return res.sendStatus(400)
}
// ...
})
GET /balances — по токену кабинета.
Баланс ведётся отдельно по каждой валюте и не сводится в одну: курса на момент зачисления у нас нет, а пересчёт задним числом показал бы сумму, которой вы никогда не видели.
GET /balances/{currency_alias}/history отдаёт движения: каждое
зачисление, каждое удержание комиссии, каждое списание под вывод.
POST /withdrawals
{
"currency_alias": "USDT_TRON",
"amount": "25000000",
"address_to": "TN7hUEFWqqSiFH1K27tDqwPAdRRa7YwbAi"
}
Сумма списывается с баланса сразу, в ответ приходит 202. Дальше
заявка проходит статусы PENDING → SENT →
COMPLETED. Если сеть отвергнет перевод, статус станет
FAILED, а деньги вернутся на баланс обратной проводкой.
Вернуть деньги покупателю можно из кабинета или запросом. По умолчанию возвращается переплата — то, что пришло сверх суммы счёта:
POST /invoices/{invoice_uuid}/refunds
{} // вся переплата, на кошелёк плательщика
Можно указать сумму и адрес явно — например, когда заказ отменён целиком или оплата пришла с нескольких кошельков:
{
"amount": "2500000",
"address_to": "TEqqQkYGtae6ic6BXPYvfCXLfrqji5MY5z"
}
Возврат уходит тем же путём, что и вывод средств: списывается с вашего баланса, требует второго фактора и проходит те же проверки. Сумма всех возвратов по счёту не может превысить полученного по нему.
Настройка проекта. По умолчанию комиссия платформы удерживается из пришедшего — вы получаете меньше суммы заказа. Второй вариант: сумма к оплате увеличивается так, чтобы после удержания вам досталась ровно цена заказа.
PATCH /projects/{project_uuid}
{ "fee_paid_by": "CUSTOMER" } // или MERCHANT
Это не «сумма плюс процент»: комиссия берётся от итога платежа, поэтому счёт на
100 USDT при ставке 1% превращается в 101.010102 — ровно столько, чтобы вам
осталось 100. В ответе счёта обе величины видны: amount — сколько
платит покупатель, merchant_amount — сколько получите вы.
Настройка действует на новые счета. Уже выставленный счёт хранит снимок правил: сумма, названная покупателю, не меняется задним числом.
Ключ можно привязать к адресам, с которых он принимается. Ключ живёт годами в конфиге сервера, попадает в бэкапы, в CI и в скриншоты — привязка к адресу отличает утёкший ключ от рабочего.
PUT /projects/{project_uuid}/keys/{key_uuid}/allowed-ips
{ "allowed_ips": ["203.0.113.10", "198.51.100.0/24"] }
Пустой список означает «откуда угодно». Запрос с чужого адреса получает
403 ip_not_allowed — отдельный код, а не общий отказ, чтобы не
путать его с отозванным ключом. Список меняется без перевыпуска ключа.
Все ошибки приходят в одном виде:
{
"error": "bad_request",
"message": "amount must be a decimal integer in minor units"
}
| Код | HTTP | Когда |
|---|---|---|
bad_request | 400 | Запрос не прошёл проверку. |
unauthorized | 401 | Нет ключа, он неверен или отозван. |
not_found | 404 | Объекта нет либо он не ваш. |
conflict | 409 | Например, недостаточно средств. |
too_many_requests | 429 | Слишком часто. Смотрите Retry-After. |
service_unavailable | 503 | Наша сторона временно не готова — повторите. |
error стабилен и предназначен для кода, message — для
человека и может меняться. Не разбирайте message программно.
Пока система работает в сети TRON Nile. Тестовые TRX выдаёт кран Nile, стоят они ноль, и вести себя всё будет ровно так же, как в боевой сети.
Транзакции удобно смотреть в обозревателе Nile.