Skip to main content
POST
https://api.firmly.work
/
api
/
v1
/
discovery
/
search
curl --request POST \
  --url 'https://api.firmly.work/api/v1/discovery/search' \
  --header 'x-firmly-authorization: YOUR_AUTH_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{
    "query": "leather sofa"
  }'
{
  "products": [
    {
      "base_sku": "12345",
      "title": "Modern Leather Sofa",
      "handle": "modern-leather-sofa",
      "description": "A beautiful modern leather sofa perfect for any living room.",
      "pdp_url": "https://benchmademodern.com/products/modern-leather-sofa",
      "domain": "benchmademodern.com",
      "price_range": {
        "min": 249999,
        "max": 319999
      },
      "has_available_variants": true,
      "images": [
        {
          "url": "https://cdn.benchmademodern.com/images/modern-leather-sofa.jpg",
          "alt": "Modern Leather Sofa"
        }
      ],
      "variants": [],
      "variant_option_values": [],
      "tags": ["leather", "sofa", "modern", "living-room"]
    }
  ],
  "page_size": 20,
  "time_taken_ms": 45,
  "next_page": "https://api.firmly.work/api/v1/discovery/search?query=leather+sofa&page_size=20&cursor=eyJzY29yZSI6MTIuNSwiaWQiOiJhYmMxMjMifQ"
}
Search for products across merchant catalogs using keyword-based queries. Use filters to narrow results by domain, price, availability, and variant attributes.

Authentication

This endpoint requires authentication via the x-firmly-authorization header. See Browser Session Authentication for complete details on:
  • Obtaining and refreshing tokens
  • Token format and expiry
  • Required permissions
  • Device, App ID, and Developer authentication modes

Request Body

query
string
required
Search keyword(s). Required for all search requests.
filters
object
Optional filters to narrow search results.
page_size
number
default:"20"
Number of results per page. Maximum: 100

Response

products
array
Array of product objects matching the search criteria. Returns empty array [] when no results found (HTTP 200).
page_size
number
Number of results returned in this page
time_taken_ms
number
Time taken to execute the search query in milliseconds
next_page
string
Full URL to fetch the next page of results. Use this URL directly with your authorization header. null when there are no more results.

Error Responses

Status CodeErrorDescription
400InvalidRequestMissing required query parameter or invalid parameter values
401UnauthorizedMissing or invalid x-firmly-authorization header
401TokenExpiredAuthentication token has expired. Refresh and retry.
403ForbiddenToken does not have permission for this operation
429RateLimitExceededToo many requests. Check Retry-After header.
500ErrorStoreUnavailableSearch service is temporarily unavailable
503ServiceUnavailableSystem maintenance in progress
No results is not an error: When no products match your search criteria, the API returns HTTP 200 with an empty products array. This is standard REST behavior—404 is reserved for “resource not found” (e.g., invalid endpoints), not “no results found”.
curl --request POST \
  --url 'https://api.firmly.work/api/v1/discovery/search' \
  --header 'x-firmly-authorization: YOUR_AUTH_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{
    "query": "leather sofa"
  }'
{
  "products": [
    {
      "base_sku": "12345",
      "title": "Modern Leather Sofa",
      "handle": "modern-leather-sofa",
      "description": "A beautiful modern leather sofa perfect for any living room.",
      "pdp_url": "https://benchmademodern.com/products/modern-leather-sofa",
      "domain": "benchmademodern.com",
      "price_range": {
        "min": 249999,
        "max": 319999
      },
      "has_available_variants": true,
      "images": [
        {
          "url": "https://cdn.benchmademodern.com/images/modern-leather-sofa.jpg",
          "alt": "Modern Leather Sofa"
        }
      ],
      "variants": [],
      "variant_option_values": [],
      "tags": ["leather", "sofa", "modern", "living-room"]
    }
  ],
  "page_size": 20,
  "time_taken_ms": 45,
  "next_page": "https://api.firmly.work/api/v1/discovery/search?query=leather+sofa&page_size=20&cursor=eyJzY29yZSI6MTIuNSwiaWQiOiJhYmMxMjMifQ"
}

Usage Notes

Domain authorization: Search results are limited to merchant domains associated with your account. Requests for unauthorized domains will return empty results. Contact your account manager to request access to additional merchants.
Domain filtering: Use the filters.domains array to search specific merchants:
  • Single merchant: "domains": ["benchmademodern.com"]
  • Multiple merchants: "domains": ["merchant1.com", "merchant2.com", "merchant3.com"]
  • All merchants: omit the domains field entirely
Variant filtering: Use the filters.variants array to filter by product attributes like color, size, or material. Each variant filter specifies an option name and an array of allowed values. Use the Options endpoint to discover available options and values.
Filter matching: Variant filters use exact string matching. The values must match exactly as stored in the product data (e.g., "Blue" will not match "blue" or "Navy").
Pricing: All prices are integers in USD cents (not dollars). "min_price": 100000 filters for 1,000.00+,andpricerange.min:249999means1,000.00+, and `price_range.min: 249999` means 2,499.99 USD. To display: (price / 100).toFixed(2).
Data Types: price_range.min, price_range.max are always integers in cents (e.g., 249999 for $2,499.99). has_available_variants is always a boolean (true or false). These are never returned as strings.
Why cents? Integer cents avoid floating-point precision issues (e.g., 0.1 + 0.2 !== 0.3 in JavaScript). This is the industry standard used by Stripe, PayPal, and other payment APIs.

Example: Search Implementation

// Search with filters
const searchProducts = async (query, options = {}) => {
  const body = { query };

  // Build filters object
  const filters = {};
  if (options.domains) filters.domains = options.domains;
  if (options.minPrice) filters.min_price = options.minPrice;
  if (options.maxPrice) filters.max_price = options.maxPrice;
  if (options.inStock) filters.in_stock = true;
  if (options.variants) filters.variants = options.variants;

  if (Object.keys(filters).length > 0) {
    body.filters = filters;
  }

  if (options.pageSize) body.page_size = options.pageSize;
  if (options.cursor) body.cursor = options.cursor;

  const response = await fetch(
    'https://api.firmly.work/api/v1/discovery/search',
    {
      method: 'POST',
      headers: {
        'x-firmly-authorization': authToken,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(body)
    }
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.error || 'Search failed');
  }

  return response.json();
};

// Usage - prices in cents (100000 = $1,000.00, 500000 = $5,000.00)
const results = await searchProducts('leather sofa', {
  domains: ['benchmademodern.com', 'merchant2.com'],
  minPrice: 100000,
  maxPrice: 500000,
  inStock: true,
  variants: [
    { option: 'color', values: ['Brown', 'Tan'] },
    { option: 'size', values: ['Large', 'Sectional'] }
  ],
  pageSize: 20
});

// Check if there are results (empty array is valid, not an error)
if (results.products.length === 0) {
  console.log('No products found matching your criteria');
} else {
  console.log(`Found ${results.products.length} products`);

  // Client-side sorting by price (low to high)
  const sortedByPrice = results.products.sort(
    (a, b) => a.price_range.min - b.price_range.min
  );

  // Display price: convert cents to dollars
  const displayPrice = (cents) => `$${(cents / 100).toFixed(2)}`;
  console.log(displayPrice(results.products[0].price_range.min)); // e.g., "$2,499.99"
}

// Pagination - use cursor from previous response
if (results.next_page) {
  // Extract cursor from next_page URL or use it directly
  const nextPageUrl = new URL(results.next_page);
  const cursor = nextPageUrl.searchParams.get('cursor');

  const nextResults = await searchProducts('leather sofa', {
    cursor,
    pageSize: 20
  });
  console.log(`Loaded ${nextResults.products.length} more products`);
}