Listar y Crear Retiros
Listar los retiros del usuario o crear un nuevo retiro.
GET /withdraw
Retorna la lista paginada de retiros del usuario autenticado, ordenados del más reciente al más antiguo.
Autenticación
Bearer Token (Authorization: Bearer {token}).
Query parameters
| Parámetro | Tipo | Default | Descripción |
|---|---|---|---|
page | number | 1 | Número de página (≥ 1) |
take | number | 20 | Resultados por página (1 - 30) |
status | string | — | Filtra por estado: pending, processing, paid, cancelled |
payment_method | string | — | Filtra por tick de la moneda (ej: BTC, USDT) |
include_total | boolean | false | Si es true, incluye total y total_pages en meta |
Request
curl -X GET "https://api.qvapay.com/withdraw?page=1&take=20&status=paid&include_total=true" \
-H "Authorization: Bearer {tu-token}"Response
{
"result": "OK",
"data": [
{
"id": "12345",
"amount": 50.00,
"receive": 48.50,
"payment_method": "USDT",
"status": "paid",
"details": {
"Wallet": "TXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"tx_id": "abc123def456...",
"evidence_url": "https://tronscan.org/#/transaction/abc123def456...",
"transaction_uuid": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"created_at": "2026-05-20T14:30:00.000Z",
"updated_at": "2026-05-20T15:00:00.000Z"
}
],
"meta": {
"page": 1,
"take": 20,
"count": 1,
"total": 1,
"total_pages": 1
}
}Campos de respuesta
| Campo | Tipo | Descripción |
|---|---|---|
result | string | Estado de la operación |
data[].id | string | ID del retiro (BigInt serializado como string) |
data[].amount | number | Monto solicitado en USD |
data[].receive | number | Monto a recibir después de fees |
data[].payment_method | string | Tick de la moneda del retiro |
data[].status | string | pending, processing, paid o cancelled |
data[].details | object | Detalles del retiro ya parseados (wallet, etc.) |
data[].tx_id | string | null | ID de transacción on-chain (si aplica) |
data[].evidence_url | string | null | URL del explorador o evidencia (si aplica) |
data[].transaction_uuid | string | null | UUID de la transacción QvaPay asociada |
data[].created_at | string | Fecha de creación (ISO 8601) |
data[].updated_at | string | Fecha de última actualización (ISO 8601) |
meta.page | number | Página actual |
meta.take | number | Tamaño de página solicitado |
meta.count | number | Resultados devueltos en esta página |
meta.total | number | Total de retiros que cumplen el filtro (solo con include_total=true) |
meta.total_pages | number | Total de páginas (solo con include_total=true) |
Errores
| Código | Descripción |
|---|---|
400 | Parámetros inválidos (take fuera de rango o status no permitido) |
401 | Token ausente o inválido |
POST /withdraw
Crea un nuevo retiro de fondos. El retiro se procesa según el método de pago seleccionado.
Autenticación
Bearer Token (Authorization: Bearer {token}) o Credenciales de App (app-id + app-secret en headers).
Cuando se usa Bearer Token, el parámetro pin es obligatorio. Con credenciales de App, el PIN no es requerido.
Request
{
"amount": 50.00,
"pay_method": "USDT",
"details": {
"Wallet": "TXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"pin": "1234",
"note": "Retiro semanal",
"webhook": "https://mitienda.com/webhook/withdraw"
}Parámetros
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
amount | number | Si | Monto a retirar (1 - 100,000 USD) |
pay_method | string | Si | Tick de la moneda (ej: USDT, USDCASH, BANK_CUP), tal como viene en GET /coins/v2?enabled_out=1 |
details | object | Si | Formulario del método de pago: las claves son exactamente los name del working_data de la moneda. Ver Ejemplos por método (cripto, USD CASH, BANK_CUP, Zelle, BANK) |
pin | string | Solo Bearer | PIN de 4 dígitos o código OTP de 6 dígitos |
note | string | No | Nota descriptiva del retiro |
webhook | string | No | URL válida para recibir notificaciones del estado del retiro |
idempotency_key | string | No | Clave de idempotencia (8-64 caracteres, [A-Za-z0-9._-]) para reintentos seguros en conexiones inestables. Repetir la misma clave en 24h devuelve el retiro original con duplicate: true en vez de debitar de nuevo |
compliance | object | No* | Atestación de cumplimiento (OFAC / CACR). Ver Cumplimiento. *Obligatoria en retiros cripto y a Cuba desde el navegador web, y en todos los canales si la cuenta es US person; en los demás casos es opcional (recomendada) |
dest_label | string | No | Declaración del titular de la cuenta destino: OWN_ACCOUNT (cuenta propia) o THIRD_PARTY (un tercero). Si no se envía y compliance trae W-1, se registra OWN_ACCOUNT |
Response
{
"result": "OK",
"data": {
"withdraw_id": 12345,
"transaction_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"receive_amount": 48.50,
"receive_amount_coin": 48.50,
"fee_to_apply": 1.50,
"amount": 50.00,
"coin": "USDT"
}
}Campos de respuesta
| Campo | Tipo | Descripción |
|---|---|---|
withdraw_id | number | ID del retiro creado |
transaction_id | string | UUID de la transacción asociada |
receive_amount | number | Monto a recibir en USD después de fees |
receive_amount_coin | number | Monto a recibir en la moneda seleccionada |
fee_to_apply | number | Comisión aplicada al retiro |
amount | number | Monto original solicitado |
coin | string | Tick de la moneda del retiro |
Idempotencia
Si se envía idempotency_key y un retiro ya fue creado con esa misma clave, la respuesta es 200 con los datos del retiro original y el campo adicional "duplicate": true (no se debita saldo de nuevo). Si el retiro original aún está en proceso, se responde 409 con "code": "DUPLICATE_REQUEST" — reintenta unos segundos después.
Cumplimiento (compliance)
QvaPay registra, junto a cada retiro, una autocertificación del cliente exigida por la normativa OFAC (31 CFR Part 515) y por el programa BSA/AML de la plataforma. El objeto compliance viaja en el mismo body del retiro (en envíos multipart, dentro del JSON de payload).
{
"amount": 58.41,
"pay_method": "USDTBSC",
"details": { "Wallet": "0x7dba714e606Ac161f0FDe1A056F3Fd12Db3848D3" },
"pin": "1234",
"compliance": {
"purposeCode": "W-1",
"attestations": {
"notProhibitedOfficial": true,
"notProhibitedPartyMember": true,
"notRestrictedList": true,
"lawfulPurpose": true
},
"purposeNote": "Billetera personal",
"language": "es"
}
}Campos de compliance
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
purposeCode | string | Sí | Código de propósito (ver tabla). Debe ser válido para el tipo de retiro: W-1 en salidas cripto, P-1…P-4 en destinos con valor a Cuba |
attestations | object | Sí | Las cuatro casillas deben ser true: notProhibitedOfficial, notProhibitedPartyMember, notRestrictedList, lawfulPurpose. Si falta alguna, el payload se ignora íntegro (no se registra a medias) |
purposeNote | string | No | Nota libre del cliente sobre el propósito (máx. 200 caracteres) |
language | string | No | Idioma en que el cliente leyó el texto de la atestación (es por defecto) |
specificLicense | string | No | Número de licencia específica de OFAC, si la operación se ampara en una |
generalLicense | string | No | Legado. Cita 31 CFR (515.570, 515.542) que enviaban las apps antiguas en lugar de purposeCode. Se sigue aceptando en destinos Cuba; no aplica a salidas cripto |
Códigos de propósito
| Código | Aplica a | Significado |
|---|---|---|
W-1 | Retiros cripto (on-chain) | La dirección de destino es una billetera de autocustodia bajo control del titular. No es un tercero |
P-1 | Destinos con valor a Cuba | Remesa familiar |
P-2 | Destinos con valor a Cuba | Pago a un emprendedor o negocio del sector privado cubano |
P-3 | Destinos con valor a Cuba | Donativo, sin contraprestación |
P-4 | Destinos con valor a Cuba | Recarga o servicios de telecomunicaciones |
Con un purposeCode que no corresponde al tipo de retiro (p. ej. P-1 en un retiro cripto) el payload se descarta y el retiro se trata como enviado sin atestación.
Cuándo es obligatoria
La exigencia se despliega por canal para no romper integraciones existentes:
| Canal | Cómo se identifica | Retiro cripto sin W-1 | Retiro con destino Cuba sin P-1…P-4 |
|---|---|---|---|
| Navegador web (app.qvapay.com) | Sesión con cookie | 400 W1_ATTESTATION_REQUIRED | 400 CACR_ATTESTATION_REQUIRED |
| Apps móviles oficiales | Header x-qvapay-client-platform: ios | android | Aceptado (se registra la ausencia) | Aceptado (se registra la ausencia) |
| Apps de comercio | Headers app-id + app-secret | Aceptado (se registra la ausencia) | Aceptado (se registra la ausencia) |
| Clientes API con Bearer propio | Authorization: Bearer sin cookie de sesión | Aceptado (se registra la ausencia) | Aceptado (se registra la ausencia) |
Excepción, cuentas US person (desde el 29 sep 2026): una cuenta US person (se declaró persona estadounidense, verificó con un documento emitido en EE. UU. o sus territorios, o tiene SSN/ITIN en su ficha) no tiene la exención de la tabla: un retiro cripto sin W-1 o un retiro con destino Cuba sin P-1…P-4 responde 400 (W1_ATTESTATION_REQUIRED / CACR_ATTESTATION_REQUIRED) desde cualquier canal.
Para las demás cuentas, en apps y clientes API la atestación es opcional hoy y recomendada: QvaPay anunciará con antelación cuando pase a ser obligatoria para todos los canales. Envíala desde ya si tu integración muestra al usuario el texto de la atestación. La atestación nunca se asume por defecto: si no llega, no se registra certificado alguno.
Otros controles regulatorios en el retiro
| Control | Umbral | Efecto |
|---|---|---|
| Verificación de identidad (KYC) | Cualquier monto | Una cuenta sin KYC aprobado recibe 403 con code: "KYC_REQUIRED" y kyc_url |
| Travel Rule (31 CFR § 1010.410) | Retiros de $3,000 o más, en cualquier rail | El titular debe tener nombre y dirección de calle en su ficha (Configuración → Datos de tu cuenta); si no, 400 TRAVEL_RULE_SENDER_ADDRESS. En rails fiat, details debe incluir además nombre y dirección de calle del beneficiario; si no, 400 TRAVEL_RULE_RECIPIENT_ADDRESS |
| Factura en retiros bancarios | BANK / BANK_INTL / BANK_EUR de $3,000 o más | Envío multipart/form-data con el JSON del retiro en el campo payload y un PDF (máx. 10 MB) en invoice |
Número de ruta ABA en BANK | Cualquier monto | BANK es solo bancos de EE. UU.: Nro de Ruta o ACH de 9 dígitos con dígito de control válido, si no 400 BANK_ROUTING_INVALID. Se exige hoy al asistente web; apps y clientes API quedan registrados hasta que sea obligatorio para todos. Ver Número de ruta (BANK) |
Entrega de efectivo por zona
Las monedas de efectivo (p. ej. USDCASH) pueden tener un tarifario por municipio. Cuando lo tienen, details debe llevar la provincia y el municipio exactamente como los devuelve GET /withdraw/cash-zones, y el cargo de la zona (tarifa de entrega + margen de servicio) se suma al fee de la moneda.
El efectivo se entrega en billetes: en toda moneda de efectivo (con o sin tarifario) lo que recibe el beneficiario (receive_amount en USD para USDCASH, en CUP para CUPCASH) tiene que ser un entero múltiplo del paso de su unidad: 5 USD/EUR, 20 CUP (billetes). La comprobación es sobre lo entregado, no sobre amount: calcula el bruto a partir de lo que quieres entregar (p. ej. entregar $250 a Santa Clara son $260.41 de amount). Si no cumple, la respuesta es 400 CASH_AMOUNT_STEP con below y above (los dos importes válidos más cercanos, en la moneda del efectivo) y, desde el 2 de noviembre de 2026, amount_for_below y amount_for_above: el amount exacto en USD que deja cada uno con la tarifa vigente (null cuando esa opción no existe, p. ej. below 0). Si tu app no calcula el bruto con el tarifario por zonas, usa esos dos valores tal cual para ofrecer el ajuste; el error ya viene redactado en bruto («pon 79.80 para recibir 70 o 84.85 para recibir 75 USD»).
El ejemplo completo de un retiro USDCASH (los seis campos de details, cómo elegir la zona y cuánto se entrega) está en Ejemplos por método.
GET /withdraw/cash-zones?coin=USDCASH
Público, sin autenticación. Una lista zones vacía significa que la moneda no funciona por zonas. El mismo tarifario viene en el campo cash_zones del coin (GET /coins, GET /coins/v2, GET /coins/:id).
{
"coin": "USDCASH",
"margin_pct": 1,
"zones": [
{
"id": "12",
"province_code": "CMG",
"province_name": "Camagüey",
"municipality": "Camagüey",
"is_capital": true,
"provider_pct": 3,
"margin_pct": null,
"total_pct": 4,
"min_usd": 50,
"small_flat_usd": 4,
"eta_min_hours": 24,
"eta_max_hours": 48,
"home_delivery": true,
"route": "p2p",
"enabled": true
}
]
}| Campo | Descripción |
|---|---|
provider_pct | Tarifa de entrega en % del monto |
margin_pct | Margen de servicio en %; null = usa el margin_pct global de la respuesta |
total_pct | Recargo total en % (provider_pct + margen efectivo), cuando la tarifa fija no es mayor |
small_flat_usd | Tarifa de entrega fija cuando es mayor que el % (montos pequeños); null = no aplica |
min_usd | Mínimo a entregar (monto neto a recibir) en ese municipio |
eta_min_hours / eta_max_hours | Plazo estimado de entrega |
home_delivery | false = sin entrega a domicilio (el beneficiario recoge el efectivo) |
Cargo de la zona sobre el amount del retiro: max(amount × provider_pct / 100, small_flat_usd) + amount × margin_pct / 100. Una vez asignada, una entrega solo puede cancelarse pasados 6 días.
Notas
-
Validación de wallet: Las direcciones de wallet para criptomonedas son validadas antes de crear el retiro.
-
Número de tarjeta (BANK_CUP, BANK_MLC, CLASICA, TROPICAL): el campo
Nro de tarjetadedetailstiene que ser un string con exactamente 16 dígitos. Los espacios, guiones y puntos se aceptan y se quitan ("9227 0699 …"se guarda como"92270699…"). Envíalo como string, nunca como número JSON: un número de 16 dígitos pierde precisión al parsearse. Además, la tarjeta tiene que ser del tipo que paga ese método:Método Tarjetas que acepta BANK_CUPTarjetas RED en CUP ( 9200,9204,9205,9212,9224,9227,9238…)BANK_MLCTarjetas RED en MLC ( 9225,9226,9228,9229,9235,9245)TROPICAL ( pay_method: "TROPICAL"o"BANDECPREPAGO")Tropical, prepago en USD ( 9240,9250,9256)CLASICAClásica ( 9760) y TropicalEl banco son los dígitos 5–8 (
0699BANDEC,1299BPA,9598Metropolitano) y el último dígito es un dígito de control Luhn. Si no se cumple, la respuesta es400INVALID_CARD_NUMBERcon unreason(ver la tabla de errores más abajo). Una tarjeta RED de un banco conocido cuyo producto aún no está en la lista se acepta. El catálogo completo de prefijos está en qvapay/cuban_cards_prefixes. La misma regla aplica al guardar un método de pago de esos rails. -
Rate limit: 1 solicitud cada 5 segundos por usuario.
Errores
| Código | Descripción |
|---|---|
400 | Datos inválidos (monto fuera de rango, wallet incorrecta, balance insuficiente, PIN incorrecto) |
400 | W1_ATTESTATION_REQUIRED: retiro cripto sin atestación W-1 desde el navegador, o desde cualquier canal si la cuenta es US person (ver Cumplimiento) |
400 | CACR_ATTESTATION_REQUIRED: retiro con destino Cuba sin autocertificación CACR desde el navegador, o desde cualquier canal si la cuenta es US person (purposeCode P-1…P-4 + las cuatro declaraciones; ver Cumplimiento) |
400 | TRAVEL_RULE_SENDER_ADDRESS / TRAVEL_RULE_RECIPIENT_ADDRESS / TRAVEL_RULE_UNAVAILABLE: retiro de $3,000 o más sin los datos que exige la Travel Rule |
400 | CASH_ZONE_UNAVAILABLE: retiro de efectivo a una provincia/municipio sin cobertura (ver Entrega de efectivo por zona) |
400 | CASH_BELOW_MIN: el monto a entregar está por debajo del mínimo de ese municipio |
400 | CASH_AMOUNT_STEP: en efectivo (USDCASH, CUPCASH…) lo que se entrega no es un entero múltiplo del paso de su unidad (5 USD/EUR, 20 CUP). La respuesta trae step, unit, los dos importes válidos más cercanos (below, above) en la moneda del efectivo y el amount en USD que deja cada uno (amount_for_below, amount_for_above, null si no existe) |
400 | INVALID_CARD_NUMBER: en BANK_CUP, BANK_MLC, CLASICA o TROPICAL, Nro de tarjeta no es válido. field indica la clave y reason el motivo: format (falta o no tiene 16 dígitos), checksum (dígito de control incorrecto, casi siempre un dígito mal tecleado), account_number (es un número de cuenta, no de tarjeta), not_cuban (no es una tarjeta cubana) o wrong_method (es una tarjeta de otro método; expected_method dice cuál) |
403 | KYC_REQUIRED: la cuenta no tiene verificación de identidad aprobada (kyc_url indica dónde completarla) |
403 | La app no tiene habilitado allowed_withdraw, o la sesión es demasiado reciente (espera 5 minutos tras iniciar sesión) |
409 | DUPLICATE_REQUEST: ya hay un retiro en proceso con la misma idempotency_key |
429 | Límite de solicitudes excedido (máximo 1 cada 5 segundos) |
500 | Error interno del servidor |
Estado de una transacción
Consulta ligera del estado de una transacción propia y de su retiro o compra asociados, pensada para refrescar una pantalla mientras la operación está pendiente.
Ejemplos por Método
Cómo armar el objeto details de POST /withdraw para cripto, USD CASH, BANK_CUP, Zelle, transferencia bancaria en EE. UU. (BANK) e internacional (BANK_INTL), y de dónde sacar cada dato.