Credenciales

Cada API key tiene dos claves: la pública (pk_…), que identifica la key y puede viajar en el navegador, y la privada (sk_…), que solo existe en tu servidor. Una API key pertenece a un portal y lleva una lista de scopes. La clave pública del portal ya no sirve en la v1: usa siempre la pk_ de una API key.

⚠️

El sk_ nunca va en el navegador

Un sk_ enviado desde un navegador se rechaza con 403 sin comprobarlo. Si llegó a verse en uno, considéralo expuesto y pide otra key.

Pedir el token

MétodoRutaScopeQué hace
POST/api/v1/auth/tokenNinguno (público)Devuelve un JWT con los scopes de la key o de un subconjunto que pidas. 120 peticiones por minuto e IP
Campo del cuerpo JSONObligatorioDescripción
public_keySíLa pk_ de la API key
api_keyNoEl sk_ de la misma key. Solo desde servidor; hace falta para portal:admin
scopesNoSubconjunto de los scopes de la key. Sin él, el token lleva todos menos portal:admin

El Content-Type debe ser application/json. Desde tu servidor:

curl
curl -X POST https://limbo.lefebvre.es/api/v1/auth/token \
  -H "Content-Type: application/json" \
  -d '{
    "public_key": "pk_tu_clave_publica",
    "api_key": "sk_...",
    "scopes": ["asset:read", "asset:write"]
  }'
200
{
  "success": true,
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIs...",
    "token_type": "Bearer",
    "expires_in": 3600,
    "scopes": ["asset:read", "asset:write"],
    "portal": {
      "id": "7b0c2f1e-5a3d-4c9b-8e21-6d4f90a1c3b2",
      "name": "Mi portal",
      "public_key": "pk_tu_clave_publica"
    },
    "policies": {
      "format_policy": { "allowed_output_formats": ["webp", "jpg", "png"] },
      "presets_aspect": [],
      "presets_size": [],
      "allow_arbitrary": true,
      "cross_access_policy": "own"
    }
  }
}

La respuesta lleva Cache-Control: no-store; no la caches. portal.public_key es la de la key, no la del portal. expires_in son 3600 s (60 minutos) salvo que la API key tenga otra duración (entre 60 y 86400 s).

Pedirlo desde tu backend

PHP
$ch = curl_init('https://limbo.lefebvre.es/api/v1/auth/token');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode([
        'public_key' => getenv('LIMBO_PUBLIC_KEY'),
        'api_key' => getenv('LIMBO_SECRET_KEY'),
    ]),
    CURLOPT_RETURNTRANSFER => true,
]);
$body = json_decode(curl_exec($ch), true);
$jwt = $body['data']['token'] ?? throw new RuntimeException($body['detail'] ?? 'Limbo token error');
Node
const res = await fetch('https://limbo.lefebvre.es/api/v1/auth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    public_key: process.env.LIMBO_PUBLIC_KEY,
    api_key: process.env.LIMBO_SECRET_KEY,
  }),
});
const body = await res.json();
if (!res.ok) throw new Error(body.detail);
const jwt = body.data.token;

Para el navegador, tu backend devuelve solo el JWT y el componente lo pide con tokenProvider (configuración del componente).

Usar el token

Envíalo en cada petición. Cuando caduque (401), pide otro:

curl
curl https://limbo.lefebvre.es/api/v1/assets \
  -H "Authorization: Bearer $JWT"

Scopes

El token solo puede lo que dicen sus scopes; sin el scope necesario la respuesta es 403.

ScopeLo piden
asset:readListar y leer assets, tags, portals/me/cross-portal-access; también sirve para listar carpetas
asset:writeSubir, editar y restaurar assets; variants/from-source
asset:deleteBorrar assets
variant:readListar variantes de un asset
variant:writeGenerar, subir y editar variantes
variant:deleteBorrar variantes
folder:readListar carpetas y sus assets
folder:writeCrear, editar y borrar carpetas
portal:readportals/me, listar presets, métricas
portal:adminCrear, editar y borrar presets; external/portals/{slug}/refresh
ai:read / ai:generateServicios y parámetros de IA / generar con IA (imagen y vídeo)
stock:search / stock:downloadBuscar en bancos de imágenes / descargar un resultado
external:readLeer contenido de portales externos

portal:admin

Es el único scope que no se concede por defecto. Para obtenerlo pídelo en scopes y envía el api_key (sk_) de esa key; si falta, 403 portal_admin_requires_api_key. Pedir un scope que la key no tiene es un 422.

Navegador, Origin y sk_

  • Si la petición lleva cabecera Origin y envías api_key: 403 sk_not_allowed_from_browser, sin mirar la clave.
  • Si el Origin no está en los allowedOrigins del portal: 403 origin_not_allowed.
  • Una petición sin Origin (tu servidor) no se comprueba contra allowedOrigins.

Errores propios del token

El código de máquina es el prefijo de detail.

Estadodetail empieza porCausa
400Invalid JSON payload.JSON inválido o cuya raíz no es un objeto
401invalid_credentialspk_ desconocida (incluida la del portal), key revocada, portal inactivo o api_key incorrecta; no se distinguen entre sí
403origin_not_allowed, sk_not_allowed_from_browser, portal_admin_requires_api_keyVer las secciones anteriores
415Supported content typeFalta Content-Type: application/json
422The request contains invalid parameters.Cuerpo inválido, o scopes que la key no tiene (con errors[])
429Rate limit exceededMás de 120 peticiones por minuto desde la misma IP; espera Retry-After segundos

Los schemas de las respuestas están en la referencia OpenAPI. Siguiente paso: el inicio rápido.