Платіжна інтеграція, яку можна тестувати, звіряти й автоматизувати, не плутаючи платежі клієнтів із виплатами продавцям.
1. Визнач, хто за що відповідає
Твій магазин відповідає за каталог, акаунти продавців, кошик, доставку та виконання замовлень. Wholly Crypto обробляє форму оплати, перевірку платежів, розподіл сум продавцям і схвалені виплати. Запис продавця не є обліковим записом для входу; наявні плагіни магазинів не розподіляють кошики кількох продавців автоматично.
- Один спільний кошик
- Один рахунок клієнта
- Перевірені частки продавців
- Схвалені виплати
Клієнти спочатку платять у гаманці проєкту. Ти контролюєш ключі та зберігаєш кошти, належні продавцям. Це не прямий розподіл від клієнта продавцям і не некастодіальний сервіс для твоїх продавців.
Marketplace підтримує Bitcoin mainnet, підтримувані монети EVM і стандартні токени ERC-20. Продавці отримують актив у мережі, яку використав клієнт, а не автоматичну конвертацію у фіат. Не всі 30 мереж отримання підтримують виплати Marketplace.
2. Розрахуй суми
Троє продавців продають товари по $100 кожен. Для цього прикладу встанови комісію проєкту 4%, без перевизначень на рівні магазину чи продавця.
| Частка | Валова сума | Твоя комісія | Продавець отримує |
|---|---|---|---|
| Кожен продавець | $100 | $4 | $96 |
| Усі троє | $300 | $12 | $288 |
Це еквіваленти за зафіксованим котируванням рахунку, виплачені в криптовалюті. Їхня майбутня доларова вартість не гарантується. Звичайна комісія за обробку 1% використовує $3 із передплаченого балансу для цього рахунку на $300 - один раз, а не окремо для кожного продавця. Коли ця комісія застосовується, з твоєї валової комісії $12 залишається $9 до витрат мережі.
Зберігай окремо вільні нативні монети для комісій Bitcoin або gas EVM. Комісії не повинні витрачати захищену основну суму продавців. Звичайний sweep не може витрачати кошти з адрес отримання Marketplace.
3. Підготуй гаманці й продавців
- У Проєкт → Marketplace → Налаштування увімкни Marketplace, вибери магазини та встанови комісію. Під час налаштування залиш виплати призупиненими, а автоматичні політики - вимкненими.
- Зроби резервні копії гаманців проєкту та бази даних. Увімкни потрібні способи оплати BTC/EVM у магазині, перевір готовність сканерів, поповни кошти на обробку й окремо забезпеч нативні монети для комісій.
- Додай кожного продавця. Збережи відповідність його UUID ідентифікатору продавця у твоєму магазині; external_id може містити це посилання на продавця. Незалежно перевір і схвали кожну адресу виплат у точному блокчейні та мережі.
Для способу оплати кожен продавець-учасник має мати схвалену відповідну адресу призначення. Подальша зміна адреси продавця не переспрямовує наявні зобов’язання непомітно.
Для перевірки виплат потрібні ненульова кількість підтверджень і два незалежні сумісні провайдери, навіть якщо форма оплати дозволяє нуль підтверджень або один сканер.
Покрокове налаштування в панелі · Резервне копіювання та відновлення
4. Підключи спільний кошик
Створи облікові дані Marketplace з областю доступу проєкту в Налаштування → Доступ до API. Надай серверній частині форми оплати marketplace.read та invoices.write, за потреби обмеживши доступ її магазином. Не додавай до цього ключа дозволи схвалювати адреси й виплати.
Розрахуй ціни, знижки, податки й доставку на своєму сервері, а потім розподіли їх між частками продавців. Надсилай 1–100 різних продавців із додатними сумами у вигляді десяткових рядків; їхні валові суми мають точно дорівнювати сумі рахунку. Ніколи не довіряй розподілу, переданому браузером, і не використовуй арифметику з рухомою комою для грошей.
Заміни ім’я хоста API та заповнювачі UUID нижче. Завантажуй WHOLLY_TOKEN із середовища свого сервера. Запит успадковує налаштовану комісію 4%; йому не потрібен дозвіл перевизначати комісію.
Відкрити запит у cURL, JavaScript, PHP або Python
cURL
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/invoices" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: cart-1042-marketplace-v1' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"store_id": "YOUR_STORE_ID",
"amount": "300.00",
"currency": "USD",
"order_id": "cart-1042",
"description": "One order from three vendors",
"ipn_url": "https://shop.example.com/payments/wholly-ipn",
"metadata": {
"cart_id": "cart-1042"
},
"allocations": [
{
"vendor_id": "VENDOR_A_UUID",
"gross_amount": "100.00"
},
{
"vendor_id": "VENDOR_B_UUID",
"gross_amount": "100.00"
},
{
"vendor_id": "VENDOR_C_UUID",
"gross_amount": "100.00"
}
]
}'JavaScript
// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"store_id": "YOUR_STORE_ID",
"amount": "300.00",
"currency": "USD",
"order_id": "cart-1042",
"description": "One order from three vendors",
"ipn_url": "https://shop.example.com/payments/wholly-ipn",
"metadata": {
"cart_id": "cart-1042"
},
"allocations": [
{
"vendor_id": "VENDOR_A_UUID",
"gross_amount": "100.00"
},
{
"vendor_id": "VENDOR_B_UUID",
"gross_amount": "100.00"
},
{
"vendor_id": "VENDOR_C_UUID",
"gross_amount": "100.00"
}
]
}`;
const response = await fetch("https://api.example.com/v1/marketplace/invoices", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "cart-1042-marketplace-v1",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());PHP
<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"store_id": "YOUR_STORE_ID",
"amount": "300.00",
"currency": "USD",
"order_id": "cart-1042",
"description": "One order from three vendors",
"ipn_url": "https://shop.example.com/payments/wholly-ipn",
"metadata": {
"cart_id": "cart-1042"
},
"allocations": [
{
"vendor_id": "VENDOR_A_UUID",
"gross_amount": "100.00"
},
{
"vendor_id": "VENDOR_B_UUID",
"gross_amount": "100.00"
},
{
"vendor_id": "VENDOR_C_UUID",
"gross_amount": "100.00"
}
]
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/invoices");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: cart-1042-marketplace-v1", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));Python
# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "cart-1042-marketplace-v1",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"store_id": "YOUR_STORE_ID",
"amount": "300.00",
"currency": "USD",
"order_id": "cart-1042",
"description": "One order from three vendors",
"ipn_url": "https://shop.example.com/payments/wholly-ipn",
"metadata": {
"cart_id": "cart-1042"
},
"allocations": [
{
"vendor_id": "VENDOR_A_UUID",
"gross_amount": "100.00"
},
{
"vendor_id": "VENDOR_B_UUID",
"gross_amount": "100.00"
},
{
"vendor_id": "VENDOR_C_UUID",
"gross_amount": "100.00"
}
]
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/invoices",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Збережи тіло запиту та Idempotency-Key перед надсиланням. Після тайм-ауту повтори те саме тіло з тим самим ключем. Збережи відповідність data.invoice_id своєму замовленню та переспрямуй клієнта на links.checkout верхнього рівня. Ніколи не розміщуй API-ключі в браузері клієнта.
Де знайти UUID проєкту й магазину →
Віддаєш перевагу SDK? Почни з прикладів Marketplace:
5. Відстежуй оплату й виплату окремо
URL повернення з форми оплати не є доказом оплати. Перевіряй підписи callbacks над сирим тілом, перевіряй часову позначку й очікуваний проєкт та зберігай event_id з обмеженням унікальності перед підтвердженням отримання. Використовуй окремий захист на рівні замовлення, щоб повторні спроби не могли виконати замовлення двічі.
| Подія | Що це повідомляє |
|---|---|
invoice.settled | Рахунок клієнта остаточно оплачено. Перед виконанням замовлення перевір позначки перевірки й утримання Marketplace. |
marketplace.allocations.available | Перевірені частки доступні для виплати. Це ще не означає, що хоч одному продавцю вже виплатили кошти. |
marketplace.payout.confirmed | Виплата пройшла перевірки підтверджень. |
IPN рахунків використовує секрет IPN магазину. Webhooks життєвого циклу Marketplace використовують власний секрет підпису ендпойнта. Тримай ці два обробники окремо. Під час звіряння пропущених подій або подій, що надійшли не по порядку, отримуй поточний рахунок або виплату через API.
Callbacks рахунків і перевірка підпису · Події Marketplace та довідник API
6. Спочатку перевіряй, потім автоматизуй
Коли перевірені частки стануть доступними, віднови виплати, але залиш автоматичні політики вимкненими. У Marketplace → Виплати підготуй розподілені суми, перевір отримувачів і ліміти комісій у нативній монеті/gas, а потім один раз схвали точний план. Відстежуй його до стану «Виплачено», а не лише «Трансльовано».
Bitcoin об’єднує всі невиплачені частки вибраного рахунку в один пакет. Виплати токенів EVM можуть потребувати поповнення на gas перед переказами токенів. Це кілька транзакцій, а не атомарний розподіл за принципом «усе або нічого».
Після успішного малого тесту налаштуй по одній автоматичній політиці на актив: мінімальну виплату, ліміти основної суми на виплату й на день, бюджети комісій у нативній монеті/gas, інтервал і підтвердження. Увімкнення дозволяє надсилання без додаткових натискань. Низький баланс коштів на обробку або вимкнений дозвіл переказів на сервері все одно може призупинити витрачання.
7. Обробляй складні випадки
Недоплачені, пізні, змішані платежі або платежі, яких торкнулася реорганізація
Остаточна оплата у формі оплати не гарантує повного фінансування розподілу продавцям. Допуск не може створити відсутні кошти. Перевір утримані розподілені суми, дочекайся оплати, поверни кошти або явно схвали менший, повністю забезпечений розподіл. Надлишкові платежі не стають автоматично додатковим доходом маркетплейсу.
Переплати не блокують передбачені котируванням виплати продавцям у Bitcoin, монетах EVM або підтримуваних токенах ERC-20. Частки продавців не змінюються, а надлишок залишається окремим для звіряння.
Виплата зупинилася або запит завершився тайм-аутом
Перевір збережені хеші транзакцій, вихідні баланси, gas і причину перевірки. Віднови або звір наявну виплату; не створюй другий переказ через втрачену відповідь. Після відновлення резервної копії залиш виплати призупиненими, доки результати on-chain і журнал обліку не збігатимуться.
Відновлення може замінити невдалий переказ EVM лише після того, як два незалежні провайдери доведуть підтверджений revert on-chain. Суми операцій із невідомим результатом залишаються зарезервованими, а комісії невдалих спроб усе одно враховуються в початковому бюджеті.
Клієнту потрібне повернення коштів
Перевір адресу повернення, яку контролює клієнт. Підтримуються повні повернення в тому самому активі. Якщо всім продавцям уже виплачено кошти, окремо поповни основний гаманець; Wholly Crypto не може списати кошти з них назад. Частково виплачений пакет продавців потребує ручного звіряння, а не припущення, що є кнопка часткового повернення.
8. Перевір усе перед запуском
- Проведи малий платіж у BTC та/або EVM до підтверджених виплат продавцям. Перевір мережу, контракт токена, адреси призначення, комісію та окремі витрати на перекази.
- Протестуй повторний callback, тайм-аут відповіді API, недоплату й утриману виплату. Перевір, що жоден випадок не спричиняє повторного виконання замовлення або повторного надсилання коштів.
- Зберігай приватні резервні копії поза сервером, стеж за збоями виплат і регулярно звіряй зобов’язання перед продавцями з коштами on-chain.
Почни з виплат із перевіркою та невеликої кількості продавців. Автоматизуй лише перевірені активи, бюджети й адреси призначення.