# 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": ""}` 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=`, 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.