Files
soft_usite/docs/api-v1-contrato.md
T
Lizandro GuarnizoandClaude Sonnet 5 ff4237eced docs: contrato real de la API v1 para el equipo integrador
Responde las 10 preguntas que mandaron sobre admin.u-site.app leyendo el
código del servidor en vez de inferirlas desde el cliente, con cita de
archivo y línea en cada una.

Lo que sale de ahí:

- expires_in sí es timestamp absoluto (su lectura era correcta), pero el
  token se emite con 90 años de vigencia porque Login no pasa vencimiento.
- La API v1 NO acepta Bearer: autentica solo por la cookie
  Verify-Rest-Token, así que el placeholder "..." sin completar deja los 12
  endpoints en 401. Es la causa más probable de que la integración nunca
  haya autenticado por sí misma.
- Hay DOS textos de error 401 ("Token not found" y "Invalid Attempt"), y su
  reintento solo cubre el segundo.
- El envoltorio doble-codificado de dLocal es real, nadie lo adivinó mal.
- generate-qr devuelve imagen binaria, no JSON.

Incluye los pendientes de nuestro lado (vigencia del token, Secure=false en
la cookie, auth por cookie en una API máquina-a-máquina) y la recomendación
de usar /api/v1/pagos-externos, que ya usa Bearer + IP, para integraciones
nuevas.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 02:02:59 -05:00

8.2 KiB

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_intimestamp Unix absoluto

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

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 ninetyYears90 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:

{
  "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:

{ "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:

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:

{
  "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:

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

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):

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.