🧩

API

Referencia HTTP de la API pública de UBFB (Bun + Express). Todo lo listado aquí como público no requiere clave alguna.

Resumen

Base URL

https://ubfb-api.theindiebrand.es

Límite global

300 peticiones/min por IP

Escritura

20/min en POST /reports

Autenticación

Todo público, salvo Apelaciones

Blacklist

Lecturas públicas sobre el estado decidido — solo entradas Activo.

GET/blacklist/ids
Todos los IDs de Discord actualmente activos — el más barato si solo necesitas comprobar pertenencia.
{ "ids": ["123456789012345678", "234567890123456789"] }
GET/blacklist/all
Todas las entradas activas, filas completas.
{ "entries": [{ "id": "123456789012345678", "reason": "Dox", "status": "Activo", "punishmentDays": 7 }] }
GET/blacklist/:userId
Comprueba un único usuario por su ID de Discord.
{ "blacklisted": true, "entry": { "id": "123456789012345678", "reason": "Dox", "status": "Activo" } }

404 con { "blacklisted": false } si el usuario nunca ha estado en la blacklist.

Reportes

La cola pendiente — cualquiera puede leerla o añadir un reporte; nadie puede resolverla por API.

GET/reports
La cola completa, de solo lectura.
POST/reports

Archiva un nuevo reporte.

  • No lleva punishmentDays — el castigo ya no lo propone quien reporta, se calcula a partir de reason (ver Motivos de blacklist).
  • proofs es obligatorio y no puede ir vacío — 400 si falta, no es un array, o está vacío. Cada URL se descarga, se limpia de metadatos y se re-aloja server-side antes de guardarse.
  • 409 si el usuario ya tiene un reporte pendiente.
  • 201 con el reporte creado — proofs en la respuesta ya son rutas locales, no las URLs originales.
{
  "userId": "123456789012345678",
  "reason": "Flood",
  "authorId": "111111111111111111",
  "authorUsername": "someone",
  "intermediaryId": "222222222222222222",
  "intermediaryUsername": "SomeBot",
  "proofs": ["https://cdn.discordapp.com/attachments/.../screenshot.png"]
}

Exportar datos

GET/export?format=json|csv|sql
Descarga la blacklist activa completa en el formato indicado. Nunca incluye reports ni appeals — esos datos contienen identidades de quien reporta y mensajes privados de apelación.

400 si format falta o no es uno de los tres válidos.

Tiempo real (WebSocket)

Evita que los bots tengan que hacer polling constante a GET /blacklist/ids, y permite saber en todo momento cuántos/qué bots están conectados.

WS/ws
ws://<host>:3000/ws (o wss:// cuando haya TLS configurado).

Al conectar

El servidor envía inmediatamente los IDs de la blacklist activa — servidos desde un cache en memoria, sin consultar la base de datos en cada conexión.

Servidor → cliente"sync"
Solo IDs, no filas completas — con 1000+ entradas no tiene sentido mandar reason,proofs, timestamps, etc. en cada conexión si el cliente solo necesitaisBlacklisted(id). Si necesitas el detalle de un usuario puntual, pídelo aparte conGET /blacklist/:userId.
{ "type": "sync", "ids": ["123456789012345678", "234567890123456789"] }

Opcionalmente, el cliente puede identificarse enviando un mensaje hello justo después de conectar:

Cliente → servidor"hello"
name y avatarUrl son públicos y no verificados — cualquiera puede poner cualquier nombre, es solo información cosmética de presencia, no autenticación. Valores inválidos, vacíos o demasiado largos se descartan en silencio.
{ "type": "hello", "name": "MiBot", "avatarUrl": "https://..." }

Eventos en vivo

A partir de ahí, el servidor empuja actualizaciones sin que el cliente pida nada:

Servidor → cliente"add"
Se emite cuando staff aprueba un reporte. Este sí manda la fila completa — es un evento puntual, no una ráfaga de 1000.
{ "type": "add", "entry": { "id": "123...", "reason": "Estafa", "...": "..." } }
Servidor → cliente"remove"
Se emite cuando staff aprueba una apelación (el usuario sale de la blacklist activa).
{ "type": "remove", "userId": "123..." }

Quién está conectado ahora

GET/bots/active
Bots conectados al WebSocket ahora mismo.
{ "count": 3, "bots": [{ "name": "MiBot", "avatarUrl": "https://...", "connectedAt": "2026-08-23T20:46:22.903Z" }] }

Detalles de la conexión

  • Heartbeat cada 30s (ping/pong) — conexiones muertas se limpian automáticamente.
  • El cache de IDs activos se mantiene en memoria del proceso y se actualiza incrementalmente en cada add/remove — nada más lo toca.
  • No hay autenticación ni rate-limit específico por ahora; es de solo lectura desde el lado del cliente.

Cliente oficial (ubfb)

El paquete oficial en TypeScript ya evita implementar este protocolo a mano — ver Package.

Apelaciones

Uso interno exclusivo del backend del panel web — requiere una cabecera Authorization: Bearer con un secreto compartido que no se distribuye a terceros. No están pensados para integraciones externas: la identidad del usuario ya viene verificada por el login de Discord del panel antes de llegar aquí.

GET/appeals/:userId
La apelación pendiente del usuario, si existe.
POST/appeals
Archiva una apelación. El usuario debe estar actualmente Activo; solo puede haber una pendiente a la vez.

Errores

Formato consistente en toda la API:

{ "error": "mensaje descriptivo" }
{ "errors": ["campo x es obligatorio", "..."] }
400Validación de entrada fallida
401Credenciales internas inválidas
404No encontrado
409Conflicto — ya existe algo pendiente
429Límite de peticiones superado
ANYRuta no reconocida

404 con el mismo formato de error, en vez de la página HTML por defecto de Express.

{ "error": "Not found." }