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": "tron_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: tron_usdt, btc, sol) |
details | object | Si | Detalles del retiro (dirección de wallet, etc.) |
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 para el navegador web en retiros cripto y a Cuba; opcional (recomendada) para apps móviles, apps de comercio y clientes API |
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": "tron_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 |
|---|---|---|
| Navegador web (app.qvapay.com) | Sesión con cookie | 400 W1_ATTESTATION_REQUIRED |
| Apps móviles oficiales | Header x-qvapay-client-platform: ios | android | Aceptado (se registra la ausencia) |
| Apps de comercio | Headers app-id + app-secret | Aceptado (se registra la ausencia) |
| Clientes API con Bearer propio | Authorization: Bearer sin cookie de sesión | Aceptado (se registra la ausencia) |
Para 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_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 |
Notas
- Validación de wallet: Las direcciones de wallet para criptomonedas son validadas antes de crear el retiro.
- 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 desde el navegador sin atestación W-1 (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 |
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 |