Endpoints Principales

Subir: POST /api/assets
Listar: GET /api/assets
Obtener: GET /api/assets/{id}
Eliminar: DELETE /api/assets/{id}
Variantes: GET /api/assets/{id}/variants
Generar Variante: POST /api/assets/{id}/variants

📤 Subir Assets

Sube imágenes con metadatos automáticos y optimización en tiempo real:

cURL
curl -X POST https://limbo.lefebvre.es/api/assets \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
  -F "file=@/path/to/image.jpg" \
  -F "tags=producto,principal,hero" \
  -F "alt_text=Producto destacado del mes" \
  -F "force_webp=true" \
  -F "generate_variants=true"
JavaScript
async function uploadAsset(file, metadata = {}) {
    const formData = new FormData();
    formData.append('file', file);
    
    // Metadata opcional
    if (metadata.tags) formData.append('tags', metadata.tags);
    if (metadata.alt_text) formData.append('alt_text', metadata.alt_text);
    if (metadata.force_webp !== undefined) formData.append('force_webp', metadata.force_webp);
    if (metadata.generate_variants !== undefined) formData.append('generate_variants', metadata.generate_variants);

    try {
        const response = await fetch('/api/assets', {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${await getAuthToken()}`
            },
            body: formData
        });

        if (!response.ok) {
            throw new Error(`Upload failed: ${response.statusText}`);
        }

        const result = await response.json();
        console.log('Asset uploaded:', result);
        return result;
        
    } catch (error) {
        console.error('Upload error:', error);
        throw error;
    }
}

// Uso con drag & drop
const dropZone = document.getElementById('drop-zone');

dropZone.addEventListener('drop', async (e) => {
    e.preventDefault();
    const files = Array.from(e.dataTransfer.files);
    
    for (const file of files) {
        if (file.type.startsWith('image/')) {
            try {
                const result = await uploadAsset(file, {
                    title: file.name.split('.')[0],
                    is_public: true,
                    tags: 'galeria,nuevo'
                });
                
                // Actualizar UI
                addAssetToGallery(result.data);
                
            } catch (error) {
                showErrorNotification(`Error subiendo ${file.name}: ${error.message}`);
            }
        }
    }
});
PHP
httpClient = $httpClient;
        $this->authService = $authService;
        $this->apiUrl = $apiUrl;
    }

    public function uploadAsset(
        string $filePath,
        array $metadata = []
    ): array {
        $token = $this->authService->getValidToken();
        
        $formData = [
            'file' => DataPart::fromPath($filePath)
        ];
        
        // Agregar metadata
        if (isset($metadata['title'])) {
            $formData['title'] = $metadata['title'];
        }
        if (isset($metadata['description'])) {
            $formData['description'] = $metadata['description'];
        }
        if (isset($metadata['tags'])) {
            $formData['tags'] = $metadata['tags'];
        }
        if (isset($metadata['is_public'])) {
            $formData['is_public'] = $metadata['is_public'] ? 'true' : 'false';
        }

        $formDataPart = new FormDataPart($formData);

        $response = $this->httpClient->request('POST', $this->apiUrl . '/assets', [
            'headers' => [
                'Authorization' => 'Bearer ' . $token,
            ] + $formDataPart->getPreparedHeaders()->toArray(),
            'body' => $formDataPart->bodyToIterable()
        ]);

        if ($response->getStatusCode() !== 201) {
            throw new \Exception('Asset upload failed: ' . $response->getContent(false));
        }

        return $response->toArray();
    }

    public function uploadFromUrl(string $imageUrl, array $metadata = []): array
    {
        // Descargar imagen
        $tempFile = tempnam(sys_get_temp_dir(), 'limbo_asset_');
        file_put_contents($tempFile, file_get_contents($imageUrl));
        
        try {
            $result = $this->uploadAsset($tempFile, $metadata);
            return $result;
        } finally {
            unlink($tempFile);
        }
    }
}

// Uso
$assetService = new LimboAssetService($httpClient, $authService, $apiUrl);

try {
    $result = $assetService->uploadAsset('/path/to/image.jpg', [
        'title' => 'Producto Hero',
        'description' => 'Imagen principal del producto',
        'tags' => 'producto,hero,principal',
        'is_public' => true
    ]);
    
    echo "Asset subido: " . $result['data']['id'] . "\n";
    echo "URL: " . $result['data']['url'] . "\n";
    
} catch (\Exception $e) {
    echo "Error: " . $e->getMessage() . "\n";
}

💡 Formatos Soportados

  • Imágenes: JPEG, PNG, GIF, WebP, SVG, TIFF
  • Tamaño máximo: 25MB por archivo
  • Resolución máxima: 8K (7680×4320)

📋 Respuesta de Upload

Después de subir un asset, recibirás información completa:

JSON Response
{
  "success": true,
  "message": "Asset uploaded successfully",
  "data": {
    "id": "64f7b1a2c8e1234567890abcd",
    "filename": "producto-hero.jpg",
    "original_filename": "DSC_0001.jpg",
    "title": "Producto Hero",
    "description": "Imagen principal del producto",
    "mime_type": "image/jpeg",
    "size": 2048576,
    "dimensions": {
      "width": 1920,
      "height": 1080
    },
    "url": "https://limbo.lefebvre.es/assets/64f7b1a2c8e1234567890abcd/view",
    "thumbnail_url": "https://limbo.lefebvre.es/assets/64f7b1a2c8e1234567890abcd/thumbnail",
    "download_url": "https://limbo.lefebvre.es/assets/64f7b1a2c8e1234567890abcd/download",
    "tags": ["producto", "hero", "principal"],
    "is_public": true,
    "metadata": {
      "exif": {
        "camera": "Canon EOS R5",
        "lens": "RF 24-70mm F2.8 L IS USM",
        "focal_length": "50mm",
        "aperture": "f/2.8",
        "iso": 400,
        "datetime": "2024-01-15T14:30:22Z"
      },
      "colors": {
        "dominant": "#2C3E50",
        "palette": ["#2C3E50", "#3498DB", "#E74C3C", "#F39C12"]
      },
      "analysis": {
        "faces": 0,
        "objects": ["product", "background"],
        "quality_score": 0.95
      }
    },
    "portal_id": "your-portal-id",
    "created_at": "2024-01-15T14:35:18Z",
    "updated_at": "2024-01-15T14:35:18Z"
  }
}

Campos de la Respuesta

📁 Información Básica

  • id - Identificador único del asset
  • filename - Nombre procesado del archivo
  • original_filename - Nombre original
  • mime_type - Tipo MIME del archivo
  • size - Tamaño en bytes

🖼️ URLs de Acceso

  • url - Vista optimizada de la imagen
  • thumbnail_url - Miniatura (200x200)
  • download_url - Descarga del archivo original

🎨 Análisis Automático

  • metadata.exif - Datos EXIF de la cámara
  • metadata.colors - Paleta de colores extraída
  • metadata.analysis - Detección de objetos y caras

📋 Listar Assets

Obtén una lista paginada de todos tus assets con filtros y ordenamiento:

cURL
# Listar assets con paginación
curl -X GET "https://limbo.lefebvre.es/api/assets?page=1&limit=20" \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
  -H "Content-Type: application/json"

# Filtrar por nombre y fecha
curl -X GET "https://limbo.lefebvre.es/api/assets?name=producto&dateFrom=2025-01-01&status=active" \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
  -H "Content-Type: application/json"

Parámetros de Consulta

page
integer
Número de página (default: 1)
limit
integer
Assets por página (1-100, default: 20)
name
string
Filtrar por nombre de archivo (búsqueda parcial)
uploadedBy
string
Filtrar por identificador del usuario que subió la imagen
dateFrom
date
Filtrar desde fecha (formato: YYYY-MM-DD)
dateTo
date
Filtrar hasta fecha (formato: YYYY-MM-DD)
status
string
Filtrar por estado: active, processing, error, deleted
portalId
uuid
Filtrar por portal (solo para acceso cross-portal)

Respuesta de Lista

JSON Response
{
  "success": true,
  "data": {
    "assets": [
      {
        "id": "64f7b1a2c8e1234567890abcd",
        "filename": "producto-hero.jpg",
        "title": "Producto Hero",
        "description": "Imagen principal del producto",
        "url": "https://limbo.lefebvre.es/assets/64f7b1a2c8e1234567890abcd/view",
        "thumbnail_url": "https://limbo.lefebvre.es/assets/64f7b1a2c8e1234567890abcd/thumbnail",
        "mime_type": "image/jpeg",
        "size": 2048576,
        "dimensions": {
          "width": 1920,
          "height": 1080
        },
        "tags": ["producto", "hero", "principal"],
        "is_public": true,
        "created_at": "2024-01-15T14:35:18Z"
      }
      // ... más assets
    ],
    "pagination": {
      "current_page": 1,
      "total_pages": 5,
      "total_items": 94,
      "items_per_page": 20,
      "has_next": true,
      "has_previous": false
    }
  }
}

🔍 Detalles de Asset

Obtén información completa de un asset específico:

cURL
curl -X GET https://limbo.lefebvre.es/api/assets/01234567-89ab-cdef-0123-456789abcdef \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
  -H "Content-Type: application/json"
JavaScript
curl -X GET https://limbo.lefebvre.es/api/assets/01234567-89ab-cdef-0123-456789abcdef \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
  -H "Content-Type: application/json"</code></pre>
                </div>

                <div class="tabs-container">
                    <div class="tabs-header">
                        <button class="tab-btn active" data-tab="js-detail">JavaScript</button>
                        <button class="tab-btn" data-tab="php-detail">PHP</button>
                    </div>
					
                    <div class="tab-content active" id="js-detail">
                        <div class="code-block">
                            <div class="code-header">
                                <span class="code-lang">JavaScript</span>
                                <button class="copy-btn" data-copy="js-detail-code">Copiar</button>
                            </div>
                            <pre class="docs-code-block"><code class="docs-code-inline" id="js-detail-code">async function getAssetDetails(assetId) {
    try {
        const response = await fetch(`/api/assets/${assetId}`, {
            headers: {
                'Authorization': `Bearer ${await getAuthToken()}`,
                'Content-Type': 'application/json'
            }
        });

        if (!response.ok) {
            throw new Error(`Failed to get asset: ${response.statusText}`);
        }

        const result = await response.json();
        return result.data;

    } catch (error) {
        console.error('Error getting asset details:', error);
        throw error;
    }
}

// Crear preview del asset
function createAssetPreview(asset) {
    const preview = document.createElement('div');
    preview.className = 'asset-preview';
    
    preview.innerHTML = `
        <div class="asset-image">
            <img src="${asset.url}" alt="${asset.title}" loading="lazy">
        </div>
        <div class="asset-info">
            <h3>${asset.title}</h3>
            <p>${asset.description}</p>
            <div class="asset-meta">
                <span class="asset-size">${formatFileSize(asset.size)}</span>
                <span class="asset-dimensions">${asset.dimensions.width}×${asset.dimensions.height}</span>
                <span class="asset-date">${formatDate(asset.created_at)}</span>
            </div>
            <div class="asset-tags">
                ${asset.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
            </div>
        </div>
    `;
    
    return preview;
}
PHP
public function getAssetDetails(string $assetId): array
{
    $token = $this->authService->getValidToken();
    
    $response = $this->httpClient->request('GET', $this->apiUrl . '/assets/' . $assetId, [
        'headers' => [
            'Authorization' => 'Bearer ' . $token,
            'Content-Type' => 'application/json'
        ]
    ]);

    if ($response->getStatusCode() !== 200) {
        throw new \Exception('Failed to get asset details: ' . $response->getContent(false));
    }

    return $response->toArray();
}

public function renderAssetGallery(array $assets): string
{
    $html = '<div class="assets-gallery">';
    
    foreach ($assets as $asset) {
        $html .= sprintf(
            '<div class="asset-card" data-id="%s">
                <div class="asset-thumbnail">
                    <img src="%s" alt="%s" loading="lazy">
                </div>
                <div class="asset-details">
                    <h4>%s</h4>
                    <p>%s</p>
                    <div class="asset-meta">
                        <span>%s</span>
                        <span>%dx%d</span>
                    </div>
                    <div class="asset-actions">
                        <a href="%s" target="_blank" class="btn btn-sm">Ver</a>
                        <a href="%s" download class="btn btn-sm">Descargar</a>
                    </div>
                </div>
            </div>',
            htmlspecialchars($asset['id']),
            htmlspecialchars($asset['thumbnail_url']),
            htmlspecialchars($asset['title']),
            htmlspecialchars($asset['title']),
            htmlspecialchars($asset['description']),
            $this->formatFileSize($asset['size']),
            $asset['dimensions']['width'],
            $asset['dimensions']['height'],
            htmlspecialchars($asset['url']),
            htmlspecialchars($asset['download_url'])
        );
    }
    
    $html .= '</div>';
    return $html;
}

✏️ Actualizar Assets

Modifica metadata de assets existentes:

cURL
curl -X PUT https://limbo.lefebvre.es/assets/64f7b1a2c8e1234567890abcd \
  -H "Authorization: Bearer your-jwt-token" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Nuevo Título",
    "description": "Nueva descripción actualizada",
    "tags": "actualizado,nuevo,metadata",
    "is_public": false
  }'
⚠️

Limitaciones de Actualización

Solo se puede actualizar metadata (título, descripción, tags, visibilidad). Para cambiar el archivo, sube un nuevo asset y elimina el anterior.

🗑️ Eliminar Assets

Elimina permanentemente assets de tu biblioteca:

cURL
curl -X DELETE https://limbo.lefebvre.es/api/assets/01234567-89ab-cdef-0123-456789abcdef \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
JavaScript
async function deleteAsset(assetId) {
    if (!confirm('¿Estás seguro de eliminar este asset? Esta acción no se puede deshacer.')) {
        return false;
    }

    try {
        const response = await fetch(`/api/assets/${assetId}`, {
            method: 'DELETE',
            headers: {
                'Authorization': `Bearer ${await getAuthToken()}`
            }
        });

        if (!response.ok) {
            throw new Error(`Delete failed: ${response.statusText}`);
        }

        // Remover del DOM
        const assetElement = document.querySelector(`[data-asset-id="${assetId}"]`);
        if (assetElement) {
            assetElement.remove();
        }

        showSuccessNotification('Asset eliminado correctamente');
        return true;

    } catch (error) {
        console.error('Error deleting asset:', error);
        showErrorNotification('Error al eliminar el asset');
        throw error;
    }
}
🚨

Eliminación Permanente

La eliminación es irreversible. Todos los archivos asociados (original, variantes, thumbnails) se eliminarán permanentemente.

🎯 Casos de Uso Comunes

🖼️

Galería de Imágenes

Implementa galerías dinámicas con lazy loading, filtros por tags y búsqueda.

Frontend UX
📱

Upload Móvil

Sube fotos desde dispositivos móviles con compresión automática.

Mobile PWA
🤖

Automatización

Procesa lotes de imágenes con scripts automáticos y workflows.

Automation Batch
🎨

CMS Integration

Integra como biblioteca de medios en tu sistema de gestión de contenidos.

CMS Integration

🚀 Próximos Pasos