Search & Filters Flow

Build a dynamic product search with filters, sorting, and pagination.

What you'll build

A search experience powered by the v2 filter catalog: load all available filter dimensions, perform a text search, combine multiple filters (category + type + price range), sort results by price, and paginate through the result set.

Prerequisites

  • A Store UUID — e.g. e87437f2-3e35-4738-af5e-6307e368255c (staging)
  • cURL or any HTTP client

No authentication is required. All endpoints in this flow use the jwt_optional_authenticated pipeline.


Step 1: Load the Filter Catalog (v2)

The v2 filters endpoint returns everything you need to build a filter UI: price ranges, weight options, THC/CBD ranges, on-sale count, and relationships to types, categories, and brands (sideloaded in included).

cURL

curl -X GET https://ecom-api.staging.blaze.me/api/v2/products/filters \
  -H "Content-Type: application/vnd.api+json" \
  -H "Accept: application/vnd.api+json" \
  -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c"

JavaScript (fetch)

const STORE_UUID = "e87437f2-3e35-4738-af5e-6307e368255c";
const BASE_URL = "https://ecom-api.staging.blaze.me";
const headers = {
  "Content-Type": "application/vnd.api+json",
  "Accept": "application/vnd.api+json",
  "X-Store": STORE_UUID,
};

const filterRes = await fetch(`${BASE_URL}/api/v2/products/filters`, { headers });
const { data: filters, included } = await filterRes.json();

// Price range — use for a slider
const { min, max } = filters.attributes.price_ranges;
console.log(`Price: $${min} – $${max}`);

// Weights — use for weight-picker buttons
console.log("Weights:", filters.attributes.weights); // [1.0, 3.5, 7.0, 14.0, 28.0]

// THC/CBD ranges — use for range sliders
console.log("THC:", filters.attributes.thc_ranges);   // { min: 0, max: 35, unit: "%" }
console.log("CBD:", filters.attributes.cbd_ranges);   // { min: 0, max: 20, unit: "%" }

// On-sale badge
console.log(`${filters.attributes.on_sale.count} products on sale`);

// Types, categories, brands from included
const types = included.filter((r) => r.type === "product_types");
const categories = included.filter((r) => r.type === "product_categories");
const brands = included.filter((r) => r.type === "product_brands");

console.log(`${types.length} types, ${categories.length} categories, ${brands.length} brands`);

Response

{
  "data": {
    "id": "filters",
    "type": "filters",
    "attributes": {
      "price_ranges": { "min": 5.0, "max": 120.0 },
      "weights": [1.0, 3.5, 7.0, 14.0, 28.0],
      "thc_ranges": { "min": 0, "max": 35, "unit": "%" },
      "cbd_ranges": { "min": 0, "max": 20, "unit": "%" },
      "on_sale": { "count": 14 }
    },
    "relationships": {
      "types": {
        "data": [
          { "id": "Hybrid", "type": "product_types" },
          { "id": "Indica", "type": "product_types" },
          { "id": "Sativa", "type": "product_types" }
        ]
      },
      "categories": {
        "data": [
          { "id": "cat-flower-001", "type": "product_categories" }
        ]
      },
      "brands": {
        "data": [
          { "id": "brand-001", "type": "product_brands" }
        ]
      }
    }
  },
  "included": [
    {
      "id": "Hybrid",
      "type": "product_types",
      "attributes": { "name": "Hybrid", "count": 25 }
    },
    {
      "id": "Indica",
      "type": "product_types",
      "attributes": { "name": "Indica", "count": 18 }
    },
    {
      "id": "Sativa",
      "type": "product_types",
      "attributes": { "name": "Sativa", "count": 12 }
    }
  ]
}

v2 vs v1: The v2 filter catalog adds weights, thc_ranges, cbd_ranges, on_sale, and sideloads related resources in included. Prefer v2 for building filter UIs.


Step 2: Search by Text

The user types "blue dream" in the search bar.

cURL

curl -X GET "https://ecom-api.staging.blaze.me/api/v1/products?search=blue+dream&limit=10" \
  -H "Content-Type: application/vnd.api+json" \
  -H "Accept: application/vnd.api+json" \
  -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c"

JavaScript (fetch)

const searchQuery = "blue dream";

const searchParams = new URLSearchParams({
  search: searchQuery,
  limit: "10",
});

const searchRes = await fetch(`${BASE_URL}/api/v1/products?${searchParams}`, { headers });
const { data: results, meta } = await searchRes.json();

console.log(`"${searchQuery}" → ${meta.total} results`);
results.forEach((p) => {
  console.log(`  ${p.attributes.name} — $${p.attributes.unit_price}`);
});

Response

{
  "data": [
    {
      "id": "b5c8d1e2-f3a4-5678-9012-abcdef123456",
      "type": "store_products",
      "attributes": {
        "name": "Blue Dream",
        "slug": "blue-dream",
        "type": "Hybrid",
        "in_stock": true,
        "unit_price": 45.00
      }
    }
  ],
  "meta": {
    "total": 3,
    "limit": 10,
    "offset": 0
  }
}

Step 3: Combine Multiple Filters

The user narrows results: category = flower, type = Hybrid, price $20–$60.

cURL

curl -X GET "https://ecom-api.staging.blaze.me/api/v1/products?category=flower&product_type=Hybrid&min_price=20&max_price=60&limit=10" \
  -H "Content-Type: application/vnd.api+json" \
  -H "Accept: application/vnd.api+json" \
  -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c"

JavaScript (fetch)

const filterParams = new URLSearchParams({
  category: "flower",
  product_type: "Hybrid",
  min_price: "20",
  max_price: "60",
  limit: "10",
});

const filteredRes = await fetch(`${BASE_URL}/api/v1/products?${filterParams}`, { headers });
const { data: filtered, meta: filteredMeta } = await filteredRes.json();

console.log(`Flower + Hybrid + $20–$60 → ${filteredMeta.total} products`);

Response

{
  "data": [
    {
      "id": "b5c8d1e2-f3a4-5678-9012-abcdef123456",
      "type": "store_products",
      "attributes": {
        "name": "Blue Dream",
        "slug": "blue-dream",
        "type": "Hybrid",
        "in_stock": true,
        "unit_price": 45.00
      }
    },
    {
      "id": "a4d7c9e3-b2f1-4a56-8901-fedcba654321",
      "type": "store_products",
      "attributes": {
        "name": "Wedding Cake",
        "slug": "wedding-cake",
        "type": "Hybrid",
        "in_stock": true,
        "unit_price": 55.00
      }
    }
  ],
  "meta": {
    "total": 12,
    "limit": 10,
    "offset": 0
  }
}

Filter parameters: category, subcategory, product_type, brand, tags, min_price, max_price, and search can all be combined freely.


Step 4: Sort Results

Add sorting to show cheapest products first.

cURL

curl -X GET "https://ecom-api.staging.blaze.me/api/v1/products?category=flower&product_type=Hybrid&min_price=20&max_price=60&order=price_asc&limit=10" \
  -H "Content-Type: application/vnd.api+json" \
  -H "Accept: application/vnd.api+json" \
  -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c"

JavaScript (fetch)

const sortedParams = new URLSearchParams({
  category: "flower",
  product_type: "Hybrid",
  min_price: "20",
  max_price: "60",
  order: "price_asc",
  limit: "10",
});

const sortedRes = await fetch(`${BASE_URL}/api/v1/products?${sortedParams}`, { headers });
const { data: sorted } = await sortedRes.json();

sorted.forEach((p) => {
  console.log(`$${p.attributes.unit_price}${p.attributes.name}`);
});
// $25.00 — Girl Scout Cookies
// $35.00 — Gelato
// $45.00 — Blue Dream
// ...

Sort options: name_asc, name_desc, price_asc, price_desc. The exact set of supported values depends on the store configuration.


Step 5: Paginate Through Results

Navigate through larger result sets page by page.

cURL

# Page 1
curl -X GET "https://ecom-api.staging.blaze.me/api/v1/products?category=flower&order=price_asc&limit=5&offset=0" \
  -H "Content-Type: application/vnd.api+json" \
  -H "Accept: application/vnd.api+json" \
  -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c"

# Page 2
curl -X GET "https://ecom-api.staging.blaze.me/api/v1/products?category=flower&order=price_asc&limit=5&offset=5" \
  -H "Content-Type: application/vnd.api+json" \
  -H "Accept: application/vnd.api+json" \
  -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c"

JavaScript (fetch)

async function searchProducts({ category, type, minPrice, maxPrice, order, search, page = 1, pageSize = 10 }) {
  const params = new URLSearchParams();
  if (category)  params.set("category", category);
  if (type)      params.set("product_type", type);
  if (minPrice)  params.set("min_price", String(minPrice));
  if (maxPrice)  params.set("max_price", String(maxPrice));
  if (order)     params.set("order", order);
  if (search)    params.set("search", search);
  params.set("limit", String(pageSize));
  params.set("offset", String((page - 1) * pageSize));

  const res = await fetch(`${BASE_URL}/api/v1/products?${params}`, { headers });
  const { data, meta } = await res.json();

  return {
    products: data,
    total: meta.total,
    page,
    pageSize,
    totalPages: Math.ceil(meta.total / pageSize),
  };
}

// Usage
const page1 = await searchProducts({
  category: "flower",
  type: "Hybrid",
  minPrice: 20,
  maxPrice: 60,
  order: "price_asc",
  page: 1,
  pageSize: 5,
});

console.log(`Page ${page1.page} of ${page1.totalPages} (${page1.total} total)`);

// Load next page
if (page1.page < page1.totalPages) {
  const page2 = await searchProducts({
    category: "flower",
    type: "Hybrid",
    minPrice: 20,
    maxPrice: 60,
    order: "price_asc",
    page: 2,
    pageSize: 5,
  });
  console.log(`Page ${page2.page}: ${page2.products.length} products`);
}

Pagination response

{
  "meta": {
    "total": 48,
    "limit": 5,
    "offset": 0
  }
}

Pagination math: total_pages = ceil(meta.total / limit). Current page = (offset / limit) + 1. There are no more results when offset + limit >= total.


Putting It All Together

const STORE_UUID = "e87437f2-3e35-4738-af5e-6307e368255c";
const BASE_URL = "https://ecom-api.staging.blaze.me";
const headers = {
  "Content-Type": "application/vnd.api+json",
  "Accept": "application/vnd.api+json",
  "X-Store": STORE_UUID,
};

// 1. Load filter catalog to build the UI
const { data: filters, included } = await (
  await fetch(`${BASE_URL}/api/v2/products/filters`, { headers })
).json();

const types = included.filter((r) => r.type === "product_types");
console.log("Available types:", types.map((t) => `${t.attributes.name} (${t.attributes.count})`));
console.log(`Price: $${filters.attributes.price_ranges.min}–$${filters.attributes.price_ranges.max}`);

// 2. User searches for "gummies"
const { data: searchResults, meta: searchMeta } = await (
  await fetch(`${BASE_URL}/api/v1/products?search=gummies&limit=10`, { headers })
).json();
console.log(`Search "gummies" → ${searchMeta.total} results`);

// 3. User applies filters: Edibles + $10–$30 + sorted by price
const filterParams = new URLSearchParams({
  search: "gummies",
  category: "edibles",
  min_price: "10",
  max_price: "30",
  order: "price_asc",
  limit: "10",
});
const { data: filtered, meta: filteredMeta } = await (
  await fetch(`${BASE_URL}/api/v1/products?${filterParams}`, { headers })
).json();
console.log(`Filtered → ${filteredMeta.total} results`);
filtered.forEach((p) => {
  console.log(`  $${p.attributes.unit_price}${p.attributes.name}`);
});

What's Next?