API Referencev2

Products (v2)

POST /v2/products. Legacy JSON-body product search.

POST https://api.pricehero.ai/v2/products

Legacy. Prefer v3 catalog search for new integrations.

Transitional endpoint: same catalog logic as v1, but parameters are sent as a JSON body instead of the query string.

Request

curl -X POST https://api.pricehero.ai/v2/products \
  -H "Authorization: Bearer ph_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "search": "Louis Vuitton Speedy",
    "category": "handbags,watches",
    "attribute": "product",
    "model": "model_abc,model_def",
    "style": "style_xyz",
    "page": 0,
    "page_size": 20
  }'

Body parameters

ParameterTypeDefaultDescription
searchstring;Free-text search
attributestringproductWhich taxonomy node rows to return: product, model, style, brand, category, family, case, or dial. Drives search filtering. Invalid values fall back to product.
pagenumber0Page index (0-based)
page_sizenumber20Page size (max 100)
sortstringpopularity_descpopularity_desc, popularity_asc, name_asc, name_desc. Listings only, a search is ordered by relevance.
tagsstring;Comma-separated tag ids, OR: the row must carry at least one. Unknown tags return 400.
exclude_tagsstring;Comma-separated tag ids to drop. A row carrying any of them is excluded.
exclude_<field>string;Same as any facet key, negated. "exclude_model": "model_a,model_b" drops those models.

Optional attribute facets

Use top-level JSON properties to narrow results. Catalog attributes (brand, model, style, family, case, dial, …) follow the same OR within a field, AND across fields rules as v3 catalog search.

  • category. Comma-separated category names or IDs that scope the search. Omit to use the default handbags,watches.

Other attribute keys depend on the category (e.g. handbags: brand, model, style, family; watches often add case, dial). Values are comma-separated entity ids where multiple ids mean OR within that field.

{
  "category": "handbags,watches",
  "brand": "brand_R7xVn3LpQ2sK8mTzH5YwE9aU4FbG1JdC6uN",
  "model": "model_abc,model_def",
  "style": "style_xyz",
  "case": "case_aaa,case_bbb",
  "dial": "dial_xyz"
}

Your API key must allow v2 (or v1). A 403 is returned if the key cannot access v2.

Response

Same envelope and product shape as v1: success, data, search, results, total_results, page, total_pages, stats. See Products (v1) for a representative JSON example.

Credits: the catalog credit per successful billable response, plus the pricing module only on product-scope requests (attribute=product). Non-product scopes (model, brand, style family, case, style, …) return a max market estimate but are currently billed the catalog credit only. See Credits.

Filtering by tags

A tag is curated knowledge about an item that is not in its text; material:exotic is the first one. It exists because no text rule can decide it: "Crocodile Embossed Leather" is calfskin and "Python Print" is canvas, so searching for "crocodile" is not the same question as "is this a CITES-regulated skin".

Tags are a closed vocabulary. Fetch a category to see what it offers, in available_tags; an unrecognised tag returns 400 rather than silently matching nothing. That matters most when excluding: a mistyped exclusion that quietly matched nothing would return more rows than you asked for.

"available_tags": [
  { "id": "material:exotic", "name": "Exotic material (CITES)", "group": "material" }
]

A flat list, ordered by group, then by display order within a group. group repeats the namespace already in the id, so it is there for building sections without parsing ids: group by it when you want headings, ignore it when you just need to know whether a tag is offered.

Need a tag that does not exist? Ask for it: write to mailys@pricehero.ai with the filtering decision it would drive. The vocabulary stays small on purpose and each request is assessed, on two questions: is it knowledge a text search cannot decide, and can it be applied consistently to the whole catalogue? A tag that is only sometimes right is worse than no tag, because a filter that quietly misses rows looks exactly like one that works.

Tags inherit down and aggregate up. A product carries its style's tags, and a model carries a tag only when every one of its styles does. So excluding exotic material hides the models that are only ever exotic, while keeping a Birkin that also comes in Togo, whose exotic styles are then filtered one step later.

That rule is built for exclusion, so tags= is narrower than it may look when listing models: with attribute=model it returns models that are entirely exotic, not every model that offers an exotic style. For the second question list styles instead (attribute=style&tags=material:exotic); a style either carries the tag or it does not. Products are unaffected: a product is one style in different sizes or colours, so it carries its style's tags directly.

{
  "category": "handbags",
  "attribute": "product",
  "brand": "brand_9YJxjSHS5UK1jFmA2dThev",
  "exclude_tags": "material:exotic",
  "sort": "name_asc"
}

Send "attribute": "category" to see a category's available_tags.

Sorting

sort follows a <field>_<direction> grammar.

valuemeaning
popularity_descdefault: most popular first
popularity_ascleast popular first
name_ascA→Z
name_descZ→A

name is the listed row's own name: model name when listing models, style name when listing styles, brand name when listing brands. Products have no single name, so theirs is composed brand + model + style; the category is deliberately excluded, or every handbag would sort under "Handbags".

Sorting applies to listings, not searches. A request with search / query is ordered by relevance regardless, and the response reports sort_applied so you can tell which ordering you actually got rather than assuming your request was honoured.

Sorting is case-insensitive and accent-folding, so Épi sorts with Epi and apple with Apple. Nothing else is normalised: a leading article counts (The Row sorts under T), and a name starting with punctuation or a digit sorts before the letters.

Market estimates and condition grades

market_estimates gives an estimated resale value per condition grade, for one region and one marketplace source. There is one object per (source, region) pair; google_shopping, ebay, the_realreal, fashionphile, vestiaire_collective and others, so a variant typically carries several.

gradeconditionmeaning
AExcellentMinimal to no visible wear. Looks nearly new, no significant flaws.
BGreatLight wear, still very good. Minor marks, slight fading or light scratches.
CGoodModerate wear. Visible marks or fading, fully functional and presentable.
DFairSignificant wear. Noticeable marks, stains or damage, but still usable.

Values are in currency (lowercase ISO, e.g. usd) and descend from A to D. They use the same grade scale the vision endpoints return when assessing an item's condition, so an estimate can be read directly against an identified item's grade. Any grade may be null when there is not enough signal for that condition tier.

region is one of NA, EU-UK, EU-IE.

Pricing data and historical estimates

pricing_data and historical_estimates are both arrays of { "region": "...", "url": "..." } pointing at per-variant CSV files, one entry per region.

  • pricing_data: the individual marketplace listings the estimate is derived from. Columns: link, variant_name, vendor, price, currency, date. This is the evidence behind a number, not the number itself.
  • historical_estimates: the same variant's estimate history for that region, so you can show how a value has moved over time. Displaying it is permitted; retaining it is not — it may not power stored history, monitoring, or change alerts. See Usage Policy.

These URLs expire after one hour. The files live in a private bucket; what you receive is a time-limited signed link generated for your request, not a permanent address.

  • Fetch them promptly, and do not store or share the URL: it will stop working, and a stored one is a credential with an hour of life left in it.
  • To read a file again later, request the resource again and use the fresh URL. The raw address without a signature returns 403.
  • Signing is fail-closed: if a URL cannot be signed it is omitted from the array rather than returned unsigned. So the array can be shorter than the number of regions, or empty.
Products (v2) — Price Hero Docs