Desde tu sistema · API directa
Docs · Desde tu sistema Descargar .md

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:

  1. Conseguir tu API key y autenticarte.
  2. Hacer tu primer cobro (débito inmediato con OTP).
  3. Enviar un pago a un beneficiario.
  4. Consultar el estado de una operación.
  5. Recibir webhooks de cambios de estado.
  6. 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:

bash
export 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:
LlavePrefijoQué hace
Pruebask_test_…Ambiente de ensayo. No mueve dinero real (sandbox simulado en QA).
Producciónsk_live_…Mueve dinero real sobre los bancos.

El modo lo fija la llave, no tu código. No envías ningún campo mode ni test=true: el servidor deriva el modo (test/live) del prefijo de la API key. La misma petición, con sk_test_… corre simulada y con sk_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:

bash
export SWITCH_KEY="acme:sk_test_xxxxxxxxxxxxxxxxxxxxxxxx"

Verifica que tu llave funciona pidiendo tus transacciones (no mueve dinero):

bash
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 defecto VES).
  • El estado se lee del cuerpo, no del HTTP. Un 200 OK significa que el banco respondió; el campo estado (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.

bash
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):

json
{
  "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 12345678 en 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:

bash
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:

json
{
  "id": "qp_txn_018f3a...",
  "estado": "LIQUIDADA",
  "referencia_banco": "98765432",
  "endtoend": "0000001234567890",
  "motivo_rechazo": null,
  "intentos": 1
}
  • Si estado es LIQUIDADA, 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 webhook transaccion.liquidada.
  • Si es RECHAZADA, mira motivo_rechazo (sección 6). El error típico aquí es OTP_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 /cobrar con el otp, 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: pagar debita 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_test o sk_live) sin esa habilitación responde 403 con { "error": "pagar_no_habilitado" }, y lo mismo POST /switch/v1/dispersiones.

La pieza clave es via, que elige el riel de envío:

viaRielCuándoDestinoResultado
credito_inmediato (default)Crédito Inmediato (CCE)Predominanteteléfono o cuentaLiquida en BCV de forma asíncrona → ACEPTADA, luego LIQUIDADA por consulta/webhook
pago_movilPago entre teléfonos (P2P)Alterno (p. ej. reintegros)solo teléfonoInstantáneo → LIQUIDADA

Reglas de oro:

  • A cuenta siempre es credito_inmediato (aunque envíes via: "pago_movil", a cuenta va por CCE).
  • A teléfono es credito_inmediato por defecto; usa pago_movil solo si quieres el riel P2P instantáneo.
bash
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:

json
{
  "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, default transferencia) y via (default credito_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:

bash
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):

json
{
  "id": "qp_txn_018f4b...",
  "estado": "LIQUIDADA",
  "referencia_banco": "55512345",
  "endtoend": "0000009876543210",
  "motivo_rechazo": null,
  "intentos": 1
}

Ciclo de vida de los estados

EstadoSignificado¿Final?
ACEPTADAEl banco recibió y aprobó (síncrono o "enviada a BCV")No (espera liquidación)
PENDIENTEAceptada, en liquidación interbancaria; consulta o espera webhookNo
LIQUIDADAConfirmada y liquidadaSí (OK)
RECHAZADARechazo de negocio (ver motivo_rechazo)Sí (fallo)
ERRORFallo técnico interno (reintentable)No
DESCONOCIDASin respuesta / timeout post-envíoNo

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 existe idempotency_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)

HTTPSignificadoQué hacer
400Validación del cuerpo. Devuelve { "error": "VALIDATION", "detail": {...} } con los campos inválidosCorrige el payload
401x-switch-key ausente o inválidaRevisa formato <comercio>:<api_key> y que la llave esté activa
404id 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 pruebaVerifica el id, el comercio y el modo de la llave
200El 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.

json
{
  "id": "qp_txn_018f5c...",
  "estado": "RECHAZADA",
  "referencia_banco": null,
  "motivo_rechazo": "SALDO_INSUFICIENTE",
  "intentos": 1
}

Valores de motivo_rechazo y cómo reaccionar:

motivo_rechazoQué pasóAcción sugerida
SALDO_INSUFICIENTEEl pagador/ordenante no tiene fondosAvisa y reintenta luego
OTP_INVALIDOOTP vencido o incorrectoPide token nuevo (/cobrar/token) y reintenta
CUENTA_NO_EXISTECuenta destino/origen inexistenteCorrige el instrumento
CUENTA_BLOQUEADACuenta bloqueadaNo reintentable; contacta al titular
CUENTA_NO_DEBITO / CUENTA_NO_CREDITOLa cuenta no admite débito/créditoUsa otro instrumento
MONTO_INVALIDOMonto fuera de rango/formatoCorrige el monto
EXCEDE_LIMITESupera un tope (banco o riesgo)Reduce monto o revisa límites
BENEFICIARIO_NO_COINCIDENombre/ID no coincide con la cuentaVerifica datos del destino
NO_AFILIADO_SERVICIONo afiliado al servicio de pagoEl titular debe afiliarse
BANCO_NO_DISPONIBLEBanco caído o no procesóReintenta más tarde (QuickPay puede reenrutar en pagar)
DUPLICADAOperación ya procesadaConsulta el estado de la original
FUERA_DE_HORARIOFuera de ventana operativaReintenta en horario
CANCELADA_POR_PAGADOREl pagador cancelóNo reintentable
PENDIENTE_BCVEn liquidación BCVConsulta más tarde
RECHAZO_TECNICOFallo técnico del bancoReintentable con backoff
FIRMA_AUTHCredencial/config del bancoNo reintentable; reportar (no reenrutar dinero)
NO_SOPORTADOOperación no soportada (p. ej. reverso)Maneja por flujo alterno
BLOQUEO_RIESGOBloqueado por el motor de riesgo/fraudeRevisa listas/velocidad; no reintentar a ciegas
SIN_DEBITO_EN_EXTRACTOSó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 QuickPayNo reenvíes con otra idempotency_key sin revisarlo antes con QuickPay
DESCONOCIDOSin clasificarConsulta 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. Ante DESCONOCIDA o 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_… vs sk_live_…).
  • Cobrar = débito inmediato en 2 pasos: /cobrar/token → OTP → /cobrar.
  • Pagar = payout; via elige el riel (credito_inmediato por defecto; pago_movil P2P por teléfono).
  • Estados asíncronos: ACEPTADA → LIQUIDADA. Confírmalos con /consultar o 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