Ejemplos por Método
Cómo armar el objeto details de POST /withdraw para cripto, USD CASH, BANK_CUP, Zelle y transferencia bancaria, y de dónde sacar cada dato.
details es el formulario del método de pago: los datos que QvaPay necesita para entregar el dinero (una wallet, una tarjeta, la dirección donde se entrega el efectivo…). Cada moneda tiene su propio formulario, descrito en su campo working_data, y details tiene que coincidir con él clave por clave.
Esta página explica las reglas, de dónde sale cada dato y trae un ejemplo completo por método. El resto de parámetros de POST /withdraw (pin, webhook, idempotency_key, compliance…) está en Listar y Crear Retiros.
Reglas de details
- Las claves son exactamente los
namedelworking_datade la moneda, con sus mayúsculas, tildes y espacios."Nro de Celular"(USDCASH) y"Nro de celular"(BANK_CUP) son claves distintas, igual que"Dirección"(USDCASH) y"Direccion"(BANK). - Van todas. Cada campo del
working_datatiene que llegar con un valor no vacío. - Ninguna clave extra. Una clave que no está en el
working_data(zone,walleten minúscula,phone…) rechaza el retiro entero. - Todos los valores son string, también el carné, el teléfono y el número de tarjeta. En PHP, Python o JS no los mandes como número.
pay_methodes eltickde la moneda (USDT,USDTBSC,USDCASH,BANK_CUP…), no su nombre.
De dónde sacar cada dato
| Dato | Fuente |
|---|---|
pay_method | Campo tick del coin en GET /coins/v2?enabled_out=1 (solo monedas con retiro habilitado) |
Claves de details | working_data[].name del mismo coin |
| Tipo de cada campo | working_data[].type: text, phone (solo dígitos, espacios, guiones, paréntesis y + inicial), email, number o select |
Valores de un select | working_data[].options[].value, salvo la provincia del efectivo por zonas (ver USD CASH) |
| Provincia y municipio del efectivo | Campo cash_zones del coin, o GET /withdraw/cash-zones?coin=USDCASH |
| Datos del beneficiario (nombre, carné, teléfono, dirección, tarjeta, wallet) | Tu propio formulario o tu sistema: es lo que tu cliente te dice sobre quién recibe |
| Métodos guardados del usuario | GET /user/payment-methods?coin=… (solo Bearer): su details ya trae las claves del formulario, pero comprueba que estén todas antes de reenviarlo; hay métodos viejos incompletos |
| Monto mínimo | min_out del coin; en efectivo, además, min_usd de la zona sobre lo que se entrega |
Lee el working_data en tiempo de ejecución en lugar de copiarlo a tu código: los formularios se editan desde el panel de QvaPay y pueden cambiar. GET /coins/v2 es público y se puede cachear unos minutos (la propia respuesta trae Cache-Control: max-age=300).
const coins = await fetch('https://api.qvapay.com/coins/v2?enabled_out=1').then(r => r.json())
const coin = coins.find(c => c.tick === 'USDCASH')
const keys = coin.working_data.map(field => field.name)
// ["Nombre y Apellidos", "Carnet de Identidad", "Nro de Celular", "Dirección", "Municipio", "Provincia"]Formularios de los métodos más usados
Los formularios vigentes el 20 de octubre de 2026, como referencia. La fuente de verdad sigue siendo el working_data de cada coin.
pay_method | Claves de details | Mínimo (min_out) |
|---|---|---|
USDT, USDTBSC, USDCSOL y demás cripto | Wallet | Según la red (USDT TRC20: $2) |
BTCLN | Wallet (factura BOLT11 o Lightning Address) | $2 |
USDCASH | Nombre y Apellidos, Carnet de Identidad, Nro de Celular, Dirección, Municipio, Provincia | $50 |
BANK_CUP, BANK_MLC, CLASICA, BANDECPREPAGO | Nombre y Apellidos, Nro de tarjeta, Nro de celular | $1 |
ZELLE | Nombre y Apellidos, Correo o Celular | $50 |
BANK (transferencia en USD) | Nombre y Apellidos, Nombre del Banco, Nro de Cuenta, Nro de Ruta o ACH, Nro de telefono, Direccion | $100 |
En todos los ejemplos de abajo la autenticación es con credenciales de App (app-id + app-secret), que no piden pin. Con Bearer Token añade "pin" al body.
Cripto (USDT TRC20, USDT BSC, USDC Solana…)
El formulario es un único campo, Wallet. La dirección se valida contra la red de la moneda: una dirección TRON (T…) en USDTBSC, o una 0x… en USDT (TRC20), responde 400.
curl -X POST https://api.qvapay.com/withdraw \
-H "Content-Type: application/json" \
-H "app-id: {tu-app-id}" \
-H "app-secret: {tu-app-secret}" \
-d '{
"pay_method": "USDT",
"amount": 100,
"details": {
"Wallet": "TXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"note": "Pedido 4521",
"webhook": "https://tu-dominio.com/webhooks/qvapay/withdraw",
"idempotency_key": "pedido-4521",
"compliance": {
"purposeCode": "W-1",
"attestations": {
"notProhibitedOfficial": true,
"notProhibitedPartyMember": true,
"notRestrictedList": true,
"lawfulPurpose": true
},
"purposeNote": "Billetera personal",
"language": "es"
}
}'W-1 declara que la wallet es de autocustodia del propio titular de la cuenta. Mándalo solo cuando sea cierto. Si pagas a la wallet de un tercero (por ejemplo, un cliente tuyo), no envíes W-1 y declara "dest_label": "THIRD_PARTY". Ver Cumplimiento.
USD CASH (efectivo en Cuba)
USD CASH entrega dólares en efectivo en casi todo el país, con un precio y un plazo que dependen del municipio. El formulario tiene seis campos:
| Clave | Qué va | Ejemplo |
|---|---|---|
Nombre y Apellidos | Nombre completo de quien recibe el efectivo | Yanelis Pérez Rodríguez |
Carnet de Identidad | Carné de identidad de quien recibe (11 dígitos) | 85010112345 |
Nro de Celular | Teléfono de quien recibe, para coordinar la entrega. C mayúscula | +5355512345 |
Dirección | Dirección de entrega: calle, número, entre calles, reparto | Calle Martí #123 e/ Céspedes y Maceo, Reparto Centro |
Provincia | province_name (o province_code) de una zona del tarifario | Villa Clara |
Municipio | municipality de esa misma zona | Santa Clara |
No hay clave zone. QvaPay identifica la zona a partir de Provincia + Municipio. Tampoco uses las options del campo Provincia del working_data (La Habana / Santa Clara): vienen del formulario antiguo. Con el efectivo por zonas, la lista válida es la del tarifario (16 provincias y 131 municipios hoy).
1. Cargar el tarifario y elegir la zona
GET /withdraw/cash-zones?coin=USDCASH es público. El mismo contenido viene en el campo cash_zones del coin en GET /coins/v2. QvaPay compara sin tildes ni mayúsculas, pero lo más seguro es enviar los nombres tal como vienen en el tarifario.
const norm = (s) => String(s ?? '')
.normalize('NFD').replace(/[̀-ͯ]/g, '')
.toLowerCase().replace(/\s+/g, ' ').trim()
const { margin_pct, zones } = await fetch('https://api.qvapay.com/withdraw/cash-zones?coin=USDCASH').then(r => r.json())
// Provincia y municipio de la dirección de tu cliente
function findZone(province, municipality) {
return zones.find(z =>
(norm(z.province_name) === norm(province) || norm(z.province_code) === norm(province)) &&
norm(z.municipality) === norm(municipality)
) ?? null
}
const zone = findZone('Villa Clara', 'Santa Clara')
if (!zone) throw new Error('No hay entrega de efectivo en ese municipio')Si guardas las direcciones de tus clientes con un campo libre, conviene que tu formulario ofrezca primero la provincia y después el municipio a partir de este tarifario, en vez de texto libre. Así cada dirección cae en una zona válida.
2. Calcular cuánto se entrega
amount es lo que se debita del saldo (en USD, fees incluidos). Lo que recibe el beneficiario es amount menos el fee de la moneda y el cargo de la zona:
- Fee de la moneda:
fee_out/fee_out_fixeddel coin (ver Cómo funcionafee_out_fixed). - Cargo de la zona:
max(amount × provider_pct / 100, small_flat_usd) + amount × margen / 100, donde el margen es elmargin_pctde la zona o, si esnull, elmargin_pctglobal de la respuesta.
const zoneMargin = zone.margin_pct ?? margin_pct
const zoneCharge = Math.max(amount * zone.provider_pct / 100, zone.small_flat_usd ?? 0) + amount * zoneMargin / 100Ejemplo con las tarifas del 20 de octubre de 2026: retirar $200 a Santa Clara cuesta $0 de fee de la moneda (por encima de $99.99 el fee_out de USDCASH es 0 %) más max(6, 4) + 2 = $8 de zona, y se entregan $192. Lo entregado tiene que ser al menos el min_usd de la zona ($20 en Santa Clara). El cálculo de tu lado sirve para mostrarlo antes de enviar; el valor que manda es el de la respuesta (fee_to_apply y receive_amount).
3. Crear el retiro
curl -X POST https://api.qvapay.com/withdraw \
-H "Content-Type: application/json" \
-H "app-id: {tu-app-id}" \
-H "app-secret: {tu-app-secret}" \
-d '{
"pay_method": "USDCASH",
"amount": 200,
"details": {
"Nombre y Apellidos": "Yanelis Pérez Rodríguez",
"Carnet de Identidad": "85010112345",
"Nro de Celular": "+5355512345",
"Dirección": "Calle Martí #123 e/ Céspedes y Maceo, Reparto Centro",
"Municipio": "Santa Clara",
"Provincia": "Villa Clara"
},
"note": "Pedido 4521",
"webhook": "https://tu-dominio.com/webhooks/qvapay/withdraw",
"idempotency_key": "pedido-4521",
"compliance": {
"purposeCode": "P-1",
"attestations": {
"notProhibitedOfficial": true,
"notProhibitedPartyMember": true,
"notRestrictedList": true,
"lawfulPurpose": true
},
"purposeNote": "Ayuda familiar",
"language": "es"
}
}'El mismo body en PHP (Symfony HttpClient):
$payload = [
'pay_method' => 'USDCASH',
'amount' => 200,
'details' => [
'Nombre y Apellidos' => 'Yanelis Pérez Rodríguez',
'Carnet de Identidad' => '85010112345', // string, no int
'Nro de Celular' => '+5355512345', // C mayúscula
'Dirección' => 'Calle Martí #123 e/ Céspedes y Maceo, Reparto Centro',
'Municipio' => 'Santa Clara', // municipality del tarifario
'Provincia' => 'Villa Clara', // province_name del tarifario
],
'note' => 'Pedido 4521',
'webhook' => 'https://tu-dominio.com/webhooks/qvapay/withdraw',
'idempotency_key' => 'pedido-4521',
'compliance' => [
'purposeCode' => 'P-1',
'attestations' => [
'notProhibitedOfficial' => true,
'notProhibitedPartyMember' => true,
'notRestrictedList' => true,
'lawfulPurpose' => true,
],
'purposeNote' => 'Ayuda familiar',
'language' => 'es',
],
];
$response = $client->request('POST', 'https://api.qvapay.com/withdraw', [
'headers' => ['app-id' => $appId, 'app-secret' => $appSecret],
'json' => $payload,
]);Respuesta:
{
"result": "OK",
"data": {
"withdraw_id": 123456,
"transaction_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"receive_amount": 192,
"receive_amount_coin": 192,
"fee_to_apply": 8,
"amount": 200,
"coin": "USDCASH"
}
}El plazo de entrega de cada zona está en eta_min_hours / eta_max_hours. El estado del retiro llega al webhook y se puede consultar con GET /withdraw/:id.
Error frecuente: reutilizar el formulario de BANK_CUP para USD CASH. En BANK_CUP la clave es Nro de celular (c minúscula) y no hay carné, dirección, provincia ni municipio. En USD CASH, una clave de más (zone, Nro de celular) o una de menos rechaza el retiro.
USD CASH es entrega de valor en Cuba: el purposeCode va de P-1 a P-4 (remesa familiar, sector privado, donativo o telecomunicaciones), nunca W-1.
BANK_CUP (tarjeta en CUP), BANK_MLC, CLASICA y BANDECPREPAGO
Los cuatro métodos de tarjeta cubana comparten formulario:
| Clave | Qué va | Ejemplo |
|---|---|---|
Nombre y Apellidos | Titular de la tarjeta | Yanelis Pérez Rodríguez |
Nro de tarjeta | 16 dígitos, como string. Los espacios y guiones se quitan | 9227 0699 1234 5678 |
Nro de celular | Celular del titular de la tarjeta. c minúscula | +5355512345 |
La tarjeta tiene que ser del tipo que paga ese método (una tarjeta MLC no entra por BANK_CUP, una Clásica va por CLASICA). La tabla de prefijos y los motivos de rechazo de INVALID_CARD_NUMBER están en las notas de POST /withdraw.
curl -X POST https://api.qvapay.com/withdraw \
-H "Content-Type: application/json" \
-H "app-id: {tu-app-id}" \
-H "app-secret: {tu-app-secret}" \
-d '{
"pay_method": "BANK_CUP",
"amount": 50,
"details": {
"Nombre y Apellidos": "Yanelis Pérez Rodríguez",
"Nro de tarjeta": "9227 0699 1234 5678",
"Nro de celular": "+5355512345"
},
"note": "Pedido 4522",
"idempotency_key": "pedido-4522",
"compliance": {
"purposeCode": "P-1",
"attestations": {
"notProhibitedOfficial": true,
"notProhibitedPartyMember": true,
"notRestrictedList": true,
"lawfulPurpose": true
},
"language": "es"
}
}'El número de tarjeta del ejemplo pasa la validación de formato pero no existe; usa siempre el de tu beneficiario.
Estos formularios no tienen campo de dirección. Un retiro de $3,000 o más exige la dirección de calle del beneficiario (Travel Rule), así que por estos métodos responde 400 TRAVEL_RULE_RECIPIENT_ADDRESS.
Zelle
{
"pay_method": "ZELLE",
"amount": 100,
"details": {
"Nombre y Apellidos": "John Smith",
"Correo o Celular": "[email protected]"
},
"idempotency_key": "pedido-4523"
}Correo o Celular es el correo o el teléfono registrado en Zelle por quien recibe.
Transferencia bancaria en USD (BANK)
{
"pay_method": "BANK",
"amount": 500,
"details": {
"Nombre y Apellidos": "Acme Supplies LLC",
"Nombre del Banco": "Bank of America",
"Nro de Cuenta": "000123456789",
"Nro de Ruta o ACH": "026009593",
"Nro de telefono": "+13055551234",
"Direccion": "100 Main St, Miami, FL 33101"
},
"idempotency_key": "pedido-4524"
}Direccion va sin tilde en este formulario. A partir de $3,000 el envío es multipart/form-data con el JSON en payload y la factura en PDF en invoice (ver Otros controles regulatorios).
Errores frecuentes con details
| Respuesta | Causa | Qué revisar |
|---|---|---|
400 Método de pago inválido | pay_method no es el tick de una moneda con retiro habilitado | Usa el tick de GET /coins/v2?enabled_out=1 (USDT, no tron_usdt) |
400 Los datos de retiro contienen campos no válidos | Hay una clave que no está en el working_data | Claves extra (zone, wallet), o mal escritas (Nro de celular en USDCASH, Dirección en BANK) |
400 Los datos de retiro deben estar completos | Falta un campo del formulario, o va vacío | Que estén todas las claves del working_data, con valor |
400 El campo … debe ser un teléfono válido | Un campo phone con letras u otros símbolos | Solo dígitos, espacios, guiones, paréntesis y + inicial |
400 El valor "…" no está disponible para … | Un select con un valor fuera de sus options | El valor exacto de options[].value |
400 CASH_ZONE_UNAVAILABLE | Provincia + Municipio no forman una zona del tarifario | province_name y municipality de la misma zona |
400 CASH_BELOW_MIN | Lo que se entrega queda por debajo del min_usd de la zona | Sube amount |
400 INVALID_CARD_NUMBER | Tarjeta mal escrita o de otro método | reason y expected_method de la respuesta |
400 TRAVEL_RULE_RECIPIENT_ADDRESS | Retiro de $3,000 o más sin dirección de calle del beneficiario | El método necesita un campo de dirección con calle y número |