Broslunas Link

API pública

Crea, lee, edita, borra y ordena los enlaces de tu página desde scripts, Zapier, Make, tu CI o tu propia aplicación. Versión v1.

Empezar

  • URL base: https://app.broslunas.link/api/v1
  • Formato: JSON en peticiones y respuestas (UTF-8). Envía Content-Type: application/json.
  • Alcance: cada token da acceso a una sola página y solo a sus enlaces. No hay endpoints de perfil, tema, estadísticas ni dominio.
  • Sin CORS: está pensada para servidores. No uses un token en código que corra en un navegador.

1. Entra en el panel → API y crea un token. 2. Pruébalo:

Primera petición
curl https://app.broslunas.link/api/v1/links \
  -H "Authorization: Bearer $TOKEN"

Autenticación

Crea tokens en el panel (Dashboard → API). Elige un nombre y un permiso:

PermisoPuede
readSolo lectura: GET
writeLeer y modificar: GET, POST, PATCH, PUT, DELETE

Envía el token en cada petición:

Authorization: Bearer bl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • El token (bl_ + 43 caracteres) se muestra una sola vez. Solo guardamos su hash SHA-256; si lo pierdes, revócalo y crea otro.
  • Máximo 5 tokens por página. Se eliminan al borrar o transferir la página, y dejan de valer si la página se suspende.
  • Guárdalo en variables de entorno, usa un token por integración y con el permiso mínimo. Si se filtra, revócalo: el efecto es inmediato.
  • Sin cabecera, mal formada, token desconocido o revocado → 401 con WWW-Authenticate: Bearer.

Límites

LímiteValorAl superarlo
Peticiones por token60 por minuto429 + Retry-After: 60
Peticiones con token no válido, por IP30 por minuto429
Enlaces por página30422 «Máximo 30 enlaces»
Tokens por página5Se rechaza en el panel

Errores

Todos los errores tienen la misma forma:

{ "error": { "code": "invalid_link", "message": "URL no válida" } }
HTTPcodeCuándo
400invalid_bodyEl cuerpo no es JSON o no es un objeto
401unauthorizedToken ausente, inválido o revocado; página suspendida
403forbiddenToken de solo lectura en una operación de escritura
404not_foundEl enlace no existe en tu página (los ids de otras páginas también dan 404)
422invalid_linkValidación: campo incorrecto, URL no admitida, máximo de enlaces…
422invalid_orderLista de ids de la reordenación incompleta, repetida o con ids ajenos
429rate_limitedLímite de peticiones
500internalError inesperado

message está en español y se puede mostrar al usuario. Para la lógica usa code y el estado HTTP.

El objeto enlace

{
  "id": "6720f1c2a9b3c4d5e6f70812",
  "kind": "link",
  "label": "Mi portfolio",
  "url": "https://ejemplo.com",
  "icon": "web",
  "description": "Trabajos recientes",
  "imageUrl": null,
  "badge": "Nuevo",
  "featured": false,
  "social": false,
  "sensitive": false,
  "enabled": true,
  "startAt": null,
  "endAt": null,
  "position": 0
}
CampoTipoNotas
idstringIdentificador de 24 caracteres hexadecimales
kindlink · header · embed · feedSe fija al crear y no se puede cambiar
labelstring1–80 caracteres. Obligatorio al crear
urlstringHasta 2000 caracteres. Vacío en los títulos de sección (header)
iconstring | nullVer iconos más abajo
descriptionstring | nullHasta 120 caracteres
imageUrlstring | nullhttps y de una imagen subida a tu propia página
badgestring | nullHasta 16 caracteres
featuredbooleanDestacado. Solo kind link
socialbooleanSe muestra como icono de red social. Solo link
sensitivebooleanPide confirmación +18 antes de abrir. Solo link
enabledbooleanfalse lo oculta de la página pública
startAt, endAtISO 8601 | nullPrograman la visibilidad, con zona horaria (p. ej. 2026-12-01T09:00:00+01:00). endAt debe ser posterior a startAt
positionnumberOrden ascendente. Lo gestionan crear y reordenar

Qué admite url según kind

kindurl
linkObligatoria. Solo http, https, mailto y tel. Cualquier otro esquema se rechaza.
headerSe ignora: es un título de sección.
embedObligatoria. YouTube, Spotify, Apple Music, SoundCloud, Deezer, TIDAL, Audiomack, Mixcloud o un podcast.
feedObligatoria. Dirección http o https de un feed.

Los campos que no aplican a un tipo (por ejemplo featured en un embed) se descartan sin error.

Iconos e imágenes

  • Catálogo: web, whatsapp, instagram, tiktok, youtube, x, facebook, linkedin, github, email, phone, telegram, signal, discord, threads, bluesky, mastodon, twitch, pinterest, snapchat.
  • Lucide: lucide: + nombre en kebab-case, p. ej. lucide:map-pin.
  • Propio: img:https://…, solo si es un icono que ya subiste a esta página desde el panel.
  • imageUrl solo acepta imágenes alojadas en el almacenamiento de tu página (las que genera el panel al subirlas). Una URL externa o de otra página da 422 «Imagen no válida». La API no sube archivos.

Listar enlaces

GET/links· token de lectura o escritura

Devuelve todos los enlaces, también los desactivados y los programados, ordenados por position.

curl https://app.broslunas.link/api/v1/links -H "Authorization: Bearer $TOKEN"
200
{ "links": [ { "id": "…", "label": "…", "position": 0 } ] }

Crear un enlace

POST/links· requiere escritura

Lo añade al final. Solo label es obligatorio (y url salvo en header). No acepta enabled ni position: se crea activo; ocúltalo después con PATCH.

curl -X POST https://app.broslunas.link/api/v1/links \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Reserva tu cita",
    "url": "https://ejemplo.com/reservas",
    "icon": "lucide:calendar",
    "badge": "Nuevo",
    "featured": true,
    "startAt": "2026-12-01T00:00:00Z",
    "endAt": "2026-12-31T23:59:59Z"
  }'

Respuesta 201 con { "link": { … } }. Errores: 400, 403, 422.

Otros tipos
{ "kind": "header", "label": "Redes" }
{ "kind": "embed", "label": "Mi último vídeo", "url": "https://youtu.be/dQw4w9WgXcQ" }
{ "label": "Escríbeme", "url": "mailto:hola@ejemplo.com", "icon": "email" }
{ "label": "Llámame", "url": "tel:+34600123456", "icon": "phone" }

Obtener un enlace

GET/links/:id· token de lectura o escritura

200 con { "link": { … } }. 404 si no existe o es de otra página.

Editar un enlace

PATCH/links/:id· requiere escritura

Edición parcial: solo cambian los campos que envíes. Acepta label, url, enabled, icon, description, imageUrl, badge, featured, social, sensitive, startAt y endAt. Envía null para vaciar un campo opcional. kind no se puede cambiar.

# Ocultar un enlace
curl -X PATCH https://app.broslunas.link/api/v1/links/6720f1c2a9b3c4d5e6f70812 \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "enabled": false }'

# Quitar la programación
-d '{ "startAt": null, "endAt": null }'

200 con el enlace ya actualizado. Si cambias icon o imageUrl, el archivo propio anterior se elimina. Errores: 400, 403, 404, 422.

Borrar un enlace

DELETE/links/:id· requiere escritura

Borra el enlace y sus imágenes e iconos propios. Las estadísticas históricas de clics se conservan.

200 con { "ok": true }. Repetir un DELETE ya hecho devuelve 404.

Reordenar

PUT/links/order· requiere escritura

ids debe contener todos los enlaces de la página, una vez cada uno, en el orden deseado (el primero queda arriba). Si falta, sobra o se repite alguno no se cambia nada y se devuelve 422 invalid_order; así dos clientes a la vez no pueden dejar posiciones duplicadas.

curl -X PUT https://app.broslunas.link/api/v1/links/order \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "ids": ["6720…0813", "6720…0812", "6720…0814"] }'

200 con { "links": [ … ] } ya ordenados. Para mover uno: GET, reordena el array de ids y envíalo.

Notas importantes

  • Visibilidad pública: la página solo muestra enlaces con enabled: true y dentro de su ventana startAt/endAt. La API siempre devuelve todos.
  • Caché: cada escritura invalida la caché de tu página; el cambio se ve de inmediato.
  • Concurrencia: no hay control de versiones; gana la última escritura.
  • Reintentos: GET, PUT /links/order y PATCH con los mismos valores se pueden repetir. POST no: reintentar tras un timeout puede duplicar el enlace; comprueba con GET antes.
  • Aislamiento: cada consulta lleva la página del token; nunca verá ni modificará datos de otra.

Ejemplos

Node.js
const BASE = "https://app.broslunas.link/api/v1";
const headers = {
  Authorization: `Bearer ${process.env.LINK_TOKEN}`,
  "Content-Type": "application/json",
};

async function api(path, init) {
  const res = await fetch(BASE + path, { ...init, headers });
  const body = await res.json();
  if (!res.ok) throw new Error(`${res.status} ${body.error.code}: ${body.error.message}`);
  return body;
}

const { link } = await api("/links", {
  method: "POST",
  body: JSON.stringify({ label: "Blog", url: "https://ejemplo.com/blog" }),
});

// Ponerlo el primero
const { links } = await api("/links");
const ids = [link.id, ...links.filter((l) => l.id !== link.id).map((l) => l.id)];
await api("/links/order", { method: "PUT", body: JSON.stringify({ ids }) });
Python
import os, requests

BASE = "https://app.broslunas.link/api/v1"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['LINK_TOKEN']}"

r = s.post(f"{BASE}/links", json={"label": "Podcast", "url": "https://ejemplo.com/podcast"})
r.raise_for_status()
print(r.json()["link"]["id"])