# Brands

Three endpoints, in the order you'll use them: **suggest** while the shopper types, **search** once
they've finished, **products** to show the catalogue.

## Autocomplete — `/v1/brands/suggest`

Call this on every keystroke.

```bash
curl "https://api.ziffi.xyz/v1/brands/suggest?q=yelow+chimes" \
  -H "X-Ziffi-Key: $ZIFFI_KEY"
```

```json
{
  "data": [
    { "name": "Yellow Chimes", "slug": "yellow-chimes",
      "logo_url": "https://…", "product_count": 10337 }
  ],
  "meta": { "count": 1, "query": { "q": "yelow chimes" } },
  "tier": "graph"
}
```

It forgives the things people actually get wrong — real results from the live catalogue:

| typed | resolves to | why |
|---|---|---|
| `liba` | Libas | prefix |
| `man company` | The Man Company | matches a later word, not just the start |
| `dot key` | Dot & Key | `&` is invisible to the match |
| `dot and key` | Dot & Key | `and` and `&` are the same thing |
| `fast up` | Fast&Up | same, no space needed |
| `neemans` | Neeman's | apostrophes ignored |
| `yelow chimes` | Yellow Chimes | typo |
| `swis beauty` | Swiss Beauty | typo |
| `mamaerth` | mamaearth | typo |

Ranking is exact → prefix → word-prefix → contains → fuzzy, with catalogue size breaking ties, so
the brand a shopper more likely meant comes first.

| Parameter | Default | Notes |
|---|---|---|
| `q` | required | 1–64 chars. Under 2 characters after normalisation returns nothing. |
| `limit` | 8 | max 20 |

Under four characters the fuzzy tier switches off and only the prefix tiers answer — at two or three
letters a similarity score is mostly noise and would bury the brand you're actually typing.

Typically ~15 ms server-side. It never touches the vector index; see *Why this isn't semantic
search* below.

## Resolve a full name — `/v1/brands/search`

Same matching family, but for a name the shopper has finished typing, and it returns `domain` too.

```bash
curl "https://api.ziffi.xyz/v1/brands/search?q=libas" -H "X-Ziffi-Key: $ZIFFI_KEY"
```

| Parameter | Default | Notes |
|---|---|---|
| `q` | required | 2–64 characters |
| `limit` | 20 | max 50 |

Use `suggest` for typeahead and `search` when you have a complete name — the payload is smaller and
single-character input is rejected rather than scanned.

## The brand — `/v1/brands/{slug}`

```bash
curl "https://api.ziffi.xyz/v1/brands/yellow-chimes" -H "X-Ziffi-Key: $ZIFFI_KEY"
```

```json
{ "data": { "slug": "yellow-chimes", "name": "Yellow Chimes", "domain": "yellowchimes.com",
            "platform": "shopify", "logo_url": "https://…", "currency": "INR",
            "product_count": 10337, "redirected": false },
  "tier": "graph" }
```

**`redirected`** tells you the slug you asked for is an old one. Some brands were stored twice under
different slugs and have since been merged; the retired slug still resolves to the surviving brand
and sets `redirected: true`, so links published earlier keep working. Persist the returned `slug`,
not the one you asked with.

## The catalogue — `/v1/brands/{slug}/products`

```bash
curl "https://api.ziffi.xyz/v1/brands/yellow-chimes/products?limit=24&sort=rating" \
  -H "X-Ziffi-Key: $ZIFFI_KEY"
```

```json
{ "data": [ /* product cards */ ],
  "meta": { "limit": 24, "offset": 0, "count": 24, "total": 10337,
            "next_cursor": "b2Zmc2V0OjI0", "status": "active", "sort": "rating" },
  "tier": "graph" }
```

Page with `next_cursor` (or a raw `offset`) — see [Pagination](#/pagination). `total` is the count
for the selected `status`, so you can render "10,337 products" before fetching anything else.

| Parameter | Default | Options |
|---|---|---|
| `limit` | 24 | 1–100 |
| `offset` / `cursor` | 0 | cursor wins when both are sent |
| `sort` | `rating` | `rating` · `newest` · `price_asc` · `price_desc` · `relevance` |
| `status` | `active` | `active` · `draft` · `archived` · `duplicate_merged` |

### What the statuses mean

| status | meaning |
|---|---|
| `active` | Servable. This is what search, browse and recommendations return. |
| `draft` | Flagged during ingestion from the handle pattern — `-copy`, `-v2` and similar. Usually a merchant's working duplicate. |
| `archived` | The merchant delisted it: present in an earlier capture, absent from the latest one. Soft-deleted, and it un-archives automatically if it reappears. |
| `duplicate_merged` | A listing retired when two brand records for one store were merged. |

Nothing is ever hard-deleted, which is why you can inspect all four.

## Why brand matching isn't semantic search

A brand name is a stored token, not a concept — there is nothing to infer from "Libas". A vector
index physically cannot help: products are indexed as a graph of *similarity*, so there is no
"Libas region" to navigate to, and filtering a semantic result set down to one brand leaves almost
nothing because any single brand is a fraction of a percent of the catalogue.

So brand matching is answered in SQL, where it is exact and fast, and the vector index is reserved
for product meaning.

## For agents

```json
{"jsonrpc":"2.0","id":1,"method":"tools/call",
 "params":{"name":"suggest_brands","arguments":{"q":"yelow chimes"}}}
```

The natural sequence: `suggest_brands` or `search_brands` → take the `slug` → `brand_products` to
browse it, or pass it as `brand` to `search_products` to search within it. Always pass the **slug**,
never the display name.

## Next steps

- [Search products](#/search) — the main product search.
- [Recommendations](#/recommend) — related and cross-brand items.
- [Pagination](#/pagination) — how `next_cursor` works.
