A payment integration you can test, reconcile and automate without mixing up customer payments and vendor payouts.
1. Decide who handles what
Your shop owns the catalog, seller accounts, cart, shipping and order fulfilment. Wholly Crypto handles checkout, payment verification, vendor allocations and approved payouts. A vendor record is not a seller login; existing shop plugins do not automatically split multi-vendor carts.
- One shared cart
- One customer invoice
- Verified vendor shares
- Approved payouts
Customers pay the project wallets first. You control the keys and hold funds owed to vendors. This is not a direct customer-to-vendor split or a non-custodial service for your sellers.
Marketplace supports Bitcoin mainnet and supported EVM coins and standard ERC-20 tokens. Vendors receive the asset and network used by the customer, not an automatic fiat conversion. All 30 receiving chains are not supported for Marketplace payouts.
2. Work out the amounts
Three vendors each sell $100 of goods. Set the project commission to 4%, with no store or vendor overrides for this example.
| Share | Gross | Your commission | Vendor receives |
|---|---|---|---|
| Each vendor | $100 | $4 | $96 |
| All three | $300 | $12 | $288 |
These are equivalents at the invoice’s locked quote, paid in crypto. They are not guaranteed future dollar values. The usual 1% processing fee uses $3 of prepaid credits on this $300 invoice, once, not once per vendor. Your $12 gross commission leaves $9 before network costs when that fee applies.
Keep free native coins for Bitcoin fees or EVM gas separately. Fees must not consume the vendors’ protected principal. Ordinary sweeps cannot spend Marketplace receiving addresses.
3. Prepare wallets and vendors
- In Project → Marketplace → Settings, enable Marketplace, select the stores and set the commission. Keep payouts paused and automatic policies off during setup.
- Back up project wallets and the database. Enable the intended BTC/EVM methods in the store, check scanner readiness and fund processing credits plus separate native fees.
- Add each vendor. Save its UUID against your shop’s seller ID; external_id can hold that seller reference. Independently verify and approve each payout address on the exact chain and network.
A checkout method needs an approved matching destination for every participating vendor. Changing a vendor’s address later does not silently redirect existing obligations.
Payout verification needs positive confirmations and two independent compatible providers, even if checkout allows zero confirmations or one scanner.
4. Connect the shared cart
Create a project-scoped Marketplace credential in Settings → API access. Give your checkout backend marketplace.read and invoices.write, restricted to its store where appropriate. Keep address approval and payout approval permissions out of that key.
Calculate prices, discounts, tax and shipping on your server, then assign them to the vendor shares. Send 1–100 distinct vendors with positive decimal strings; their gross amounts must exactly total the invoice. Never trust a browser-supplied split or use floating-point arithmetic for money.
Replace the API hostname and UUID placeholders below. Load WHOLLY_TOKEN from your server environment. The request inherits the configured 4% commission; it does not need permission to override commission.
Open the request in cURL, JavaScript, PHP or 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))Persist the request body and Idempotency-Key before sending. After a timeout, retry the same body with the same key. Save data.invoice_id against your order and redirect to the top-level links.checkout. Never put API keys in the customer’s browser.
Where to find project and store UUIDs →
Prefer an SDK? Start with the Marketplace examples:
5. Track payment and payout separately
A checkout return URL is not proof of payment. Verify callback signatures over the raw body, check the timestamp and expected project, and store event_id uniquely before acknowledging. Use a separate order-level guard so retries cannot fulfil an order twice.
| Event | What it tells you |
|---|---|
invoice.settled | The customer invoice settled. Check review flags and Marketplace holds before fulfilment. |
marketplace.allocations.available | Verified shares are available for payout. No vendor has necessarily been paid yet. |
marketplace.payout.confirmed | The payout completed its confirmation checks. |
Invoice IPN uses the store’s IPN secret. Marketplace lifecycle webhooks use their own endpoint signing secret. Keep both receivers distinct. Read the current invoice or payout through the API when reconciling missed or out-of-order events.
Invoice callbacks and signature verification · Marketplace events and API reference
6. Review first, automate later
Once verified shares are available, unpause payouts but leave automatic policies off. In Marketplace → Payouts, prepare the allocations, review recipients and native fee/gas caps, then approve the exact plan once. Follow it to Paid, not just Broadcast.
Bitcoin batches every unpaid share of a selected invoice together. EVM token payouts may need gas funding before token transfers. They are several transactions, not an atomic all-or-nothing split.
After a successful small test, set one automatic policy per asset: minimum payout, per-payout and daily principal limits, native fee/gas budgets, interval and confirmations. Enabling it authorizes sending without more clicks. Low credits or a disabled server transfer gate can still pause spending.
7. Handle the awkward cases
Underpaid, late, mixed or reorged payments
A settled checkout does not guarantee a fully funded vendor split. Tolerance cannot create missing funds. Inspect held allocations, wait for payment, refund or explicitly approve a smaller fully backed split. Excess payments are not automatically extra marketplace revenue.
A payout stalls or a request times out
Check its saved transaction hashes, source balances, gas and review reason. Resume or reconcile the existing payout; do not create a second transfer because the response was lost. After restoring a backup, keep payouts paused until on-chain results and the ledger agree.
The customer needs a refund
Verify a customer-controlled refund address. Supported refunds are full and in the same asset. After all vendors were paid, fund the primary wallet separately; Wholly Crypto cannot debit them back. A partly paid vendor batch needs manual reconciliation, not an assumed partial-refund button.
8. Check before opening
- Run a small BTC and/or EVM payment through to confirmed vendor payouts. Confirm the chain, token contract, destinations, commission and separate fees.
- Test a repeated callback, a timed-out API response, an underpayment and a held payout. Verify that none causes duplicate fulfilment or duplicate sending.
- Keep private backups off the server, monitor payout failures and reconcile vendor obligations against on-chain funds regularly.
Start with reviewed payouts and a small number of vendors. Automate only the assets, budgets and destinations you have tested.