1. Recorrer todas las páginas de un listado

Problema: un listado devuelve como máximo 100 elementos por petición y necesitas todos.

Solución: pide per_page=100 y avanza page mientras meta.pagination.has_next_page sea true. No calcules el número de páginas por tu cuenta ni sigas hasta recibir data vacío: una página fuera de rango no es un error.

curl
page=1
while :; do
  resp=$(curl -s "https://limbo.lefebvre.es/api/v1/assets?page=$page&per_page=100" \
    -H "Authorization: Bearer $JWT")
  echo "$resp" | jq -r '.data[].id'
  [ "$(echo "$resp" | jq '.meta.pagination.has_next_page')" = "true" ] || break
  page=$((page + 1))
done

Vigila:

  • Si alguien sube o borra assets mientras recorres, un elemento puede aparecer dos veces o saltarse. Deduplica por id.
  • per_page fuera de 1–100 es un 422; limit ya no existe y también da 422.
  • Cada página cuenta como una petición del límite de 600 por minuto (receta 3).

2. Reintentar una subida sin duplicar

Problema: la conexión se corta tras enviar el fichero y no sabes si el asset llegó a crearse.

Solución: genera una Idempotency-Key (un UUID) antes de la primera petición, guárdala junto a la tarea y reenvía exactamente la misma petición con la misma clave. Si la primera llegó a completarse recibes su respuesta guardada (201 con Idempotent-Replayed: true) y no se crea un segundo asset.

curl
KEY=$(uuidgen)

# Primer intento (se corta la conexión) y reintento: misma clave, mismo fichero
for attempt in 1 2 3; do
  curl -i --fail-with-body -X POST https://limbo.lefebvre.es/api/v1/assets \
    -H "Authorization: Bearer $JWT" \
    -H "Idempotency-Key: $KEY" \
    -F "file=@portada.jpg" -F "tags=portada" && break
  sleep $((attempt * 2))
done

# HTTP/1.1 201 Created
# Idempotent-Replayed: true      (solo si es la respuesta guardada del primer intento)

Vigila:

  • La comparación del cuerpo es byte a byte. En multipart cuentan los campos, el nombre del fichero y el hash de su contenido. Un JSON re-serializado con otro orden de claves o espaciado se considera otro cuerpo: 422. Reenvía los bytes guardados, no vuelvas a construir la petición.
  • 409: la primera petición sigue en curso. Espera unos segundos y reintenta con la misma clave.
  • Una petición que falló (4xx o 5xx) no se guarda: puedes corregirla y reutilizar la clave. Para otra subida, genera una clave nueva.
  • La respuesta guardada dura 24 horas.

3. Esperar ante un 429

Problema: una importación masiva supera las 600 peticiones por minuto del portal y recibes 429.

Solución: espera lo que indica Retry-After (segundos, de 1 a 60) y repite la misma petición. No uses un tiempo fijo: la ventana es deslizante y la espera real suele ser menor que un minuto.

curl
while :; do
  code=$(curl -s -o body.json -D headers.txt -w '%{http_code}' \
    "https://limbo.lefebvre.es/api/v1/assets?per_page=100" \
    -H "Authorization: Bearer $JWT")
  [ "$code" != "429" ] && break
  wait=$(grep -i '^retry-after:' headers.txt | tr -dc '0-9')
  sleep "${wait:-1}"
done

Vigila:

  • Mira X-RateLimit-Remaining en las respuestas correctas para frenar antes de llegar al 429.
  • X-RateLimit-Reset es un timestamp Unix: si te quedan peticiones equivale a «ahora».
  • Un reintento tras un 429 en un POST de creación debe llevar Idempotency-Key (receta 2).
  • La petición de token tiene su propio límite (120 por minuto e IP): pide un JWT y reutilízalo durante sus 60 minutos.

4. Leer assets de otro portal

Problema: tu portal necesita mostrar imágenes que pertenecen a otro portal (por ejemplo, atenea).

Solución: un administrador concede la lectura por CLI y después tu portal filtra con include_portals. Comprueba primero qué tienes concedido.

curl
# 1. Administración (una vez, en el servidor de Limbo)
php bin/console limbo:grant-cross-portal-access mi-portal atenea

# 2. Qué portales puedo leer
curl https://limbo.lefebvre.es/api/v1/portals/me/cross-portal-access \
  -H "Authorization: Bearer $JWT"

# 3. Listar solo los assets de atenea
curl "https://limbo.lefebvre.es/api/v1/assets?include_portals=atenea&per_page=50" \
  -H "Authorization: Bearer $JWT"

Vigila:

  • include_portals=atenea devuelve solo los assets de esos portales, no los tuyos. Para ambos, haz dos peticiones.
  • 403 en el listado: tu portal no tiene concedido ese slug. 400: lo combinaste con un filtro de carpeta.
  • Es acceso de lectura. PATCH y DELETE sobre un asset ajeno dan 403 write_access_denied.
  • Un asset que no puedes leer da 404, igual que uno que no existe: no es un fallo de la ruta.
  • Los tags son del portal: no se ven los de otro, ni con include_portals.

5. Reemplazar el fichero de un asset conservando su id

Problema: la imagen de un asset cambió y quieres sustituirla sin perder el id con el que la referencian tus contenidos.

Solución: PATCH /api/v1/assets/{id} con el campo file (URL, base64 o data URI) en JSON. El asset conserva su id, carpeta, tags y metadatos; el nombre de fichero pasa a ser el del nuevo, y la respuesta trae las URLs nuevas. Para fijar el nombre, envía también name en el mismo PATCH.

curl
ID=3f1c9a52-7d0e-4b8a-9c11-0a52e6d4b7f1

# 1. Reemplazar
curl -X PATCH https://limbo.lefebvre.es/api/v1/assets/$ID \
  -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
  -d '{"file": "https://cdn.ejemplo.es/portada-v2.jpg"}'

# 2. Volver a generar las variantes que usabas
curl -X POST https://limbo.lefebvre.es/api/v1/assets/$ID/variants \
  -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
  -d '{"async": false, "variants": [{"name": "card", "width": 400, "height": 300, "format": "webp"}]}'

Vigila:

  • Las variantes se borran al reemplazar: vuelve a generarlas (paso 2) y no sirvas sus URLs antiguas.
  • Lo que se conserva es el id, no la URL de descarga: el nombre del fichero nuevo puede cambiarla. Tras reemplazar, lee la URL de la respuesta (o vuelve a pedir el asset) en lugar de guardarla.
  • El fichero nuevo pasa la misma validación que una subida: formato no permitido o fichero inválido es 422, y una URL a una IP privada se rechaza.
  • Un asset borrado da 404; uno de otro portal, 403 write_access_denied.
  • Usa JSON, no multipart: según el runtime, un PATCH multipart puede no llegar parseado.

Los campos de cada respuesta están en la referencia OpenAPI.