Listar Ofertas P2P
Obtener el listado paginado de ofertas P2P disponibles con filtros avanzados.
GET /p2p
Retorna el listado de ofertas P2P disponibles, con soporte para filtros por tipo, moneda, monto, ratio, estado y paginación.
Autenticación
Bearer Token (Authorization: Bearer <token>) o Credenciales de App (app-id + app-secret en headers).
Cuando se usa Credenciales de App, la consulta se realiza en nombre del usuario propietario de la app. Dicho usuario debe cumplir los requisitos de acceso a P2P (p2p_enabled, KYC verificado, Telegram vinculado y teléfono verificado).
Request
Con Bearer Token:
curl -X GET "https://api.qvapay.com/p2p?take=20&page=1&type=buy&coin=BANK_CUP" \
-H "Authorization: Bearer <token>"Con Credenciales de App:
curl -X GET "https://api.qvapay.com/p2p?take=20&page=1&type=buy&coin=BANK_CUP" \
-H "app-id: {tu-app-uuid}" \
-H "app-secret: {tu-app-secret}"Parámetros (query string)
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
page | number | No | Página a consultar (por defecto 1) |
take | number | No | Resultados por página (por defecto 20, máximo 100) |
type | string | No | Filtrar por tipo: buy o sell (alias: offer_type) |
coin | string | No | Filtrar por tick de la moneda (ej: BANK_CUP) |
orderBy | string | No | Campo de ordenamiento: updated_at (por defecto), created_at, amount, receive, ratio, best_rate, rating, trades |
orderType | string | No | Orden: asc o desc (por defecto desc; alias: order). Con orderBy=best_rate, desc es "mejor tasa primero" |
my | boolean | No | true o 1 muestra solo las ofertas del usuario autenticado (como dueño, como peer, o las ofertas por rango de las que tomó una parte) |
peer | string | No | Filtrar por usuario: acepta UUID o username (con o sin @) |
sortByStatus | boolean | No | Solo con my=true: agrupa por estado (revisión → procesando → pagado → abierta → completada). Por defecto true |
min | number | No | Monto mínimo (alias: min_price) |
max | number | No | Monto máximo (alias: max_price) |
ratio_min | number | No | Ratio mínimo (receive / amount) (alias: min_ratio) |
ratio_max | number | No | Ratio máximo (receive / amount) (alias: max_ratio) |
offer_kind | string | No | Filtrar por tipo de oferta: fixed (se toma entera) o flexible (por rango, se toma por partes). Sin filtro llegan las dos (alias: kind) |
flexible_first | boolean | No | true o 1 pone las ofertas por rango antes que las fijas, manteniendo el orden pedido dentro de cada grupo. Solo en el listado del mercado y sin offer_kind. Por defecto false; la web de QvaPay lo usa siempre |
only_vip | boolean | No | true o 1 muestra solo ofertas marcadas como exclusivas para usuarios VIP |
vip_maker | boolean | No | true o 1 muestra solo ofertas publicadas por cuentas VIP |
search | string | No | Búsqueda general (detalles, mensaje, uuid, montos). Solo funciona junto a my=true |
status | string | No | Filtrar por estado: open, revision, processing, paid, completed, cancelled. Solo aplica junto a my=true (el listado público siempre es open) |
Los ordenamientos ratio (tasa receive / amount), best_rate, rating (valoración promedio del ofertante) y trades (operaciones P2P completadas del ofertante) son rankings globales sobre el listado público; en my=true caen a updated_at (salvo ratio, que sí se aplica sobre tus ofertas).
ratio vs best_rate
ratio es la tasa cruda receive / amount (moneda por USD) y no es lo mismo que "mejor tasa": su dirección se invierte según el lado del mercado, porque el ratio se mide siempre desde la oferta, no desde quien la toma.
| Tipo de oferta | Quien la toma… | El ratio es… | Mejor para quien la toma |
|---|---|---|---|
sell | compra QUSD pagando la moneda | lo que paga por dólar | ratio más bajo |
buy | vende QUSD y recibe la moneda | lo que recibe por dólar | ratio más alto |
Por eso orderBy=ratio&orderType=desc sobre un listado type=sell devuelve las ofertas más caras primero. Usa orderBy=best_rate, que normaliza el signo según el tipo: orderType=desc es siempre "mejor primero" y asc "peor primero", en ambos lados.
best_rate requiere type y coin (responde 400 si faltan): sin tipo el signo no está definido, y los ratios de monedas distintas no son comparables entre sí (1000 CUP/USD vs 1.2 MLC/USD ordenaría por moneda, no por tasa). Tampoco aplica con my=true, donde la perspectiva es la del dueño de la oferta.
# Mejores ofertas para comprar CUP por transferencia bancaria
curl -X GET "https://api.qvapay.com/p2p?type=sell&coin=BANK_CUP&orderBy=best_rate&orderType=desc" \
-H "Authorization: Bearer {token}"Response
Paginación estilo Laravel. amount, receive y los demás campos decimales se serializan como strings.
{
"current_page": 1,
"data": [
{
"uuid": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"type": "buy",
"coin": "BANK_CUP",
"amount": "50",
"receive": "12500",
"payment_type_id": 1,
"message": "Pago rápido",
"private": false,
"only_vip": false,
"status": "open",
"created_at": "2024-06-20T14:30:00.000Z",
"updated_at": "2024-06-20T14:30:00.000Z",
"offer_kind": "fixed",
"available_amount": "50",
"reserved_amount": "0",
"order_min": null,
"order_max": null,
"User": {
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"username": "johndoe",
"name": "John",
"image": "https://example.com/avatar.png",
"kyc": true,
"vip": false,
"golden_check": false,
"phone_verified": true,
"telegram_verified": true,
"rating_avg": 4.85,
"rating_count": 12,
"_count": {
"P2P": 34,
"P2P_Peer": 18
}
},
"Coin": {
"name": "Transferencia CUP",
"tick": "BANK_CUP",
"logo": "https://example.com/cup.png",
"price": "1"
}
}
],
"per_page": 20,
"total": 143
}Notas:
requirementstrae los requisitos del creador para aplicar (min_completed,min_rating,min_account_days,min_trustscore), onull. Para saber si tu cuenta los cumple, pide el detalle (requirements_met).offer_kindindica el tipo de cada oferta. En lasflexiblelo que se puede tomar esavailable_amount(noamount, que es el total original), en órdenes de entreorder_minyorder_max. Ver Ofertas por rango.- Las ofertas por rango aparecen para todas las cuentas, identificadas por
offer_kind. _count.P2Py_count.P2P_Peerson las operaciones P2P completadas del usuario como dueño y como peer, respectivamente.- En ofertas con peer asignado (visible con
my=true) se incluye un objetoPeercon la misma estructura queUser. - Con
my=true, una oferta por rango de la que tomaste una parte llega conmy_order(uuid,amount,receive,status,created_at): tu orden activa o, si no hay, la más reciente. En esas filas el filtrostatusse evalúa sobre tu orden (my_order.status), no sobre la oferta, cuyostatussigueopenmientras otros toman. Las órdenes canceladas no salen salvo constatus=cancelled. - Con
my=truesinstatusexplícito, la respuesta incluye"sorted_by_status": truey las ofertas llegan agrupadas por estado.
Límites y frescura
25 consultas por minuto por cuenta. Con credenciales de app el cupo es de la cuenta dueña, así que toda tu integración comparte un único bucket.
El listado del mercado —el que no lleva my ni peer— se cachea unos segundos en nuestros servidores. Es la misma respuesta para todo el que pide los mismos filtros, así que pedirla más rápido que la caché no devuelve datos más frescos: solo gasta cupo. Una consulta cada 2 o 3 segundos aprovecha el límite sin desperdiciarlo.
La cabecera X-Cache te dice si esa respuesta salió de la caché (HIT) o de la base de datos (MISS).
Si recibes un 429, el mensaje trae el límite vigente. Reintenta con backoff exponencial; no sirve de nada insistir antes.
Si tu integración necesita enterarse de cambios al instante, hacer polling no es el camino: ya existe el feed del mercado, que te avisa cuando de verdad pasa algo. Puedes recibirlo por webhook firmado o por stream, y convierte miles de consultas diarias en un aviso por evento.
Errores
| Código | Descripción |
|---|---|
400 | Parámetro inválido (page, take, orderBy, orderType, type, status o filtros numéricos), orderBy=best_rate sin type/coin, o el usuario no cumple los requisitos de acceso a P2P |
401 | Token de autenticación inválido o ausente |
429 | Demasiadas solicitudes (límite: 30 cada 60 segundos por usuario) |