# Kapix Pay en otras plataformas

Esta guía explica cómo se conecta Kapix Pay a cualquier tienda o sistema, qué ruta conviene en cada plataforma y cómo construir un conector nuevo. Los detalles de la API, los webhooks y el plugin de WooCommerce están en la guía de [Integraciones](integraciones.md), y la API completa, en la [especificación OpenAPI](api/openapi.yaml).

## Cómo funciona en cualquier plataforma

Todas las integraciones siguen el mismo patrón de **redirección**: la plataforma crea un cobro, manda al comprador a pagar en Kapix Pay y se entera del resultado por un webhook. La plataforma nunca ve datos de tarjeta, así que el comercio queda en PCI DSS SAQ A.

```mermaid
sequenceDiagram
    participant C as Comprador
    participant T as Tienda o plataforma
    participant K as Kapix Pay
    participant O as Procesador (ONVO)

    C->>T: Finaliza la compra
    T->>K: POST /api/v1/cobros (monto, moneda, order_id, return_url)
    K-->>T: reference + payment_url
    T->>T: Guarda la referencia en el pedido
    T-->>C: Redirige a payment_url
    C->>K: Paga (tarjeta, Apple Pay u otras formas)
    K->>O: Procesa el pago
    O-->>K: Confirma el pago
    K-->>C: Vuelve a return_url?kapix_reference=...
    K->>T: Webhook cobro.pagado (firmado)
    T->>T: Verifica firma, monto y moneda, y marca el pedido pagado
```

Las piezas son siempre las mismas:

1. **Crear el cobro** desde el servidor de la plataforma con la llave de API (`POST /api/v1/cobros`).
2. **Redirigir** al comprador a `payment_url`.
3. **Volver** a `return_url`. Solo es navegación: no prueba que pagó.
4. **Confirmar** con el webhook `cobro.pagado` o consultando `GET /api/v1/cobros/{referencia}`.
5. **Conciliar** reembolsos (`cobro.reembolsado`) y cobros vencidos (`cobro.vencido`).

Si la plataforma no deja hacer los pasos 1 y 4 (crear algo desde su servidor y recibir un webhook), no hay integración automática: se usan enlaces de pago y se marca el pedido a mano.

## Qué ruta usar en cada plataforma

| Plataforma                                         | Ruta                                                    | Estado                      |
| -------------------------------------------------- | ------------------------------------------------------- | --------------------------- |
| WordPress + WooCommerce                            | Plugin de Kapix Pay                                     | Listo y probado             |
| Sitio o app propia (PHP, Laravel, Node, Python…)   | API v1 + webhooks                                       | Listo                       |
| Sistemas internos, ERP, facturación, bots          | API v1 + webhooks                                       | Listo                       |
| PrestaShop, Magento / Adobe Commerce, OpenCart     | Módulo propio con el patrón de esta guía                | Posible, no desarrollado    |
| Wix                                                | Plugin de proveedor de pago de Velo, sitio por sitio    | Posible, no desarrollado    |
| Shopify                                            | Método de pago manual + cobros o enlaces                | Solo manual                 |
| Squarespace                                        | Botón o enlace de pago                                  | Solo manual                 |
| Tiendanube                                         | Método manual + enlaces de pago                         | Solo manual (por confirmar) |
| WhatsApp, Instagram, Facebook, catálogos, impresos | Enlaces de pago y código QR                             | Listo                       |
| Zapier, Make y similares                           | Llamada HTTP a la API; webhooks con cuidado (ver abajo) | Posible, con límites        |

### Sitio o app propia

Es la integración más directa: el backend del sitio llama a la API y expone una ruta para el webhook. Sigue la [receta para un conector nuevo](#receta-para-un-conector-nuevo); hay ejemplos en PHP y Node en la guía de [Integraciones](integraciones.md#verificar-la-firma) y en Python [más abajo](#ejemplo-en-python).

Las apps móviles **no** deben llamar a la API directamente: la llave de API no puede vivir en el teléfono. La app le pide al backend del comercio que cree el cobro y abre `payment_url` en el navegador del sistema (no en un WebView propio), y el backend recibe el webhook.

### PrestaShop, Magento / Adobe Commerce, OpenCart

Son plataformas de código abierto que aceptan módulos de pago propios, así que se puede construir un módulo igual al de WooCommerce. Cada uno necesita:

- Una pantalla de configuración: URL de Kapix Pay, llave de API y secreto del webhook, y que muestre la URL del webhook de la tienda.
- Al confirmar el pedido: crear el cobro con el id del pedido como `order_id`, guardar la referencia y redirigir.
- Una ruta pública para el webhook que verifique la firma y marque el pedido pagado.
- La consulta al volver como respaldo.

El plugin de WooCommerce sirve de referencia (ver [Implementación de referencia](#implementación-de-referencia)). Ninguno de estos módulos existe todavía.

### Wix

Wix permite conectar un procesador que no soporta con el **plugin de proveedor de pago** de Velo (Dev Mode → Public & Backend → Service Plugins → Payment). Tiene dos límites:

- Solo sirve para el sitio donde se crea; no se puede publicar en el Wix App Market. Hay que instalarlo sitio por sitio.
- Para ofrecerlo a todos los comercios de Wix como app, Kapix tendría que hacer el proceso de incorporación de proveedores de pago con Wix.

El esquema sería: al crear la transacción, el plugin llama a `POST /api/v1/cobros` desde el backend del sitio (con la llave guardada en el Secrets Manager de Wix) y devuelve `payment_url` como URL de redirección; una función HTTP del sitio recibe el webhook de Kapix Pay, verifica la firma y le avisa a Wix que la transacción se aprobó. Revisa en la documentación de Wix los nombres exactos de las funciones y si acepta CRC antes de construirlo. Mientras tanto, el botón o el enlace de pago funcionan en cualquier sitio de Wix.

### Shopify

Shopify no permite pasarelas propias sin ser socio de pagos aprobado. La ruta manual está en la guía de [Integraciones](integraciones.md#shopify).

### Squarespace

Squarespace solo procesa pagos con los proveedores que integra (como Stripe, PayPal y Square), así que no hay integración automática. Para servicios o productos de precio fijo, pega el **botón HTML** de un enlace de pago en un bloque de código, o enlaza a `/enlace/{referencia}`. El pedido no se marca solo: revisa los pagos en el panel de Kapix Pay.

### Tiendanube

Por lo que se pudo revisar, las reglas de Tiendanube para apps prohíben procesar pagos fuera de su checkout, y no encontramos un programa público para pasarelas propias. Hasta confirmarlo con Tiendanube, usa un método de pago manual con instrucciones y envía un cobro o un enlace de pago por cada pedido, como en Shopify.

### Redes sociales, WhatsApp y ventas sin tienda

No hace falta programar. En **Enlaces** del panel se crea un enlace con monto fijo, que se comparte por mensaje, se publica en redes o se imprime como QR. Para montos variables, se crea un cobro en **Cobros** y se envía su enlace. Ver la guía de [Integraciones](integraciones.md#enlaces-de-pago-qr-y-botón-sin-código).

### Zapier, Make y herramientas sin código

- **Crear cobros:** cualquier herramienta que haga una petición HTTP puede llamar a `POST /api/v1/cobros` con el header `Authorization: Bearer kpx_…`. Guarda la llave en el almacén de secretos de la herramienta, no en el texto del paso.
- **Recibir webhooks:** el disparador tiene que poder leer el **cuerpo crudo** y el header `Kapix-Signature` y calcular un HMAC-SHA256. Si la herramienta no puede verificar la firma, no uses el webhook para marcar pagos: cualquiera que conozca la URL podría falsificarlo. En ese caso, usa el webhook solo como aviso y confirma con `GET /api/v1/cobros/{referencia}` antes de actuar.

## Receta para un conector nuevo

### 1. Configuración

El conector guarda tres valores en el servidor de la plataforma, nunca en el navegador ni en el código del tema:

- **URL de Kapix Pay** (por ejemplo `https://pay.kapix.co.cr`).
- **Llave de API** (`kpx_test_…` o `kpx_live_…`). Una por plataforma o tienda, con un nombre que la identifique en el panel.
- **Secreto del webhook** (`whsec_…`).

La pantalla de configuración debe mostrar la URL del webhook de la tienda para que el comercio la copie en **Integraciones**. Esa URL tiene que ser `https`, con un dominio público y sin redirecciones.

### 2. Crear el cobro al finalizar la compra

```json
{
    "amount": 1500050,
    "currency": "CRC",
    "description": "Pedido #1001 en Mi Tienda",
    "order_id": "1001",
    "return_url": "https://mitienda.cr/pedido/1001/gracias"
}
```

- **`amount`** en unidad menor (céntimos), como entero. Convierte desde el total del pedido con aritmética decimal o de enteros, nunca con flotantes sin redondear: `15000.50` → `1500050`.
- **`currency`**: solo `CRC` o `USD`. Si la tienda usa otra moneda, oculta el método de pago.
- **`order_id`**: el identificador **interno** e inmutable del pedido, el que sirve para buscarlo después (hasta 100 caracteres).
- **`return_url`**: la página de confirmación del pedido.

Respuestas que hay que manejar:

| Respuesta                     | Qué hacer                                                                                       |
| ----------------------------- | ----------------------------------------------------------------------------------------------- |
| `201` o `200`                 | Guardar `reference` en el pedido y redirigir a `payment_url`. `200` es el mismo cobro de antes. |
| `409 order_in_progress`       | Otra petición está creando el cobro del mismo pedido: espera 1 o 2 segundos y reintenta.        |
| `409 merchant_not_active`     | El comercio no puede cobrar todavía: muestra un error y oculta el método de pago.               |
| `422`                         | Error del conector (monto, moneda o campos). Regístralo y muestra un error genérico.            |
| `502 provider_error`, timeout | Reintenta con el **mismo** `order_id`: si el cobro sí se creó, se reutiliza.                    |

**Guarda la referencia antes de redirigir.** Es lo que une el pedido con el cobro. Si el pedido cambia de monto y se crea otro cobro, guarda también las referencias anteriores: el cobro viejo sigue abierto y el comprador podría pagarlo.

### 3. Al volver a la tienda

El comprador llega a `return_url` con `kapix_reference=<referencia>`.

1. Comprueba que esa referencia sea la que guardaste para el pedido; si no, ignórala.
2. Si el pedido no está pagado todavía, consulta `GET /api/v1/cobros/{referencia}`.
3. Si `status` es `paid` y `amount` y `currency` coinciden con el pedido, márcalo pagado. Si no, muestra "Estamos confirmando tu pago" y espera el webhook.

Nunca marques un pedido pagado solo porque el comprador volvió: cualquiera puede abrir esa URL.

### 4. Recibir el webhook

1. Lee el **cuerpo crudo** antes de parsearlo y verifica `Kapix-Signature` (ver [Verificar la firma](integraciones.md#verificar-la-firma)). Si falla, responde `400`.
2. Ignora repetidos por `Kapix-Event-Id`. Los eventos pueden llegar más de una vez o en otro orden.
3. Busca el pedido por `data.charge.order_id` y comprueba que `data.charge.reference` sea una referencia guardada en ese pedido.
4. Actúa según el tipo (ver la tabla de abajo).
5. Responde `2xx` en menos de 10 segundos. Si hay trabajo pesado, encólalo.

Un evento que no reconoces, incluido `prueba`, también se responde con `2xx`.

| Evento                                               | Qué hacer en la plataforma                                                                                                                                             |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cobro.pagado`                                       | Si monto y moneda coinciden, marca el pedido pagado (una sola vez) y guarda la referencia como id de transacción. Si no coinciden, ponlo en revisión, no lo descartes. |
| `cobro.pagado` de una referencia anterior del pedido | El comprador pagó un cobro viejo. Pon el pedido en revisión: puede haber un pago doble.                                                                                |
| `cobro.reembolsado`                                  | Registra una nota con `data.refund.amount`. Crear el reembolso en la plataforma es decisión del comercio.                                                              |
| `cobro.vencido`                                      | Si el pedido sigue sin pagar, agrega una nota o cancélalo según las reglas de la tienda.                                                                               |

Tabla de estados sugerida:

| Kapix Pay                        | Pedido en la plataforma                  |
| -------------------------------- | ---------------------------------------- |
| Cobro creado (`open`)            | Pendiente de pago                        |
| `paid`, monto y moneda coinciden | Pagado / en proceso                      |
| `paid`, no coincide              | En espera / revisión                     |
| `expired`                        | Pendiente o cancelado (decide la tienda) |
| `refunded_amount > 0`            | Igual, con nota del reembolso            |

No dependas solo de `cobro.vencido` para liberar inventario: define también tu propio plazo para pedidos sin pagar.

### 5. Pruebas antes de activar

- [ ] Llave `kpx_test_…` y el **evento de prueba** desde Integraciones queda **entregado**.
- [ ] Compra con la tarjeta `4242 4242 4242 4242` → el pedido pasa a pagado por el webhook.
- [ ] Con un secreto equivocado, el webhook responde `400` y Kapix Pay lo reintenta.
- [ ] Con el webhook rechazado, el pedido se marca pagado al volver a la tienda (respaldo).
- [ ] Un webhook repetido no duplica notas ni cambios.
- [ ] Un cobro con monto distinto al del pedido deja el pedido en revisión.
- [ ] Tarjeta rechazada (`4000 0000 0000 0002`): el pedido sigue pendiente y el comprador puede reintentar.
- [ ] Doble clic en "Finalizar compra" crea un solo cobro (mismo `order_id`).
- [ ] La llave y el secreto no aparecen en el HTML, en el JavaScript ni en los logs.

Para recibir webhooks en una tienda local hace falta un túnel público (por ejemplo, cloudflared o ngrok).

## Ejemplo en Python

Crear el cobro (con `requests`):

```python
import os
import time

import requests

KAPIX_URL = "https://pay.kapix.co.cr/api/v1"


def create_charge(order):
    headers = {
        "Authorization": f"Bearer {os.environ['KAPIX_API_KEY']}",
        "Accept": "application/json",
    }
    body = {
        "amount": order.total_minor_units,  # entero: 1500050 = ₡15 000,50
        "currency": order.currency,  # "CRC" o "USD"
        "description": f"Pedido #{order.number}",
        "order_id": str(order.id),
        "return_url": f"https://mitienda.cr/pedido/{order.id}/gracias",
    }

    for attempt in range(3):
        response = requests.post(f"{KAPIX_URL}/cobros", json=body, headers=headers, timeout=15)
        if response.status_code == 409 and response.json().get("code") == "order_in_progress":
            time.sleep(2)
            continue
        response.raise_for_status()
        charge = response.json()
        order.save_kapix_reference(charge["reference"])
        return charge["payment_url"]

    raise RuntimeError("Kapix Pay sigue creando el cobro de este pedido")
```

Recibir el webhook (Flask):

```python
import hashlib
import hmac
import json
import os
import time

from flask import Flask, abort, request

app = Flask(__name__)


def kapix_verify(payload: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    timestamp = None
    signatures = []
    for part in (header or "").split(","):
        key, _, value = part.strip().partition("=")
        if key == "t" and value.isdigit():
            timestamp = int(value)
        elif key == "v1" and value:
            signatures.append(value)

    if timestamp is None or abs(time.time() - timestamp) > tolerance:
        return False

    expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + payload, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, signature) for signature in signatures)


@app.post("/webhooks/kapix")
def kapix_webhook():
    payload = request.get_data()  # cuerpo crudo, sin parsear
    if not kapix_verify(payload, request.headers.get("Kapix-Signature", ""), os.environ["KAPIX_WEBHOOK_SECRET"]):
        abort(400)

    event = json.loads(payload)
    # Procesa event["type"] de forma idempotente (por event["id"]).
    return "", 200
```

## Implementación de referencia

El plugin de WooCommerce ([descarga](descargas/kapix-pay-woocommerce.zip)) implementa toda la receta y se probó de punta a punta. Para escribir un conector nuevo, estas son las partes equivalentes:

| Paso                               | WooCommerce (`includes/class-wc-gateway-kapix-pay.php`, salvo donde se indica) |
| ---------------------------------- | ------------------------------------------------------------------------------ |
| Configuración                      | `init_form_fields()` y `webhook_url()`                                         |
| Cliente HTTP (sin reintentar POST) | `includes/class-kapix-pay-client.php`                                          |
| Monto en unidad menor              | `to_minor_units()`                                                             |
| Crear el cobro y redirigir         | `process_payment()`                                                            |
| Recibir el webhook                 | `handle_webhook()`                                                             |
| Verificar la firma                 | `verify_signature()`                                                           |
| Buscar el pedido                   | `find_order()`: `order_id` numérico, mismo método de pago y misma referencia   |
| Marcar pagado                      | `complete_order()`: monto y moneda o queda _en espera_                         |
| Respaldo al volver                 | `check_on_return()`                                                            |

## Límites actuales

- **Solo redirección.** No hay iframe, formulario incrustado ni SDK para la página de la tienda.
- **Solo CRC y USD.**
- **No hay API de reembolsos ni de cancelación.** Los reembolsos se hacen fuera de la API y llegan como `cobro.reembolsado`; un cobro abierto no se puede anular, solo vence.
- **Un monto por cobro.** No se envían líneas de pedido, impuestos ni datos del comprador.
- **No hay cobros recurrentes ni suscripciones** por la API.
- **Solo modo test** (`kpx_test_…`) hasta que se habilite producción.

Si una plataforma necesita algo de esta lista, hay que agregarlo primero a la API de Kapix Pay.
