API directa (aliados)
Para aliados con contrato. Si eres un comercio, no necesitas esto: usa enlaces de pago, donde QuickPay pide la clave del banco por ti. La API directa es para quien muestra su propia pantalla de pago (por ejemplo, un bot que pide la clave dentro de su conversación) o envía pagos.
Una sola integración para cobrar y pagar sobre todos los rieles bancarios del país. Tú dices qué quieres (cobrar/pagar Bs X a Y); QuickPay decide cómo y por cuál banco.
En esta guía vas a, con curl puro y copy-paste:
- Conseguir tu API key y autenticarte.
- Hacer tu primer cobro (débito inmediato con OTP).
- Enviar un pago a un beneficiario.
- Consultar el estado de una operación.
- Recibir webhooks de cambios de estado.
- Manejar errores y rechazos.
Convención. A lo largo de la guía usamos la variable
{{base_url}}como host del API. Sustitúyela por la URL que te entregamos (producción:https://pay.pagaconquickpay.com). Define una variable de entorno para copiar-pegar sin editar:bashexport base_url="https://pay.pagaconquickpay.com"
1. Obtén tu API key
Toda llamada se autentica con la cabecera x-switch-key en formato:
x-switch-key: <comercio>:<api_key><comercio>es el identificador de tu comercio (p. ej.acme).<api_key>es la llave secreta. Tienes dos tipos:
| Llave | Prefijo | Qué hace |
|---|---|---|
| Prueba | sk_test_… | Ambiente de ensayo. No mueve dinero real (sandbox simulado en QA). |
| Producción | sk_live_… | Mueve dinero real sobre los bancos. |
El modo lo fija la llave, no tu código. No envías ningún campo
modenitest=true: el servidor deriva el modo (test/live) del prefijo de la API key. La misma petición, consk_test_…corre simulada y consk_live_…mueve dinero. Cambias de ambiente cambiando una sola cosa: la llave.
Guarda tu llave en una variable para el resto de la guía. Empieza siempre en test:
export SWITCH_KEY="acme:sk_test_xxxxxxxxxxxxxxxxxxxxxxxx"Verifica que tu llave funciona pidiendo tus transacciones (no mueve dinero):
curl -s "$base_url/switch/v1/transacciones?limit=1" \
-H "x-switch-key: $SWITCH_KEY"Respuesta esperada (HTTP 200): un arreglo con tus transacciones ([] si todavía no tienes ninguna).
Si recibes 401 {"error":"unauthorized"}, revisa el formato <comercio>:<api_key> y que la llave esté activa.
Conceptos que vas a usar en todas las llamadas
idempotency_key(UUID). Obligatorio en cada operación de dinero. Reintentar con la misma llave nunca duplica el cobro/pago: te devuelve el resultado original. Genera uno por operación: ``bash uuidgen | tr 'A-Z' 'a-z' # o cualquier generador de UUID v4``Instrumento. Origen o destino del dinero. Se expresa igual sea cuenta o teléfono: ``jsonc { "tipo": "cuenta", // "cuenta" | "telefono" "valor": "01020000...", // nº cuenta (20 díg) o teléfono "banco": "0102", // código BCV de 4 dígitos "identificacion": "V12345678", // cédula/RIF con prefijo (V/E/J/P…) "nombre": "Ana Pérez" // ≤ 40 caracteres }``- Montos en VES.
{ "valor": 320.00, "moneda": "VES" }(la moneda es ISO-4217 y por defectoVES). - El estado se lee del cuerpo, no del HTTP. Un
200 OKsignifica que el banco respondió; el campoestado(ACEPTADA/PENDIENTE/RECHAZADA/…) te dice qué pasó con el dinero.
2. Tu primer cobro (débito inmediato / C2P)
cobrar debita al pagador (le cobras a una persona). Es un débito inmediato y suele requerir un OTP que el banco del pagador le envía. El flujo son 2 pasos:
POST /cobrar/token → (el pagador recibe un OTP de su banco, ~120s de validez)
POST /cobrar → (envías ese OTP y se ejecuta el cobro)Paso 2.1 — Solicitar el OTP
POST /switch/v1/cobrar/token con el mismo cuerpo del cobro sin el otp. cobrador eres tú (recibes el dinero); pagador es quien paga.
IDEM=$(uuidgen | tr 'A-Z' 'a-z')
curl -s -X POST "$base_url/switch/v1/cobrar/token" \
-H "x-switch-key: $SWITCH_KEY" \
-H "content-type: application/json" \
-d "{
\"idempotency_key\": \"$IDEM\",
\"monto\": { \"valor\": 320.00, \"moneda\": \"VES\" },
\"concepto\": \"Matricula 2026-1\",
\"canal\": \"boton_pago\",
\"cobrador\": {
\"tipo\": \"cuenta\", \"valor\": \"01020000011234567890\", \"banco\": \"0102\",
\"identificacion\": \"J123456789\", \"nombre\": \"ACME C.A.\"
},
\"pagador\": {
\"tipo\": \"telefono\", \"valor\": \"04125551234\", \"banco\": \"0105\",
\"identificacion\": \"V12345678\", \"nombre\": \"Ana Perez\"
}
}"Respuesta (Resultado):
{
"id": "qp_txn_018f3a...",
"estado": "ACEPTADA",
"referencia_banco": "12345678",
"endtoend": null,
"motivo_rechazo": null,
"intentos": 1
}ACEPTADA aquí significa "OTP solicitado": el pagador ya recibió el código en su teléfono. El token vive ~120 segundos.
Si tu cuenta tiene tope de lo que recibe (por día o en total) y este cobro lo pasaría, no se pide el OTP: responde 409 con { "error": "tope_diario_excedido", "motivo": "…", "tope": 50000, "usado": 49500 } (o tope_acumulado_excedido). tope y usado van siempre en Bs. Si tu tope está en dólares, se agregan "topes_moneda": "USD", "tope_usd" (el tope tal como está fijado) y "tasa_bcv" (los Bs por dólar con que se convirtió); tope es tope_usd × tasa_bcv. Sólo frena aquí: un /cobrar con un OTP ya pedido no se frena por el tope. → Límites
En modo test el OTP es simulado; usa
12345678en el paso siguiente.
Paso 2.2 — Ejecutar el cobro con el OTP
Toma el OTP que te da el pagador y llama a POST /switch/v1/cobrar. Es el mismo cuerpo del paso anterior más el campo otp. Reutiliza el mismo idempotency_key del flujo de cobro:
curl -s -X POST "$base_url/switch/v1/cobrar" \
-H "x-switch-key: $SWITCH_KEY" \
-H "content-type: application/json" \
-d "{
\"idempotency_key\": \"$IDEM\",
\"monto\": { \"valor\": 320.00, \"moneda\": \"VES\" },
\"concepto\": \"Matricula 2026-1\",
\"canal\": \"boton_pago\",
\"otp\": \"12345678\",
\"cobrador\": {
\"tipo\": \"cuenta\", \"valor\": \"01020000011234567890\", \"banco\": \"0102\",
\"identificacion\": \"J123456789\", \"nombre\": \"ACME C.A.\"
},
\"pagador\": {
\"tipo\": \"telefono\", \"valor\": \"04125551234\", \"banco\": \"0105\",
\"identificacion\": \"V12345678\", \"nombre\": \"Ana Perez\"
}
}"Respuesta:
{
"id": "qp_txn_018f3a...",
"estado": "LIQUIDADA",
"referencia_banco": "98765432",
"endtoend": "0000001234567890",
"motivo_rechazo": null,
"intentos": 1
}- Si
estadoesLIQUIDADA, el banco confirmó el cobro: el dinero entró. - Si es
ACEPTADA, el banco recibió el débito ("enviada a BCV") pero todavía no lo confirma: no entregues todavía. Consulta (sección 4) o espera el webhooktransaccion.liquidada. - Si es
RECHAZADA, miramotivo_rechazo(sección 6). El error típico aquí esOTP_INVALIDO(código vencido o mal escrito → pide un token nuevo en 2.1).
Atajo: si ya capturaste el OTP por tu cuenta (p. ej. el banco del pagador lo entrega in-band), puedes llamar directo a
/cobrarcon elotp, sin el paso 2.1.
3. Envía un pago (payout)
pagar envía dinero desde tu tesorería a un beneficiario. Aquí ordenante eres tú (de dónde sale el dinero) y beneficiario es quien recibe.
Quién puede usarlo:
pagardebita una cuenta de QuickPay, no un saldo tuyo en QuickPay. Por eso sólo lo usan las integraciones que QuickPay habilita. Con una llave de comercio (sk_testosk_live) sin esa habilitación responde403con{ "error": "pagar_no_habilitado" }, y lo mismoPOST /switch/v1/dispersiones.
La pieza clave es via, que elige el riel de envío:
via | Riel | Cuándo | Destino | Resultado |
|---|---|---|---|---|
credito_inmediato (default) | Crédito Inmediato (CCE) | Predominante | teléfono o cuenta | Liquida en BCV de forma asíncrona → ACEPTADA, luego LIQUIDADA por consulta/webhook |
pago_movil | Pago entre teléfonos (P2P) | Alterno (p. ej. reintegros) | solo teléfono | Instantáneo → LIQUIDADA |
Reglas de oro:
- A cuenta siempre es
credito_inmediato(aunque envíesvia: "pago_movil", a cuenta va por CCE). - A teléfono es
credito_inmediatopor defecto; usapago_movilsolo si quieres el riel P2P instantáneo.
IDEM=$(uuidgen | tr 'A-Z' 'a-z')
curl -s -X POST "$base_url/switch/v1/pagar" \
-H "x-switch-key: $SWITCH_KEY" \
-H "content-type: application/json" \
-d "{
\"idempotency_key\": \"$IDEM\",
\"monto\": { \"valor\": 250.00, \"moneda\": \"VES\" },
\"concepto\": \"Reintegro\",
\"proposito\": \"transferencia\",
\"via\": \"credito_inmediato\",
\"canal\": \"billetera_digital\",
\"ordenante\": {
\"tipo\": \"cuenta\", \"valor\": \"01020000011234567890\", \"banco\": \"0102\",
\"identificacion\": \"J123456789\", \"nombre\": \"ACME C.A.\"
},
\"beneficiario\": {
\"tipo\": \"telefono\", \"valor\": \"04145556677\", \"banco\": \"0134\",
\"identificacion\": \"V87654321\", \"nombre\": \"Maria Gomez\"
}
}"Respuesta:
{
"id": "qp_txn_018f4b...",
"estado": "ACEPTADA",
"referencia_banco": "55512345",
"endtoend": "0000009876543210",
"motivo_rechazo": null,
"intentos": 1
}Por credito_inmediato lo normal es recibir ACEPTADA (enviada a BCV) y que confirme a LIQUIDADA poco después. Para enterarte de ese cambio: consulta (sección 4) o suscríbete a webhooks (sección 5).
Campos opcionales de
pagar:proposito(transferencia|nomina|proveedor, defaulttransferencia) yvia(defaultcredito_inmediato). Para un pago entre teléfonos (P2P) usa"via": "pago_movil".
4. Consulta el estado (ACEPTADA → LIQUIDADA)
La liquidación interbancaria es asíncrona. Una operación puede quedar PENDIENTE/ACEPTADA y confirmarse después. Usa POST /switch/v1/consultar con el id que te devolvió cobrar/pagar:
curl -s -X POST "$base_url/switch/v1/consultar" \
-H "x-switch-key: $SWITCH_KEY" \
-H "content-type: application/json" \
-d '{ "id": "qp_txn_018f4b..." }'Respuesta (estado actualizado):
{
"id": "qp_txn_018f4b...",
"estado": "LIQUIDADA",
"referencia_banco": "55512345",
"endtoend": "0000009876543210",
"motivo_rechazo": null,
"intentos": 1
}Ciclo de vida de los estados
| Estado | Significado | ¿Final? |
|---|---|---|
ACEPTADA | El banco recibió y aprobó (síncrono o "enviada a BCV") | No (espera liquidación) |
PENDIENTE | Aceptada, en liquidación interbancaria; consulta o espera webhook | No |
LIQUIDADA | Confirmada y liquidada | Sí (OK) |
RECHAZADA | Rechazo de negocio (ver motivo_rechazo) | Sí (fallo) |
ERROR | Fallo técnico interno (reintentable) | No |
DESCONOCIDA | Sin respuesta / timeout post-envío | No |
El camino feliz típico es ACEPTADA → LIQUIDADA.
Regla de seguridad. Ante
DESCONOCIDA, consulta antes de reintegrar: el banco pudo haber movido el dinero. Nunca reenvíes la operación original a ciegas (por eso existeidempotency_key: reintentar la misma operación es seguro y no duplica).
No necesitas hacer polling agresivo: configura un webhook (siguiente sección) y QuickPay te avisa cuando el estado cambie.
5. Webhooks salientes
Las operaciones de la API directa también avisan por webhook (transaccion.aprobada, transaccion.rechazada, transaccion.liquidada). transaccion.aprobada llega cuando el banco confirma la operación (LIQUIDADA), junto con transaccion.liquidada; un cobro ACEPTADA (enviado a BCV, sin confirmar) no avisa nada todavía. Cómo registrarlo, verificar la firma v2 y manejar los reintentos: → Webhooks.
6. Manejo de errores y rechazos
Distingue dos planos:
6.1 Errores de protocolo (código HTTP)
| HTTP | Significado | Qué hacer |
|---|---|---|
400 | Validación del cuerpo. Devuelve { "error": "VALIDATION", "detail": {...} } con los campos inválidos | Corrige el payload |
401 | x-switch-key ausente o inválida | Revisa formato <comercio>:<api_key> y que la llave esté activa |
404 | id no encontrado (en /consultar), o la operación es de otro modo que tu llave: una llave de prueba no ve operaciones reales y una real no ve las de prueba | Verifica el id, el comercio y el modo de la llave |
200 | El banco respondió | Lee estado del cuerpo para saber qué pasó con el dinero |
6.2 Rechazos de negocio (motivo_rechazo)
Cuando estado es RECHAZADA, el campo motivo_rechazo te dice por qué, con un enum normalizado igual para todos los bancos.
{
"id": "qp_txn_018f5c...",
"estado": "RECHAZADA",
"referencia_banco": null,
"motivo_rechazo": "SALDO_INSUFICIENTE",
"intentos": 1
}Valores de motivo_rechazo y cómo reaccionar:
motivo_rechazo | Qué pasó | Acción sugerida |
|---|---|---|
SALDO_INSUFICIENTE | El pagador/ordenante no tiene fondos | Avisa y reintenta luego |
OTP_INVALIDO | OTP vencido o incorrecto | Pide token nuevo (/cobrar/token) y reintenta |
CUENTA_NO_EXISTE | Cuenta destino/origen inexistente | Corrige el instrumento |
CUENTA_BLOQUEADA | Cuenta bloqueada | No reintentable; contacta al titular |
CUENTA_NO_DEBITO / CUENTA_NO_CREDITO | La cuenta no admite débito/crédito | Usa otro instrumento |
MONTO_INVALIDO | Monto fuera de rango/formato | Corrige el monto |
EXCEDE_LIMITE | Supera un tope (banco o riesgo) | Reduce monto o revisa límites |
BENEFICIARIO_NO_COINCIDE | Nombre/ID no coincide con la cuenta | Verifica datos del destino |
NO_AFILIADO_SERVICIO | No afiliado al servicio de pago | El titular debe afiliarse |
BANCO_NO_DISPONIBLE | Banco caído o no procesó | Reintenta más tarde (QuickPay puede reenrutar en pagar) |
DUPLICADA | Operación ya procesada | Consulta el estado de la original |
FUERA_DE_HORARIO | Fuera de ventana operativa | Reintenta en horario |
CANCELADA_POR_PAGADOR | El pagador canceló | No reintentable |
PENDIENTE_BCV | En liquidación BCV | Consulta más tarde |
RECHAZO_TECNICO | Fallo técnico del banco | Reintentable con backoff |
FIRMA_AUTH | Credencial/config del banco | No reintentable; reportar (no reenrutar dinero) |
NO_SOPORTADO | Operación no soportada (p. ej. reverso) | Maneja por flujo alterno |
BLOQUEO_RIESGO | Bloqueado por el motor de riesgo/fraude | Revisa listas/velocidad; no reintentar a ciegas |
SIN_DEBITO_EN_EXTRACTO | Sólo en pagar: el banco no respondió al envío y, horas después, el débito no aparece en el estado de cuenta de QuickPay | No reenvíes con otra idempotency_key sin revisarlo antes con QuickPay |
DESCONOCIDO | Sin clasificar | Consulta el estado y, si sigue sin aclararse, escríbenos con el id |
Reintentos seguros. Para reintentar una operación, usa el mismo
idempotency_key: si la original sí se ejecutó, recibes su resultado en vez de un cobro/pago duplicado. AnteDESCONOCIDAo un timeout, consulta primero — nunca reenvíes con una llave nueva.
Recapitulando
- Una cabecera:
x-switch-key: <comercio>:<api_key>. El modo lo fija la llave (sk_test_…vssk_live_…). - Cobrar = débito inmediato en 2 pasos:
/cobrar/token→ OTP →/cobrar. - Pagar = payout;
viaelige el riel (credito_inmediatopor defecto;pago_movilP2P por teléfono). - Estados asíncronos:
ACEPTADA→LIQUIDADA. Confírmalos con/consultaro webhooks. idempotency_key(UUID) en cada operación: reintentar nunca duplica.- Rechazos normalizados en
motivo_rechazo, iguales para todos los bancos.
¿Listo para producción? Cambia tu sk_test_… por sk_live_… — nada más en tu código cambia.
Documentación interactiva: {{base_url}}/docs (Scalar) · OpenAPI: {{base_url}}/openapi.json