# API directa (aliados)

> **Para aliados con contrato.** Si eres un comercio, no necesitas esto: usa [enlaces de pago](/developers/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:

| 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 `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](/developers/limites-y-reglas)

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

| `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í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

| 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 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](/developers/webhooks-referencia).

## 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**.

```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_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. 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`
