Una integración de pagos que puedes probar, conciliar y automatizar sin confundir los cobros a clientes con los pagos a vendedores.
1. Decide quién se encarga de cada cosa
Tu tienda gestiona el catálogo, las cuentas de vendedores, el carrito, los envíos y los pedidos. Wholly Crypto se encarga del checkout, la verificación de pagos, las asignaciones y los pagos aprobados a vendedores. Un registro de vendedor no es una cuenta de acceso; los plugins de tienda existentes no reparten automáticamente carritos entre varios vendedores.
- Un carrito compartido
- Una factura para el cliente
- Partes verificadas para vendedores
- Pagos a vendedores aprobados
Los clientes pagan primero a las wallets del proyecto. Tú controlas las claves y guardas fondos que debes a los vendedores. No es un reparto directo del cliente a cada vendedor ni un servicio sin custodia para ellos.
Marketplace admite Bitcoin mainnet, las monedas EVM compatibles y tokens ERC-20 estándar. Los vendedores reciben el activo en la red que usó el cliente, sin conversión automática a dinero tradicional. Las 30 cadenas de recepción no están todas disponibles para pagos de Marketplace.
2. Calcula los importes
Tres vendedores venden productos por 100 dólares cada uno. Configura una comisión del proyecto del 4%, sin ajustes por tienda o vendedor en este ejemplo.
| Parte | Bruto | Tu comisión | Recibe el vendedor |
|---|---|---|---|
| Cada vendedor | $100 | $4 | $96 |
| Los tres | $300 | $12 | $288 |
Son equivalentes al tipo de cambio fijado en la factura, pagados en cripto. No garantizan un valor futuro en dólares. La tarifa habitual del 1% consume 3 dólares de crédito prepago en esta factura de 300 dólares, una vez, no por cada vendedor. Cuando se aplica esa tarifa, tu comisión bruta de 12 dólares deja 9 antes de los costes de red.
Reserva monedas nativas libres aparte para las comisiones de Bitcoin o el gas EVM. Las comisiones no deben consumir el principal protegido de los vendedores. Los sweeps normales no pueden gastar desde las direcciones de recepción de Marketplace.
3. Prepara wallets y vendedores
- En Project → Marketplace → Settings, activa Marketplace, selecciona las tiendas y fija la comisión. Mantén pausados los pagos a vendedores y desactivadas las reglas automáticas mientras lo configuras.
- Haz una copia de las wallets del proyecto y de la base de datos. Activa los métodos BTC/EVM deseados en la tienda, comprueba los escáneres y añade crédito de procesamiento y fondos nativos separados para las comisiones.
- Añade cada vendedor. Guarda su UUID junto al ID de vendedor de tu tienda; external_id puede guardar esa referencia. Verifica de forma independiente cada dirección de pago y apruébala para la cadena y red exactas.
Cada método del checkout necesita un destino aprobado y compatible para todos los vendedores participantes. Cambiar después la dirección de un vendedor no redirige silenciosamente las obligaciones existentes.
La verificación de pagos a vendedores exige confirmaciones positivas y dos proveedores compatibles independientes, aunque el checkout permita cero confirmaciones o un solo escáner.
Guía de configuración en la consola · Copias de seguridad y restauración
4. Conecta el carrito compartido
Crea una credencial de Marketplace limitada al proyecto en Settings → API access. Da a tu backend de checkout marketplace.read e invoices.write, con restricción a su tienda cuando corresponda. No incluyas permisos para aprobar direcciones ni pagos a vendedores en esa clave.
Calcula precios, descuentos, impuestos y envío en tu servidor y asígnalos a las partes de los vendedores. Envía entre 1 y 100 vendedores distintos con importes positivos en cadenas decimales; sus importes brutos deben sumar exactamente el total de la factura. No confíes en un reparto enviado por el navegador ni uses coma flotante para dinero.
Sustituye el hostname de la API y los UUID de ejemplo. Carga WHOLLY_TOKEN desde el entorno de tu servidor. La petición hereda la comisión configurada del 4%; no necesita permiso para cambiarla.
Abre la petición en cURL, JavaScript, PHP o 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))Guarda el cuerpo de la petición y el Idempotency-Key antes de enviarla. Tras un timeout, repite el mismo cuerpo con la misma clave. Guarda data.invoice_id junto a tu pedido y redirige a links.checkout del nivel superior de la respuesta. Nunca pongas claves API en el navegador del cliente.
Dónde encontrar los UUID del proyecto y la tienda →
¿Prefieres un SDK? Empieza con los ejemplos de Marketplace:
5. Separa el cobro del pago a vendedores
Volver del checkout no demuestra que se haya pagado. Verifica las firmas del callback sobre el cuerpo original, comprueba la fecha y el proyecto esperado y guarda event_id como único antes de confirmar la recepción. Usa además un control por pedido para que los reintentos no lo procesen dos veces.
| Evento | Qué te indica |
|---|---|
invoice.settled | La factura del cliente se liquidó. Comprueba los avisos de revisión y las retenciones de Marketplace antes de procesar el pedido. |
marketplace.allocations.available | Las partes verificadas están disponibles para pagar. Eso no significa que algún vendedor ya haya cobrado. |
marketplace.payout.confirmed | El pago a vendedores completó sus comprobaciones de confirmación. |
El IPN de facturas usa el secreto IPN de la tienda. Los webhooks de Marketplace usan su propio secreto de firma por endpoint. Mantén separados ambos receptores. Consulta el estado actual de la factura o del pago por API al conciliar eventos perdidos o desordenados.
Callbacks de facturas y verificación de firmas · Eventos de Marketplace y referencia API
6. Revisa primero, automatiza después
Cuando las partes verificadas estén disponibles, quita la pausa de pagos pero deja desactivadas las reglas automáticas. En Marketplace → Payouts, prepara las asignaciones, revisa destinatarios y límites de comisiones nativas y gas, y aprueba el plan exacto una vez. Síguelo hasta Paid, no solo Broadcast.
Bitcoin agrupa todas las partes pendientes de una factura seleccionada. Los tokens EVM pueden necesitar financiación de gas antes del envío. Son varias transacciones, no un reparto atómico de todo o nada.
Tras una prueba pequeña correcta, configura una regla automática por activo: pago mínimo, límites por pago y diarios del principal, presupuestos de comisiones nativas y gas, intervalo y confirmaciones. Activarla autoriza envíos sin más clics. Poco crédito o una barrera de transferencias desactivada en el servidor pueden seguir pausando los gastos.
7. Resuelve los casos complicados
Pagos insuficientes, tardíos, mezclados o afectados por una reorganización
Un checkout liquidado no garantiza un reparto totalmente cubierto. La tolerancia no crea fondos que faltan. Revisa las asignaciones retenidas, espera el pago, devuelve los fondos o aprueba expresamente un reparto menor totalmente respaldado. Los excesos no se convierten automáticamente en ingresos extra del marketplace.
Pagar de más no bloquea los pagos cotizados a vendedores en Bitcoin, monedas EVM ni tokens ERC-20 compatibles. Las partes de los vendedores no cambian y el exceso queda separado para conciliarlo.
Un pago se atasca o una petición agota el tiempo
Comprueba los hashes guardados, los saldos de origen, el gas y el motivo de revisión. Reanuda o concilia el pago existente; no crees otra transferencia porque se perdió la respuesta. Tras restaurar una copia, mantén los pagos pausados hasta que la cadena y el libro contable coincidan.
Reanudar solo puede reemplazar una transferencia EVM fallida cuando dos proveedores independientes prueban una reversión confirmada en la cadena. Los resultados desconocidos mantienen los fondos reservados y las comisiones de intentos fallidos siguen contando para el presupuesto original.
El cliente necesita un reembolso
Verifica una dirección de reembolso controlada por el cliente. Se admiten reembolsos completos en el mismo activo. Si todos los vendedores ya cobraron, financia la wallet principal aparte; Wholly Crypto no puede recuperar esos fondos de ellos. Un lote parcialmente pagado necesita conciliación manual, no un supuesto botón de reembolso parcial.
8. Comprueba todo antes de abrir
- Prueba un pago pequeño de BTC y/o EVM hasta confirmar los pagos a vendedores. Comprueba cadena, contrato del token, destinos, comisión y costes separados.
- Prueba un callback repetido, una respuesta API con timeout, un pago insuficiente y un pago retenido. Verifica que ninguno provoque pedidos procesados o transferencias duplicados.
- Guarda copias privadas fuera del servidor, vigila los fallos de pago y concilia regularmente las obligaciones con vendedores frente a los fondos en cadena.
Empieza revisando los pagos y con pocos vendedores. Automatiza solo los activos, presupuestos y destinos que hayas probado.