> ## Documentation Index
> Fetch the complete documentation index at: https://docs.alana.shopping/llms.txt
> Use this file to discover all available pages before exploring further.

# Preview Catalog

> GET /api/hub/catalogs/{catalogId}/preview — view the first 10 products and summary stats without cloning.

## Endpoint

```
GET /api/hub/catalogs/{catalogId}/preview
```

Returns the first 10 products from a published Hub catalog, plus aggregate summary statistics. This endpoint does not require authentication and does not require a clone or subscription — it's designed for evaluating a catalog before committing.

Returns `404` for private or unpublished catalogs.

***

## Path parameters

| Parameter   | Type   | Required | Description                                   |
| ----------- | ------ | :------: | --------------------------------------------- |
| `catalogId` | string |    Yes   | The Hub catalog ID (from the browse endpoint) |

***

## Response

```json theme={null}
{
  "catalogId": "hub_cat_9x8k2m",
  "name": "Spring 2026 Apparel",
  "products": [
    {
      "id": "prod_preview_1",
      "title": "Blue Linen Blazer",
      "sku": "BLZ-BLUE-M",
      "brand": "EuroStyle",
      "categoryPath": "Apparel > Outerwear > Blazers",
      "price": 89.99,
      "currency": "USD",
      "primaryImageUrl": "https://cdn.example.com/blz-blue-m.jpg",
      "score": 85,
      "availability": "in_stock"
    }
  ],
  "summary": {
    "total": 512,
    "avgScore": 78,
    "topCategories": [
      { "name": "Apparel > Tops", "count": 145 },
      { "name": "Apparel > Outerwear", "count": 98 },
      { "name": "Apparel > Bottoms", "count": 87 }
    ],
    "topBrands": [
      { "name": "EuroStyle", "count": 210 },
      { "name": "MediterraFashion", "count": 156 },
      { "name": "AlpinaWear", "count": 88 }
    ]
  }
}
```

### Product fields (preview)

| Field             | Type   | Description                                     |
| ----------------- | ------ | ----------------------------------------------- |
| `id`              | string | Stable product ID within this Hub catalog       |
| `title`           | string | Product name                                    |
| `sku`             | string | Stock-keeping unit                              |
| `brand`           | string | Brand name                                      |
| `categoryPath`    | string | Full category path                              |
| `price`           | number | Selling price                                   |
| `currency`        | string | ISO 4217 code                                   |
| `primaryImageUrl` | string | Main product image                              |
| `score`           | number | Optimization score (0–100)                      |
| `availability`    | string | `"in_stock"`, `"out_of_stock"`, or `"preorder"` |

### Summary fields

| Field           | Type   | Description                       |
| --------------- | ------ | --------------------------------- |
| `total`         | number | Total products in the catalog     |
| `avgScore`      | number | Average optimization score        |
| `topCategories` | array  | Top 5 categories by product count |
| `topBrands`     | array  | Top 5 brands by product count     |

***

## Examples

<CodeGroup>
  ```bash curl theme={null}
  # Preview a Hub catalog (no authentication required)
  curl "https://app.alana.shopping/api/hub/catalogs/hub_cat_9x8k2m/preview"
  ```

  ```javascript JavaScript theme={null}
  const catalogId = 'hub_cat_9x8k2m';

  // No Authorization header needed for preview
  const response = await fetch(
    `https://app.alana.shopping/api/hub/catalogs/${catalogId}/preview`
  );

  if (response.status === 404) {
    console.log('Catalog not found or is private');
    return;
  }

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  const { products, summary } = await response.json();

  console.log(`Catalog has ${summary.total} products`);
  console.log(`Average score: ${summary.avgScore}`);
  console.log('\nTop categories:');
  summary.topCategories.forEach(cat => {
    console.log(`  ${cat.name}: ${cat.count} products`);
  });

  console.log('\nFirst 10 products:');
  products.forEach(p => {
    console.log(`  ${p.title} (${p.sku}) — score: ${p.score}`);
  });
  ```

  ```python Python theme={null}
  import requests

  catalog_id = "hub_cat_9x8k2m"

  # No Authorization header needed for preview
  response = requests.get(
      f"https://app.alana.shopping/api/hub/catalogs/{catalog_id}/preview"
  )

  if response.status_code == 404:
      print("Catalog not found or is private")
  else:
      response.raise_for_status()
      data = response.json()
      summary = data["summary"]

      print(f"Catalog: {data['name']}")
      print(f"Total products: {summary['total']}")
      print(f"Average score: {summary['avgScore']}")
      print(f"\nTop brands: {[b['name'] for b in summary['topBrands']]}")
      print(f"\nFirst {len(data['products'])} products:")
      for p in data["products"]:
          print(f"  {p['title']} — ${p['price']} — score: {p['score']}")
  ```
</CodeGroup>

***

## Usage pattern

Preview is typically used before cloning or subscribing:

```mermaid theme={null}
graph LR
    A["Browse catalogs\nGET /hub/catalogs"] --> B["Open a result\nthat looks interesting"]
    B --> C["Preview it\nGET /hub/catalogs/{id}/preview"]
    C --> D{Good fit?}
    D -- yes --> E["Clone or Subscribe\nPOST /hub/catalogs/{id}/clone\nor /subscribe"]
    D -- no --> A
```

***

## Error responses

| HTTP status | Code                  | Description                                                 |
| ----------- | --------------------- | ----------------------------------------------------------- |
| 404         | `CATALOG_NOT_FOUND`   | Catalog does not exist, is private, or has been unpublished |
| 429         | `RATE_LIMIT_EXCEEDED` | 60 requests/minute for public endpoint                      |

<Note>
  Private catalogs always return 404 — the same response as non-existent catalogs. This prevents enumeration of private catalog IDs.
</Note>
