# Novasis Pay — Guía de integración del Gateway

> Documento para desarrolladores que integran un comercio (ERP, e‑commerce, app móvil)
> con el gateway **Novasis Pay**. Cubre autenticación, endpoints, modalidades de cobro,
> webhooks y flujos completos con ejemplos `curl`.
>
> - **Swagger / OpenAPI interactivo:** `GET /docs` (JSON en `/docs-json`).
> - **Spec versionada:** `docs/openapi/openapi.json`.
> - Última revisión: 2026-08-17.

---

## 1. URLs y ambientes

| Ambiente | Base URL |
|---|---|
| Producción | `https://api.pay.novasis.com/v1` |
| Local / dev | `http://localhost:3010/v1` |

- Todas las rutas cuelgan del prefijo **`/v1`** (excepto `GET /health` y `GET /docs`).
- **Test vs Live** se define por la clave usada (ver auth). No hay hosts distintos: el ambiente lo determina el prefijo de la API key (`sk_test_` vs `sk_live_`).

---

## 2. Autenticación

El gateway emite **pares de claves por comercio y por ambiente**:

| Clave | Prefijo | Uso | Exponer al cliente |
|---|---|---|---|
| **Secreta** | `sk_test_` / `sk_live_` | Llamadas servidor‑a‑servidor. Va en `Authorization`. | ❌ Nunca |
| **Pública** | `pk_test_` / `pk_live_` | Identifica al comercio en la página de pago / drop‑in. | ✅ Sí |

```
Authorization: Bearer sk_test_xxxxxxxxxxxx
Content-Type: application/json
```

- Las claves las emite el equipo de Novasis Pay (o el panel admin, tab **Claves API → Pruebas/Producción**). El **secreto completo se muestra una sola vez** al crearlo.
- Los endpoints **públicos** (catálogo de providers y el checkout hospedado por `publicToken`) **no** requieren `Authorization`.

**401** si la clave es inválida o revocada. **403** si la clave no corresponde al recurso.

---

## 3. Convenciones

### Montos y monedas
El `amount` se envía en la **unidad mínima** de la moneda:

| Moneda | `amount` | Equivale a |
|---|---|---|
| PYG (sin decimales) | `1000` | Gs. 1.000 |
| USD | `1500` | $15.00 |

Monedas sin decimales: `PYG, CLP, JPY, KRW, VND` (el resto usa 2 decimales).

### Idempotencia
En `POST /payment-intents` enviá el header **`Idempotency-Key`** (un UUID por operación lógica). Reintentos con la misma clave devuelven el intent ya creado en vez de duplicarlo.

### Formato de error
```json
{ "statusCode": 400, "error": "Bad Request", "message": "customerId does not belong to this merchant" }
```

### IDs (prefijados, opacos)
`mer_…` merchant · `cus_…` customer · `pi_…` payment intent · `att_…` attempt · `cs_…` checkout session · `pk_/sk_…` claves.

---

## 4. Modelo mental

```
Comercio (merchant)
   └─ crea Customer (opcional)         POST /customers
   └─ crea Payment Intent              POST /payment-intents
        └─ lo COBRA de 2 maneras:
             a) Confirmándolo directo   POST /payment-intents/:id/confirm   → link/QR del provider
             b) Con checkout hospedado  POST /checkout-sessions             → página de Novasis
   └─ el estado se actualiza por:
        · Webhook saliente (gateway → comercio)   [preferido]
        · Sync manual/polling                      POST /payment-intents/:id/sync
```

El gateway abstrae al **provider** (Dpago, dLocal, …): hablás **un contrato canónico** y cada
adapter traduce a la API del provider. Las diferencias entre providers se **declaran** en el
catálogo (sección 10), así el cliente se adapta sin hardcodear reglas.

---

## 5. Modalidades de cobro

| Modalidad | Cómo | Resultado | Cuándo usar |
|---|---|---|---|
| **Link de pago nativo** | `confirm` con `paymentLink: true` | URL del provider (ej. `pago.dpago.com/link?code=…`) para compartir | Cobro remoto (WhatsApp/email), no requiere método ni cliente presente |
| **Cobro directo (QR/tarjeta)** | `confirm` con `platformId` | `qr` (EMV para dibujar) y/o `redirectUrl` | Caja / cobrador: el cliente paga en el acto |
| **Checkout hospedado** | `POST /checkout-sessions` | URL `…/c/:publicToken` (página de Novasis, el cliente elige método) | Un flujo unificado multi‑provider |

> El QR embebido (`qr`) y el link nativo dependen del provider; consultá el descriptor de
> capacidades (sección 10) para saber qué soporta cada uno.

---

## 6. Endpoints del comercio (requieren `Authorization`)

### 6.1 `GET /merchants/me`
Datos del comercio autenticado. Útil para validar la conexión.

### 6.2 Catálogo de providers — `GET /providers` *(público)*
Devuelve providers soportados con sus **capacidades** (ver sección 10).

### 6.3 Métodos de pago — `GET /providers/:provider/payment-methods?country=PY` *(público)*
```json
{ "methods": [
  { "platformId": "18", "name": "QR Ueno", "method": "other" },
  { "platformId": "14", "name": "Pix",     "method": "pix" }
] }
```
Usalo para poblar el selector de método del cobro directo.

### 6.4 Customers
- `POST /customers` → crea. Body: `{ email, name, docType, docNumber, phone, country }`.
  `docType` ∈ `ruc | ci | dni | rut | cuit | cuil | passport | other`.
- `GET /customers?email=&docNumber=&limit=` → busca (filtra con AND).
- `GET /customers/:id` · `PATCH /customers/:id`.

```json
// respuesta
{ "id": "cus_01…", "email": "cliente@x.com", "name": "Juan Perez",
  "docType": "ci", "docNumber": "1234567", "phone": "0981…", "country": "PY" }
```
> Algunos providers exigen customer completo para el cobro directo (ver `requiredCustomerFields`).

### 6.5 Payment Intents

**Crear — `POST /payment-intents`**
```json
{ "amount": 1000, "currency": "PYG", "country": "PY",
  "description": "Pago pedido #123", "externalReference": "FAC-001",
  "requestedProvider": "dpago", "customerId": "cus_01…", "platformId": "18" }
```
Campos: `amount, currency, country` requeridos; `description, externalReference,
requestedProvider, customerId, platformId, metadata, expiresAt` opcionales. Header
`Idempotency-Key` recomendado.
```json
// respuesta
{ "id": "pi_01…", "merchantId": "mer_01…", "status": "created",
  "amount": "1000", "currency": "PYG", "country": "PY" }
```

**Confirmar — `POST /payment-intents/:id/confirm`**
```json
// A) Link de pago nativo:
{ "paymentLink": true }
// B) Cobro directo (QR/tarjeta):
{ "platformId": "18" }
// opcionales: returnUrl, cancelUrl, providerConfigId
```
```json
// respuesta
{ "intent":  { "id": "pi_01…", "status": "pending", … },
  "attempt": { "id": "att_01…", "status": "pending",
               "providerPaymentId": "pl_…",
               "redirectUrl": "https://pago.dpago.com/link?code=pl_…",
               "qr": "0002010102…visa.com.py…",  // EMV o null
               "method": "other", "platformId": "18" } }
```
- `redirectUrl`: URL a la que enviar/redirigir al cliente (link o página del provider).
- `qr`: payload EMV para **dibujar el QR** vos mismo (o `null` si el provider no lo da).
- Reglas: `paymentLink: true` **ignora** `platformId` y no requiere método. Sin `paymentLink`,
  los providers que exigen método (`requiresPlatformId`) necesitan `platformId`.

**Consultar — `GET /payment-intents/:id`** · **Listar — `GET /payment-intents?limit=&cursor=&status=`**

**Sincronizar — `POST /payment-intents/:id/sync`**
Consulta el estado real al provider y actualiza intent + attempt (y emite el webhook saliente).
Es el **fallback** cuando el webhook del provider no llega (dev sin túnel público).
```json
{ "id": "pi_01…", "status": "approved", … }
```

### 6.6 Checkout Sessions (hospedado)
**Crear — `POST /checkout-sessions`**
```json
{ "intentId": "pi_01…", "returnUrl": "https://tu.com/ok",
  "cancelUrl": "https://tu.com/cancel", "expirationHours": 24,
  "uiConfig": { "qrEmbedded": true } }
```
```json
// respuesta (clientSecret SOLO acá)
{ "id": "cs_01…", "intentId": "pi_01…", "publicToken": "an3r38…",
  "clientSecret": "cs_secret_…", "url": "http://localhost:5176/c/an3r38…",
  "status": "open", "expiresAt": "2026-08-19T…Z" }
```
Compartí `url` con el cliente. `uiConfig` viaja a la página de pago (ej. `qrEmbedded` decide si
el QR directo se dibuja embebido o se redirige). `GET /checkout-sessions/:id` para leerla
(no devuelve `clientSecret`).

---

## 7. Endpoints públicos del checkout (sin auth, por `publicToken`)

La página de pago (o tu propio front) los consume con el `publicToken` de la sesión:

- `GET /public/checkout-sessions/:publicToken` → datos seguros para mostrar (monto, moneda,
  descripción, `merchantName`, `uiConfig`, `returnUrl/cancelUrl`, `customer` prellenado).
- `GET /public/checkout-sessions/:publicToken/payment-methods` → métodos disponibles.
- `POST /public/checkout-sessions/:publicToken/confirm`
  ```json
  { "customer": { "email": "…", "name": "…", "docNumber": "…", "phone": "…" },
    "platformId": "18" }
  ```
  ```json
  { "status": "pending", "redirectUrl": "…", "qr": "…",
    "returnUrl": "…", "cancelUrl": "…" }
  ```
  Tras 5 intentos fallidos la sesión se bloquea.

---

## 8. Webhooks salientes (gateway → tu comercio)

Es la forma **autoritativa** de enterarte del resultado de un pago.

### 8.1 Registrar tu endpoint — `POST /webhook-endpoints`
```json
{ "url": "https://tu-backend.com/novasis-pay/webhooks", "events": ["*"] }
```
El gateway te entrega un **secreto de firma** (`whsec_…`) para verificar los envíos.

### 8.2 Headers de cada envío
```
X-Payments-Event-Id:    evt_01…        (único → usalo para idempotencia)
X-Payments-Event-Type:  payment_intent.succeeded
X-Payments-Signature:   t=<unix>,v1=<hmac_sha256_hex>
```

### 8.3 Verificación de firma (HMAC‑SHA256)
`v1 = HMAC_SHA256(secret, "<t>.<rawBody>")`. Rechazá si difiere o si `|now - t| > 300s`.
```js
const [t, v1] = parse(header);              // "t=..,v1=.."
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const ok = timingSafeEqual(expected, v1) && Math.abs(nowSec - Number(t)) <= 300;
```
Respondé **HTTP 200** siempre que la firma sea válida (aunque ignores el evento), para que el
gateway no reintente. Deduplicá por `X-Payments-Event-Id`.

### 8.4 Tipos de evento
`payment_intent.created · payment_intent.processing · payment_intent.succeeded ·
payment_intent.failed · payment_intent.expired · payment_intent.refunded ·
payment_intent.partially_refunded · checkout_session.completed · checkout_session.expired`.

Al registrar el endpoint podés suscribirte a `["*"]` (todos) o a una lista específica.
Reintentos ante fallo: hasta 8 intentos con backoff (1, 5, 30, 120, 360, 720, 1440 min).

### 8.5 Payload
```json
{ "id": "evt_01…", "event_type": "payment_intent.succeeded", "created_at": "…",
  "data": { "payment_intent_id": "pi_01…", "merchant_id": "mer_01…",
            "external_reference": "FAC-001", "amount": "1000",
            "currency": "PYG", "country": "PY", "status": "approved" } }
```

---

## 9. Ciclo de vida del estado

```
created → pending → processing → approved   (éxito)
                              ↘  rejected     (rechazado)
                              ↘  expired      (venció)
```
Marcá el documento como pagado **solo** en `approved`. `pending`/`processing` no son pago.

---

## 10. Descriptor de capacidades por proveedor

`GET /providers` devuelve, por provider, cómo integrarlo. El cliente debe **adaptar su UI y
validaciones a esto** en vez de hardcodear reglas.

```json
{ "providers": [
  { "id": "dpago", "displayName": "Dpago", "available": true,
    "countries": ["PY"], "paymentMethods": ["card","wallet","pix","zimple","qr"],
    "minAmount": { "PYG": 1000 },
    "requiresPlatformId": true,
    "requiredCustomerFields": ["email","name","docNumber","phone"],
    "supportsDirect": true, "supportsHosted": true, "supportsNativeLink": true,
    "features": ["qr"] },
  { "id": "dlocal", "displayName": "dLocal Go", "available": true,
    "minAmount": { "PYG": 10000 },
    "requiresPlatformId": false, "requiredCustomerFields": [],
    "supportsDirect": false, "supportsHosted": true, "supportsNativeLink": false,
    "features": ["refund"] }
] }
```

| Campo | Significado |
|---|---|
| `minAmount` | Monto mínimo por moneda. Validá el cobro contra esto. |
| `paymentMethods` / `GET …/payment-methods` | Métodos y sus `platformId`. |
| `requiresPlatformId` | Si el cobro directo exige elegir método. |
| `requiredCustomerFields` | Campos del comprador obligatorios para el cobro directo. |
| `supportsDirect` | Ofrece cobro directo (QR/tarjeta en el acto). |
| `supportsHosted` | Ofrece checkout hospedado de Novasis. |
| `supportsNativeLink` | Ofrece link de pago propio (ej. Dpago `/links`). |
| `features` | Extras (`qr`, `refund`, …). |

---

## 11. Flujos completos (curl)

> Reemplazá `SK` por tu `sk_test_…` y `API` por la base URL.

### A) Link de pago nativo (Dpago) — se comparte, funciona en test
```bash
PI=$(curl -s -X POST $API/payment-intents -H "Authorization: Bearer $SK" \
  -H "Content-Type: application/json" \
  -d '{"amount":1000,"currency":"PYG","country":"PY","requestedProvider":"dpago","description":"Pago 1"}' \
  | jq -r .id)

curl -s -X POST $API/payment-intents/$PI/confirm -H "Authorization: Bearer $SK" \
  -H "Content-Type: application/json" -d '{"paymentLink":true}'
# → attempt.redirectUrl = https://pago.dpago.com/link?code=pl_...  (compartir)
```

### B) Cobro directo QR (cliente presente)
```bash
CUS=$(curl -s -X POST $API/customers -H "Authorization: Bearer $SK" -H "Content-Type: application/json" \
  -d '{"email":"c@x.com","name":"Juan","docType":"ci","docNumber":"1234567","phone":"0981","country":"PY"}' | jq -r .id)

PI=$(curl -s -X POST $API/payment-intents -H "Authorization: Bearer $SK" -H "Content-Type: application/json" \
  -d "{\"amount\":1000,\"currency\":\"PYG\",\"country\":\"PY\",\"requestedProvider\":\"dpago\",\"customerId\":\"$CUS\"}" | jq -r .id)

curl -s -X POST $API/payment-intents/$PI/confirm -H "Authorization: Bearer $SK" -H "Content-Type: application/json" \
  -d '{"platformId":"18"}'
# → attempt.qr = EMV para dibujar el QR (o redirectUrl a la página del provider)
```

### C) Checkout hospedado
```bash
PI=$(curl -s -X POST $API/payment-intents -H "Authorization: Bearer $SK" -H "Content-Type: application/json" \
  -d '{"amount":50000,"currency":"PYG","country":"PY","requestedProvider":"dpago"}' | jq -r .id)

curl -s -X POST $API/checkout-sessions -H "Authorization: Bearer $SK" -H "Content-Type: application/json" \
  -d "{\"intentId\":\"$PI\",\"returnUrl\":\"https://tu.com/ok\"}"
# → url = http://localhost:5176/c/<publicToken>  (abrir/compartir)
```

### Sincronizar estado (fallback sin webhook)
```bash
curl -s -X POST $API/payment-intents/$PI/sync -H "Authorization: Bearer $SK"
# → { "status": "approved" | "pending" | ... }
```

---

## 12. Errores comunes

| Situación | Causa | Solución |
|---|---|---|
| `401 Unauthorized` | Clave inválida/revocada o ambiente equivocado | Verificá `sk_test_`/`sk_live_` según ambiente |
| `customerId does not belong to this merchant` | El `customerId` es de otro comercio | Creá el customer bajo el comercio actual |
| `platformId '…' no es válido/activo` | Método no habilitado para el provider | Consultá `GET …/payment-methods` |
| Monto rechazado | Menor al `minAmount` del provider | Respetá `minAmount` del catálogo |
| El webhook no llega (dev) | El provider/gateway no alcanza `localhost` | Usá `POST /sync` o exponé con un túnel (ngrok) |

---

## 13. Notas por proveedor

- **Dpago**: cobro directo (`/transactions`) devuelve `redirectUrl` y, según comercio/ambiente,
  `qr` (EMV). Link nativo (`/links`) funciona en test y aparece en el panel de Dpago. `refund`
  no está disponible por API. Estado de un link: `active`→pendiente, consumido→aprobado.
- **dLocal Go**: solo checkout hospedado; elige método y pide datos del comprador en su página.

Para el detalle del proveedor Dpago ver `docs/guia-tecnica-api-dpago-integrador.md`.
