# Contrato real de la API v1 de `admin.u-site.app`

Respuestas verificadas **leyendo el código del servidor**, no inferidas desde
el cliente. Cada punto cita el archivo y la línea para que se pueda auditar.

Base de todos los endpoints: `https://admin.u-site.app/api/v1/…`
(`rest/routes/routes.go:10` monta `/api`, `rest/routes/api.go:38` agrega `v1`).

---

## 1. Formato de `expires_in` → **timestamp Unix absoluto**

Su interpretación es la correcta. En `config/token.go:22-49`:

```go
t.Expire = ninetyYears                                          // acá es duración en segundos
expiresIn := time.Now().Add(time.Duration(t.Expire)*time.Second).Unix()
claims["exp"] = expiresIn
t.Expire = expiresIn                                            // ← se SOBREESCRIBE con el absoluto
```

El campo se reusa: entra como duración y sale como timestamp. Lo que viaja en
la respuesta es `expiresIn`, o sea **segundos desde epoch**.

> **Pero hay algo más importante:** cuando `Login` llama a `CreateToken` no le
> pasa vencimiento (`pkg/auth/user.go:98`), así que aplica el default de
> `ninetyYears` — **90 años**. En la práctica el token de esa integración
> nunca expira, y toda la lógica de caché y renovación no se ejerce jamás.
> Esto hay que cambiarlo del lado nuestro (ver Pendientes).

## 2. Respuesta real de `POST /api/v1/oauth/token`

`rest/controllers/api/auth_controller.go:42-45`:

```json
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expires_in": 4626547200
}
```

- El campo es **`token`**, no `access_token`.
- `expires_in` está en el nivel raíz.
- No hay `token_type`, ni `refresh_token`, ni envoltorio.

Errores (todos con HTTP 401), `auth_controller.go:15-40`:

```json
{ "error": true, "message": "Invalid Credentials" }
```

## 3. QR y VCF → **JSON plano**

| Endpoint | Método | Respuesta |
|---|---|---|
| `/api/v1/generate-qr-tmp` | POST | `{"url": "https://…"}` |
| `/api/v1/generate-url-qr` | POST | `{"url": "https://…"}` |
| `/api/v1/generate-vcf` | POST | `{"success": true, "url": "https://…"}` |
| `/api/v1/generate-qr` | POST | **no es JSON**: devuelve la imagen binaria `Content-Type: image/webp` |

`vcard_qr.go:133-135`, `vcard_vcf.go:103-106`, `vcard_qr.go:169-170`.

Ojo con el último: `generate-qr` responde bytes de imagen, no JSON. Si el
cliente hace `json_decode` de eso, falla siempre.

Errores: `{"error": "Datos inválidos"}` con 400, o `{"error": "<detalle>"}` con 500.

## 4. dLocal → **sí, el envoltorio raro es real**

Nadie lo adivinó mal. `rest/controllers/api/dlocal_controller.go:42-45`:

```go
return c.Status(200).JSON(fiber.Map{
    "message":  "Plan creado exitosamente.",
    "response": string(response),   // ← el JSON de dLocal, como STRING
})
```

O sea, doble codificación real:

```json
{
  "message": "Plan creado exitosamente.",
  "response": "{\"id\":\"PLAN-123\",\"status\":\"active\"}"
}
```

Hay que hacer `json_decode` **dos veces**: una del cuerpo y otra del campo
`response`. Aplica igual a los 7 endpoints dLocal (líneas 44, 77, 110, 137,
162, 188, 221). El campo `message` cambia de texto según el endpoint, así que
no conviene usarlo para decidir nada.

## 5. Cuerpo de error de sesión inválida → **hay DOS textos distintos**

Acá está el problema que sospechaban. `rest/middlewares/auth.go:271-286`:

```go
token := c.Cookies("Verify-Rest-Token")
if token == "" {
    return c.Status(401).JSON("Token not found")     // ← falta la cookie
}
… ErrorHandler: return ctx.Status(401).JSON("Invalid Attempt")  // ← cookie inválida/vencida
```

- Sin cookie → cuerpo `"Token not found"`
- Cookie presente pero inválida o vencida → cuerpo `"Invalid Attempt"`

Los dos son **JSON string, con comillas incluidas** — el cuerpo literal es
`"Invalid Attempt"`, 16 bytes, no el texto pelado.

Su reintento solo cubre el segundo caso. **Recomendación: reintentar ante
cualquier 401**, sin mirar el texto. Es más robusto y no depende de una
redacción que puede cambiar.

## 6. URLs base → **una sola raíz, sin derivar por `str_replace`**

Todos cuelgan de `https://admin.u-site.app/api/v1/`. No hay hosts separados
por grupo, así que lo correcto es guardar **la raíz** y concatenar la ruta,
no guardar la URL del token y derivar las demás:

```
POST   /api/v1/oauth/token
POST   /api/v1/generate-qr-tmp
POST   /api/v1/generate-qr                     (devuelve imagen)
POST   /api/v1/generate-vcf
POST   /api/v1/generate-url-qr
POST   /api/v1/dlocal/subscription/crear-plan
GET    /api/v1/dlocal/subscription/ver-plan/:planID
PATCH  /api/v1/dlocal/subscription/actualizar-plan/:planID
GET    /api/v1/dlocal/subscription/plan/all
PATCH  /api/v1/dlocal/subscription/plan/:planId/subscription/:subscriptionId/deactivate
GET    /api/v1/dlocal/subscription/:subscriptionId/execution/:invoiceId
POST   /api/v1/dlocal/payment/crear-pago
POST   /api/v1/rapyd/wallet/create
```

## 7. El header `Cookie` → **es OBLIGATORIO, y hoy está roto**

Esta es la más crítica de las 10.

La API **no acepta Bearer**. `AuthApi()` lee exclusivamente la cookie
(`rest/middlewares/auth.go:272-281`):

```go
token := c.Cookies("Verify-Rest-Token")
if token == "" { return c.Status(401).JSON("Token not found") }
TokenLookup: "cookie:Verify-Rest-Token"
```

Consecuencias:

- Mandar el token en el body o como `Authorization: Bearer` **no autentica nada**.
- Con el placeholder literal `...` sin completar, **los 12 endpoints
  autenticados devuelven 401 "Token not found"**. Si algo de eso "funciona"
  hoy, es porque el cliente HTTP tiene cookie jar y está reusando la cookie
  que `POST /oauth/token` deja seteada en la respuesta (`config/token.go:40-47`).
- **`session_id` no hace falta**: el middleware no lo mira. Se puede quitar.

Lo correcto es tomar el `token` de la respuesta de `oauth/token` y mandarlo
como `Cookie: Verify-Rest-Token=<token>`, o dejar que el cliente maneje
cookies solo.

## 8. Control de acceso por IP → **no, para `/api/v1` no hay**

`/api/v1` solo valida la cookie JWT. No hay allowlist de IP; no hace falta
registrar la IP del servidor de producción.

Sí existe validación por IP, pero en **otra** API: `/api/v2` usa `ApiKey` con
IP obligatoria y scopes, y `/api/v1/pagos-externos` usa `AuthServicioPago`
(ambas excluidas de `AuthApi` en `auth.go:266-268`). Si prefieren un esquema
con IP fija y token que no vive en una cookie, **`/api/v2` es el camino** y
vale la pena migrar ahí en vez de arreglar el actual.

## 9. Usuario y contraseña

No van en este documento. Son las credenciales de un usuario real de la tabla
`users` (`login.CheckLogin()`, `auth_controller.go:28`). Que las entreguen por
un canal seguro y, dado que llevan 15 meses sin rotar, **conviene crear un
usuario dedicado a la integración** en vez de reusar uno de persona.

## 10. Webhooks → **sí existen, pero no para esto**

Hay webhooks entrantes de pago ya montados (`rest/routes/publicas.go:22-33`):
`/webhooks/bold`, `/webhooks/dlocal`, `/webhooks/paypal`, más Coolify y
Telegram.

Pero son para que **la pasarela nos avise a nosotros**, no para avisarle a un
tercero. Hoy **no existe** un callback saliente hacia el sistema de ustedes ni
para pagos ni para QR. Si lo necesitan, es desarrollo nuevo de nuestro lado.

Mientras tanto, la alternativa ya construida es `/api/v1/pagos-externos`
(`api.go:32-34`), pensada justo para apps de terceros:

```
GET  /api/v1/pagos-externos/pasarelas
POST /api/v1/pagos-externos/solicitar
GET  /api/v1/pagos-externos/:referencia/estado
```

Usa token Bearer + IP (no cookie), así que evita todo el problema del punto 7.
**Para una integración nueva, recomendamos esta y no los endpoints dLocal
directos.**

---

## Pendientes de nuestro lado (no son preguntas, son cosas a corregir)

1. **Tokens de 90 años** (`config/token.go:26`). Un token filtrado es acceso
   permanente. Hay que ponerle un vencimiento razonable — y recién ahí la
   caché y el reintento del cliente van a tener sentido.
2. **La cookie se emite con `Secure: false`** (`config/token.go:44`), así que
   viaja también por HTTP plano.
3. **Autenticación por cookie en una API máquina-a-máquina** es el problema de
   fondo: obliga a manejar cookie jar y no permite IP allowlist. `/api/v2` ya
   resuelve esto bien.
4. **La doble codificación de dLocal** (punto 4) no aporta nada; devolver el
   JSON anidado directo sería un cambio compatible si se versiona.
