# Integraciones de Kapix Pay

Esta guía es para comercios y desarrolladores que quieren cobrar con Kapix Pay desde su tienda en línea, su sitio propio o sin código.

| Opción                     | Para quién                         | Qué necesita                               |
| -------------------------- | ---------------------------------- | ------------------------------------------ |
| Enlaces de pago, QR, botón | Cualquier comercio, sin programar  | Nada: se crean en **Enlaces** en el panel. |
| Plugin de WooCommerce      | Tiendas en WordPress + WooCommerce | Una llave de API y el secreto del webhook. |
| API v1                     | Sitios propios y otras plataformas | Una llave de API; opcionalmente webhooks.  |
| Shopify                    | Tiendas Shopify                    | Solo la ruta manual (ver abajo).           |

En todos los casos el comprador paga en la página de Kapix Pay (`/pagar/{referencia}` o `/enlace/{referencia}`). La tienda **nunca** recibe ni ve datos de tarjeta: solo redirige al comprador. Por eso el comercio queda en **PCI DSS SAQ A**; Kapix Pay asume el alcance SAQ A-EP de su página de pago. No se ofrece un iframe ni un formulario incrustado.

Para otras plataformas (Wix, PrestaShop, Squarespace, Tiendanube, apps propias, Zapier…) y para construir un conector nuevo, ver la guía [Otras plataformas](plataformas.md).

## Configurar las integraciones

En el panel del comercio, **Integraciones** (se pide la contraseña otra vez):

- **Llaves de API:** crea una por tienda o sistema (máximo 10 activas). La llave completa (`kpx_test_…` o `kpx_live_…`) se muestra **una sola vez**; Kapix Pay solo guarda un hash. Si se pierde, se revoca y se crea otra.
- **Webhook:** la URL de tu tienda que recibe los avisos y un secreto (`whsec_…`) para verificar la firma. El secreto también se muestra una sola vez al generarlo o rotarlo.
- **Enviar evento de prueba** y la lista de envíos recientes, con la opción de reenviar.

La URL del webhook debe ser `https`, con un dominio (no una IP) que resuelva a direcciones públicas. No se siguen redirecciones.

## API v1

- **URL base:** `https://<dominio de Kapix Pay>/api/v1`
- **Autenticación:** `Authorization: Bearer kpx_test_…`
- **Formato:** JSON (`Content-Type: application/json`, `Accept: application/json`).
- **Montos:** enteros en unidad menor. `1500050` CRC son ₡15 000,50; `2500` USD son $25,00.
- **Límite:** 120 peticiones por minuto por llave. Las llaves inválidas se limitan a 10 intentos por minuto por IP.

La especificación completa está en [la especificación OpenAPI](api/openapi.yaml).

### Crear un cobro

```bash
curl -X POST https://pay.kapix.co.cr/api/v1/cobros \
  -H "Authorization: Bearer $KAPIX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "amount": 1500050,
    "currency": "CRC",
    "description": "Pedido #1001",
    "order_id": "1001",
    "return_url": "https://mitienda.cr/gracias"
  }'
```

| Campo         | Tipo   | Obligatorio | Notas                                                                                |
| ------------- | ------ | ----------- | ------------------------------------------------------------------------------------ |
| `amount`      | entero | sí          | Unidad menor. Mínimo 1 (CRC) o 50 (USD).                                             |
| `currency`    | string | sí          | `CRC` o `USD`.                                                                       |
| `description` | string | sí          | Hasta 255 caracteres. La ve el comprador.                                            |
| `order_id`    | string | no          | Tu número de pedido, hasta 100 caracteres. Vuelve en la respuesta y en los webhooks. |
| `return_url`  | string | no          | URL `https` a la que vuelve el comprador después de pagar o cancelar.                |

Respuesta `201 Created` (o `200 OK` si se reutilizó un cobro abierto, ver abajo):

```json
{
    "reference": "01K7Q8Z3V5E6F7G8H9J0K1M2N3",
    "status": "open",
    "amount": 1500050,
    "currency": "CRC",
    "description": "Pedido #1001",
    "order_id": "1001",
    "payment_url": "https://pay.kapix.co.cr/pagar/01K7Q8Z3V5E6F7G8H9J0K1M2N3",
    "return_url": "https://mitienda.cr/gracias",
    "refunded_amount": 0,
    "livemode": false,
    "paid_at": null,
    "created_at": "2026-10-16T15:00:00Z"
}
```

Redirige al comprador a `payment_url`. El comercio del cobro sale de la llave; no se puede enviar otro.

**Mismo pedido, mismo cobro.** Si llega otra petición con el mismo `order_id`, monto y moneda mientras el cobro anterior sigue abierto, se devuelve ese cobro con `200` en vez de crear otro. Así un doble clic o un reintento por timeout no duplican el cobro. Si cambia el monto o la moneda, se crea un cobro nuevo.

### Consultar un cobro

```bash
curl https://pay.kapix.co.cr/api/v1/cobros/01K7Q8Z3V5E6F7G8H9J0K1M2N3 \
  -H "Authorization: Bearer $KAPIX_API_KEY" \
  -H "Accept: application/json"
```

Devuelve el mismo objeto. Solo se ven los cobros del comercio de la llave.

Estados (`status`):

- `open`: esperando el pago.
- `paid`: pagado y confirmado por el procesador.
- `expired`: venció sin pagarse.

Un reembolso no cambia el estado: aumenta `refunded_amount`.

### Volver a la tienda

Si el cobro tiene `return_url`, después de pagar (o de cancelar en el checkout alojado) el comprador vuelve a esa URL con `kapix_reference=<referencia>` agregado a la query:

```text
https://mitienda.cr/gracias?pedido=7&kapix_reference=01K7Q8Z3V5E6F7G8H9J0K1M2N3
```

**Volver no significa que pagó.** Al recibir al comprador, confirma con `GET /api/v1/cobros/{referencia}` o espera el webhook `cobro.pagado`, y comprueba que `amount` y `currency` coincidan con tu pedido.

### Errores

Los errores tienen la forma `{ "message": "...", "code": "..." }`. Los de validación (`422`) siguen el formato de Laravel, con `errors` por campo.

| HTTP | `code`                | Qué hacer                                                                          |
| ---- | --------------------- | ---------------------------------------------------------------------------------- |
| 401  | `invalid_api_key`     | Falta la llave, no existe o fue revocada.                                          |
| 404  | `not_found`           | No hay un cobro con esa referencia para este comercio.                             |
| 409  | `merchant_not_active` | El comercio no ha terminado su verificación con el procesador.                     |
| 409  | `order_in_progress`   | Otra petición está creando el cobro del mismo pedido; reintenta en unos segundos.  |
| 422  | (validación)          | Revisa `errors`.                                                                   |
| 429  | `too_many_requests`   | Espera y reintenta.                                                                |
| 502  | `provider_error`      | El procesador de pagos falló; reintenta en unos minutos (con el mismo `order_id`). |

## Webhooks

Kapix Pay envía un `POST` JSON a la URL configurada cuando cambia un cobro:

| Evento              | Cuándo                                             |
| ------------------- | -------------------------------------------------- |
| `cobro.pagado`      | El cobro se pagó. Se envía una sola vez por cobro. |
| `cobro.vencido`     | El cobro venció sin pagarse.                       |
| `cobro.reembolsado` | Se registró un reembolso (uno por reembolso).      |
| `prueba`            | Evento de prueba desde el panel.                   |

Cuerpo:

```json
{
    "id": "evt_01k7q9a1b2c3d4e5f6g7h8j9k0",
    "type": "cobro.reembolsado",
    "created_at": "2026-10-17T18:30:00Z",
    "livemode": false,
    "data": {
        "charge": {
            "reference": "01K7Q8Z3V5E6F7G8H9J0K1M2N3",
            "status": "paid",
            "amount": 1500050,
            "currency": "CRC",
            "refunded_amount": 250000,
            "order_id": "1001",
            "...": "..."
        },
        "refund": {
            "amount": 250000,
            "currency": "CRC",
            "created_at": "2026-10-17T18:29:58Z"
        }
    }
}
```

`data.charge` es el mismo objeto de la API. `data.refund` solo viene en `cobro.reembolsado`. El evento `prueba` trae `data.message`.

Encabezados:

- `Kapix-Signature: t=<unix>,v1=<hex>`
- `Kapix-Event: cobro.pagado`
- `Kapix-Event-Id: evt_…` (el mismo en cada reintento; úsalo para no procesar dos veces)
- `User-Agent: KapixPay-Webhooks/1.0`

### Verificar la firma

`v1` es `HMAC-SHA256(secreto, "<t>.<cuerpo crudo>")` en hexadecimal. Usa el **cuerpo crudo** tal como llegó (no lo vuelvas a serializar), compara en tiempo constante y rechaza firmas con más de 5 minutos (300 s) de diferencia.

PHP:

```php
function kapix_verify(string $payload, string $header, string $secret, int $tolerance = 300): bool
{
    $timestamp = null;
    $signatures = [];

    foreach (explode(',', $header) as $part) {
        [$key, $value] = array_pad(explode('=', trim($part), 2), 2, null);
        if ($key === 't' && ctype_digit((string) $value)) {
            $timestamp = (int) $value;
        } elseif ($key === 'v1' && $value !== null) {
            $signatures[] = $value;
        }
    }

    if ($timestamp === null || abs(time() - $timestamp) > $tolerance) {
        return false;
    }

    $expected = hash_hmac('sha256', $timestamp.'.'.$payload, $secret);

    foreach ($signatures as $signature) {
        if (hash_equals($expected, $signature)) {
            return true;
        }
    }

    return false;
}

$payload = file_get_contents('php://input');
if (! kapix_verify($payload, $_SERVER['HTTP_KAPIX_SIGNATURE'] ?? '', getenv('KAPIX_WEBHOOK_SECRET'))) {
    http_response_code(400);
    exit;
}
```

Node.js (Express):

```js
import crypto from 'node:crypto';
import express from 'express';

function kapixVerify(payload, header, secret, tolerance = 300) {
    let t = NaN;
    const signatures = [];
    for (const part of (header ?? '').split(',')) {
        const [key, value] = part.trim().split('=', 2);
        if (key === 't' && /^\d+$/.test(value ?? '')) t = Number(value);
        if (key === 'v1' && value) signatures.push(value);
    }
    if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > tolerance)
        return false;

    const expected = crypto
        .createHmac('sha256', secret)
        .update(`${t}.${payload}`)
        .digest();
    return signatures.some((sig) => {
        const given = Buffer.from(sig, 'hex');
        return (
            given.length === expected.length &&
            crypto.timingSafeEqual(given, expected)
        );
    });
}

const app = express();
app.post(
    '/webhooks/kapix',
    express.raw({ type: 'application/json' }),
    (req, res) => {
        const payload = req.body.toString('utf8');
        if (
            !kapixVerify(
                payload,
                req.get('Kapix-Signature'),
                process.env.KAPIX_WEBHOOK_SECRET,
            )
        ) {
            return res.sendStatus(400);
        }
        const event = JSON.parse(payload);
        // Procesa event.type de forma idempotente (por event.id).
        res.sendStatus(200);
    },
);
```

### Respuesta y reintentos

- Responde cualquier `2xx` en menos de 10 segundos. Procesa lo pesado después.
- Cualquier otra respuesta, un timeout o un error de red se reintenta: hasta 8 intentos en unas 8 horas (1 min, 5 min, 15 min, 30 min, 1 h, 2 h y 4 h).
- Las redirecciones (`3xx`) cuentan como fallo y no se siguen.
- Si se agotan los intentos, el envío queda **fallido** en el panel y se puede reenviar. Un reenvío conserva el `id` y el cuerpo, con una firma nueva.
- Los eventos pueden llegar más de una vez o fuera de orden. Usa `Kapix-Event-Id` para ignorar repetidos y, ante la duda, consulta el cobro con la API.

## Plugin de WooCommerce

Descarga: [kapix-pay-woocommerce.zip](descargas/kapix-pay-woocommerce.zip). Requiere WordPress 6.4+, WooCommerce 8.0+ y PHP 7.4+. Funciona con el checkout clásico y con el de bloques, y es compatible con HPOS.

1. Sube `kapix-pay-woocommerce.zip` en **Plugins → Añadir nuevo → Subir plugin** y actívalo.
2. En Kapix Pay, **Integraciones**: crea una llave de API llamada "WooCommerce".
3. En WooCommerce, **Ajustes → Pagos → Kapix Pay**:
    - **URL de Kapix Pay:** el dominio de Kapix Pay (por ejemplo `https://pay.kapix.co.cr`).
    - **Llave de API:** la llave del paso 2.
    - La página muestra la URL del webhook de la tienda, por ejemplo `https://mitienda.cr/wc-api/kapix_pay/` (con enlaces permanentes) o `https://mitienda.cr/?wc-api=kapix_pay`. Las dos funcionan; copia la que aparece.
4. En Kapix Pay, pega esa URL como webhook, genera el secreto y cópialo en **Secreto del webhook** en WooCommerce.
5. Envía el evento de prueba desde Kapix Pay; debe quedar **entregado**.
6. Haz una compra de prueba con la tarjeta `4242 4242 4242 4242` (cualquier fecha futura y CVV).

Cómo funciona:

- Al finalizar la compra, el plugin crea el cobro con `order_id` = id interno del pedido (puede no coincidir con el número que ve el cliente si usas un plugin de numeración) y redirige al comprador a Kapix Pay.
- Con `cobro.pagado` marca el pedido como pagado, solo si el cobro es el del pedido y coinciden monto y moneda. Si no coinciden, lo deja **en espera**.
- Al volver a la página de "gracias", consulta el cobro como respaldo por si el webhook se atrasa.
- `cobro.reembolsado` y `cobro.vencido` agregan una nota al pedido. Los reembolsos no se crean automáticamente en WooCommerce.
- La moneda de la tienda debe ser CRC o USD.

## Shopify

Shopify no permite agregar pasarelas de pago propias sin ser socio de pagos aprobado (Payments Apps API), así que por ahora **no hay integración automática**. La ruta manual:

1. En Shopify, **Configuración → Pagos → Métodos de pago manuales**, crea uno llamado "Tarjeta o SINPE con Kapix Pay", con instrucciones como "Te enviaremos un enlace de pago".
2. Por cada pedido, crea un cobro en el panel de Kapix Pay (o con la API) con el número de pedido en la descripción y envía el enlace al comprador.
3. Cuando el cobro aparezca pagado en Kapix Pay, marca el pedido como pagado en Shopify.

Para productos de precio fijo, un enlace de pago reutilizable (abajo) en la descripción del producto también funciona. En ningún caso se sincronizan pedidos automáticamente.

## Enlaces de pago, QR y botón (sin código)

En **Enlaces** del panel, el comercio crea un enlace reutilizable con monto, moneda y descripción fijos: `https://pay.kapix.co.cr/enlace/<referencia>`.

- Se puede compartir por WhatsApp o redes, imprimir su **código QR** (SVG) o pegar el **botón HTML** en cualquier sitio. El botón es un enlace (`<a>`) con estilos en línea, sin scripts.
- Cada comprador que toca **Pagar** recibe su propio cobro; el mismo comprador en la misma sesión reutiliza su cobro abierto.
- Se puede desactivar y reactivar. Un enlace desactivado o de un comercio que no puede cobrar muestra que no está disponible.
- La página muestra cuántas veces se ha pagado. Cada pago aparece como un cobro normal en el panel y genera webhooks como cualquier otro.

## Modo test y producción

Por ahora Kapix Pay opera solo en modo test (`kpx_test_…`, `livemode: false`). Las tarjetas de prueba están en la documentación de ONVO.
