Referencia de integración · admin.u-site.app

Contrato real de la API v1

Las 10 preguntas del equipo integrador, respondidas leyendo el código del servidor en vez de inferirlas desde el cliente. Cada respuesta cita archivo y línea para que se pueda auditar. Una es crítica y explica por qué la integración probablemente nunca autenticó.

1crítica
2requieren atención
7contrato confirmado
4arreglos de nuestro lado

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

01
Su lectura es correcta

expires_in es un timestamp Unix absoluto

El campo se reusa: entra como duración en segundos y sale como timestamp.

// config/token.go:26-49
t.Expire = ninetyYears                    // duración, en segundos
expiresIn := time.Now().Add(
    time.Duration(t.Expire)*time.Second).Unix()
claims["exp"] = expiresIn
t.Expire = expiresIn                     // ← se SOBREESCRIBE

Lo que viaja en la respuesta es expiresIn: segundos desde epoch.

Pero hay algo más grave. Login llama a CreateToken sin pasarle vencimiento (pkg/auth/user.go:98), así que aplica el default: 90 años.

En la práctica ese token nunca expira, y toda la lógica de caché y renovación del cliente no se ejerce jamás. Es un arreglo nuestro, no de ustedes.

02
Contrato confirmado

Respuesta de POST /oauth/token

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

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

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

Error de credenciales, siempre HTTP 401:

{ "error": true, "message": "Invalid Credentials" }
03
Plano, con una excepción

QR y VCF devuelven JSON sin envoltorio

EndpointMétodoRespuesta
/generate-qr-tmpPOST{"url": "https://…"}
/generate-url-qrPOST{"url": "https://…"}
/generate-vcfPOST{"success": true, "url": "https://…"}
/generate-qrPOSTNo es JSON — imagen binaria 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. Un json_decode sobre eso falla siempre.

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

04
Sí, el envoltorio raro es real

dLocal devuelve JSON codificado como string

Nadie lo adivinó mal. La doble codificación existe:

// rest/controllers/api/dlocal_controller.go:42-45
return c.Status(200).JSON(fiber.Map{
    "message":  "Plan creado exitosamente.",
    "response": string(response),   // ← el JSON, como STRING
})

Lo que llega:

{
  "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 texto de message cambia según el endpoint, así que no conviene usarlo para decidir nada.

05
Hay dos textos, no uno

El reintento automático cubre solo la mitad de los casos

// 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 o vencida

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

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

06
Una sola raíz

No hay hosts separados por grupo

Todo cuelga de https://admin.u-site.app/api/v1/. Lo correcto es guardar la raíz y concatenar la ruta, en vez de guardar la URL del token y derivar las demás con str_replace.

POST   /api/v1/oauth/token
POST   /api/v1/generate-qr-tmp
POST   /api/v1/generate-qr                     (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
07
Crítico

El header Cookie es obligatorio, y hoy está roto

La API no acepta Bearer. Autentica exclusivamente por 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"

Con el placeholder literal ... sin completar, los 12 endpoints autenticados devuelven 401. Si algo funciona hoy, es porque el cliente HTTP tiene cookie jar y reusa la cookie que POST /oauth/token deja seteada en la respuesta (config/token.go:40-47).

  • Mandar el token en el body o como Authorization: Bearer no autentica nada.
  • session_id no hace falta: el middleware no lo mira. Se puede quitar.

Lo correcto: 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.

08
No aplica

No hay control por IP en /api/v1

Solo se valida la cookie JWT. No hay allowlist; 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 (auth.go:266-268).

Si prefieren un esquema con IP fija y token que no viva en una cookie, /api/v2 es el camino, y conviene migrar ahí en vez de arreglar el actual.

09
Fuera de este documento

Credenciales

No van acá. Son las de un usuario real de la tabla users (login.CheckLogin(), auth_controller.go:28). Que se entreguen por un canal seguro.

Dado que llevan 15 meses sin rotar, conviene crear un usuario dedicado a la integración en vez de reusar el de una persona.

10
Existen, pero no para esto

No hay callback saliente hacia ustedes

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 sirven para que la pasarela nos avise a nosotros, no para avisarle a un tercero. Un callback hacia su sistema es desarrollo nuevo.

Mientras tanto, la alternativa ya construida es /api/v1/pagos-externos, 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 07. Para una integración nueva, recomendamos esta y no los endpoints dLocal directos.

Pendientes de nuestro lado

No son preguntas: son cosas a corregir en admin.u-site.app.

  1. Tokens de 90 años (config/token.go:26). Un token filtrado es acceso permanente. 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 allowlist por IP. /api/v2 ya lo resuelve bien.
  4. La doble codificación de dLocal no aporta nada; devolver el JSON anidado directo sería un cambio compatible si se versiona.