visualAIAPI Docs

visualAI API Docs

Developer documentation for the visualAI API — shopperGPT and the rest of the VisualAI Platform. One page, scrollable end to end.

Welcome to the developer documentation for shopperGPT on the VisualAI Platform. This entire site is a single scrollable document — use the sidebar on the left to jump to a section, or scroll from top to bottom to read it in order.

Introduction

shopperGPT is the natural-language product search API from the VisualAI Platform. It takes a shopper's query — in plain English, with optional color or image input — and returns a ranked list of matching products from your catalog.

This site is the developer reference for the public HTTP API.

What you can build

  • Natural-language product search. Convert free-text queries like "something comfortable for a beach wedding under $200" into ranked product results.
  • Color-aware search. Mix a text query with an RGB or named color and rank products by both semantic and color similarity.
  • Visual (image) search. Upload a reference image and return the closest matching products from your catalog.
  • Personalized results. Layer optional shopper preferences — style, colors, size, budget — over any of the above.

Platform-neutral by design

shopperGPT does not require Shopify. It operates on a single canonical products table, and Shopify is one of several supported ways to populate it.

If your data lives in BigCommerce, WooCommerce, Magento, a custom PIM, a flat CSV export, or an S3 data lake, you can still use the API — see the Data Model and Ingesting Your Data sections for the canonical schema and available ingest paths.

Shopify is an example integration

Today, the most common way to install shopperGPT is via our Shopify theme extension, which handles auth and data ingest automatically. The underlying API is not Shopify-specific — every endpoint on this site works against any merchant whose catalog matches the canonical data model.

Where to start

Base URL

All API requests go to:

https://shoppergpt-api-71971351019.us-central1.run.app

This is the production host served from Google Cloud Run. A staging host will be listed here once public preview is available.

Support

Questions about integration, enterprise access, or data migration? Email enterprise@vairetail.com or visit vairetail.com.

Quickstart

This walkthrough shows you how to send your first natural-language search query to the shopperGPT API and read the response.

1. Get your merchant ID

Every request to shopperGPT carries an X-Merchant-ID header that identifies your catalog. If you installed shopperGPT through the Shopify App Store, your merchant ID was provisioned automatically — retrieve it from your Shopify admin or contact enterprise@vairetail.com.

For everyone else, non-Shopify onboarding is in private preview — email enterprise@vairetail.com to request a merchant ID.

In the examples below, replace 12345 with your actual merchant ID.

2. Make a search request

The POST /api/v0/product/search/text endpoint takes a plain-text query and returns a ranked list of products.

curl -X POST \
  https://shoppergpt-api-71971351019.us-central1.run.app/api/v0/product/search/text \
  -H "X-Merchant-ID: 12345" \
  -H "Content-Type: application/json" \
  -d '{
    "query_str": "comfortable linen shirt under $80",
    "limit": 5
  }'

3. Read the response

A successful call returns JSON with a results.data array of product objects, a natural-language recommendation, and a success flag.

{
  "results": {
    "data": [
      {
        "product_id": "4829104710",
        "product_name": "Relaxed-Fit Linen Button-Up",
        "price_numeric": 69.0,
        "image1": "https://cdn.example.com/linen-shirt.jpg",
        "brand": "Cove & Coast"
      }
    ]
  },
  "recommendation": "Here are a few breezy linen shirts under $80 that should feel great all day.",
  "success": true
}

4. Next steps

400 Missing or invalid X-Merchant-ID

If you run the curl example without a valid merchant ID, the API will respond with 400 Missing or invalid X-Merchant-ID header. That is expected — it confirms the endpoint is reachable.

Authentication

shopperGPT currently supports one authentication mode, with a second mode coming to general availability. Both are documented here.

Current: X-Merchant-ID header

Every search, user, pipeline, and merchant endpoint requires an integer X-Merchant-ID header identifying your catalog.

POST /api/v0/product/search/text HTTP/1.1
Host: shoppergpt-api-71971351019.us-central1.run.app
Content-Type: application/json
X-Merchant-ID: 12345

If the header is missing or non-integer, the server returns:

{ "detail": "Missing or invalid X-Merchant-ID header" }

with HTTP status 400.

Endpoints that do NOT require X-Merchant-ID

  • GET /health — platform health check
  • GET /api/v1/proxy-image — public image proxy

Every other endpoint documented on this site requires the header.

How merchant IDs are issued

  • Shopify merchants: your merchant ID is provisioned automatically during the Shopify app install flow and stored in the theme extension's local storage as merchant_id.
  • Non-Shopify merchants: merchant IDs are issued by the VisualAI team during onboarding — contact enterprise@vairetail.com.

Trust model (today)

The production API accepts requests only from approved origins (Shopify storefronts and VisualAI-hosted services) via a CORS allow-list. The X-Merchant-ID header identifies which catalog to operate on; it does not itself authenticate the caller. This model is safe for the current browser-only deployment surface but is not suitable for server-to-server or cross-organization use.

Browser-only today

Because trust is enforced at the CORS layer, the current API is intended for calls from approved browser origins. Server-to-server integrations should wait for API-key auth (below) or contact us for a staging arrangement.

Coming soon: API keys

Status: Coming soon

The API-key authentication flow described below is in private beta. It is documented here so that integration code written today can be designed with forward compatibility in mind. Email enterprise@vairetail.com to join the beta.

The next iteration of shopperGPT auth adds a Bearer-token header alongside X-Merchant-ID:

POST /api/v0/product/search/text HTTP/1.1
Host: api.vairetail.com
Content-Type: application/json
X-Merchant-ID: 12345
Authorization: Bearer sk_live_8f3...redacted

API keys will be:

  • Issued per-merchant via a developer console
  • Scoped per environment (sk_test_… vs sk_live_…)
  • Hashed at rest (argon2id) — plaintext only shown once, at creation time
  • Rotatable and revocable without redeploying
  • Rate-limited per key

Once API keys land, the CORS allow-list relaxes for server-to-server endpoints and every endpoint page on this site will flip its Auth modes supported callout from "Shopify-embedded only" to "Shopify-embedded + API key."

Decoupling from Shopify

Today, a merchant's ID happens to equal their Shopify Shop ID for Shopify installs. This is an implementation detail and will change — new non-Shopify merchants already receive IDs from a separate range. Always treat the merchant ID as an opaque integer.

Errors

shopperGPT uses conventional HTTP status codes and returns all error details in a JSON body with a detail field.

Error shape

{
  "detail": "Missing or invalid X-Merchant-ID header"
}

detail is a human-readable string. In some validation errors it is a list of FastAPI validation objects — see Validation errors below.

Status codes

StatusMeaning
200Success.
400Bad request. Missing or malformed X-Merchant-ID, missing required body fields, invalid color input, or a search body with neither text nor color.
403Forbidden. The authenticated merchant does not have access to the target resource (e.g. calling another merchant's pipeline status).
404Not found. A referenced user profile, pipeline status, or resource does not exist for this merchant.
422Unprocessable entity. FastAPI schema validation failed (wrong field types, missing required fields).
500Internal server error. A bug or downstream failure. Retry with backoff and contact support if it persists.
502Bad gateway. The upstream service a utility endpoint depends on (e.g. proxy-image) was unreachable.

Common error examples

400 — missing merchant ID

HTTP/1.1 400 Bad Request
Content-Type: application/json

{"detail": "Missing or invalid X-Merchant-ID header"}

400 — empty search body

Sent to POST /api/v0/product/search/textandcolor with neither a query_str nor a color:

{ "detail": "At least one of 'text' or 'color' must be provided." }

403 — cross-merchant access

Attempting to read another merchant's pipeline status:

{ "detail": "Access denied to this merchant's pipeline status" }

404 — resource not found

{ "detail": "User preferences not found" }

422 — validation error

{
  "detail": [
    {
      "loc": ["body", "query_str"],
      "msg": "field required",
      "type": "value_error.missing"
    }
  ]
}

Retries

The search endpoints are idempotent — identical POSTs return identical results (and often a cached payload). Safe to retry on 5xx with exponential backoff.

Mutation endpoints (POST /api/v1/user/profile, POST /api/v1/user/preferences, PUT /api/v1/pipeline/status/{merchant_id}) are also safe to retry with the same body — they upsert based on their natural keys.

The DELETE /api/v1/merchant/{merchant_id}/data endpoint is not automatically retried and requires an explicit environment flag on the server; do not retry on a 403 for this endpoint.

Request ID

Every response includes an X-Request-ID header containing a UUID that uniquely identifies the request on the server side. Include this when reporting issues to enterprise@vairetail.com.

HTTP/1.1 200 OK
X-Request-ID: f2c8e9d1-0b3a-4a74-9cc6-b1d3a2a0f0ab

Data Model

shopperGPT operates on a small, fixed set of canonical tables. Every search, every recommendation, every preference query runs against these tables regardless of where your data originated.

Products

products is the single table every search endpoint reads. It holds one row per product per merchant. Regardless of whether your data came from Shopify, a CSV export, or a custom PIM, shopperGPT reads it here.

Rows are scoped by merchant_id — multi-tenant isolation is enforced at the database level via row-level security.

Fields you provide

FieldTypeRequiredConstraintsNotes
product_idstringyesUnique per merchant, ≤ 2048 charsOpaque stable ID from your source system — SKU, UUID, slug, or numeric ID, your choice.
product_namestringyesPlaintextShopify title, Woo name, custom productName, etc.
handlestringrecommendedURL-safe slug, lowercase, hyphenatedStorefront URL slug.
product_detailsstringrecommendedPlaintext or HTML (HTML is stripped during enrich)Long description. Drives semantic text search.
brandstringoptionalPlaintextShopify vendor.
categorystringrecommendedFree text (tops, dresses, accessories)Enrichment fills this in if missing.
sub_categorystringoptionalFree text
colorstringoptionalFree text (navy blue) or hex (#0a1f44)The color pipeline will derive color from image1 if this is missing.
tagsstringoptionalComma-separated
pricestringyesCurrency-formatted string, e.g. "29.99"Display field. Must agree with price_numeric.
price_numericfloatyesNon-negative decimalUsed for range filters, min/max price queries, and price-aware ranking.
variant_compare_at_pricestringoptionalCurrency stringOriginal / MSRP price for sale display.
option1_namestringoptionale.g. "Size", "Color"First variant option label.
genderenumoptionalmens | womens | unisex | kidsEnrichment will fill this from product_name / product_details if missing.
image1stringyesPublicly reachable HTTPS URL, ≤ 10 MB, CORS-readableThe primary product image. All image embeddings and color extraction are computed from this URL.
image1_descriptionstringoptionalPlaintextIf absent, enrichment generates a description via a vision model.
statusenumoptionalactive | draft | archived (default active)Only active rows are searchable.
display_flagbooleanoptionalDefault trueSoft-hide: set to false to exclude from search without deleting.
metadata_jsonobjectoptionalArbitrary JSONFree-form field for source-specific fields you want to preserve round-trip.

Required minimum

At an absolute minimum, a product row must have product_id, product_name, price, price_numeric, and image1. Everything else is optional but strongly recommended — enrichment can fill gaps but the best results come from the richest input.

Fields we compute

These columns are populated by the shopperGPT ETL pipeline during ingest and enrichment. Do not provide them — sending them on ingest is an error.

FieldPopulated by
text_embeddingText embedding pipeline (create_text_embeddings)
text_embedding_1Text embedding pipeline
img_embeddingImage embedding pipeline (create_img_embeddings)
image_desc_embedVision description pipeline
color_embeddingColor pipeline (create_color_embeddings)
color_embedding_1..3Color pipeline
color_hsv_embedding_1..3Color pipeline
percent_1..3Color pipeline (dominant-color coverage percentages)
merchant_idSet server-side from the authenticated identity.
last_updatedDatabase-managed timestamp.
idDatabase-managed primary key.

Uniqueness & upserts

Rows are unique on (merchant_id, product_id). Ingesting a product whose product_id already exists for the same merchant is treated as an upsert: non-null fields from the new payload overwrite the existing row, and the computed columns are recomputed in the next enrichment cycle.

User profiles

user_profile holds optional per-shopper identity data. Every profile is scoped to a single merchant (merchant_id) and keyed by email within that merchant.

User profiles are optional. Anonymous search works perfectly well — you only need profiles when you want to attach preferences or personalize results over time.

Fields

FieldTypeRequiredNotes
idintegerserverAuto-generated primary key.
user_namestringyesDisplay name (first name is acceptable).
first_namestringyesFirst name.
last_namestringyesLast name.
email_idstringyesEmail address. Unique within a merchant.
profile_namestringoptionalA label for the profile (e.g. "Work", "Home").

Uniqueness

Rows are unique on (merchant_id, email_id). Creating a profile with an existing email returns the existing row rather than a duplicate — see POST /api/v1/user/profile.

User preferences

user_preferences holds optional per-shopper preferences that search and recommendation endpoints use to personalize results. Each row is attached to a user_profile by user_profile_id, and has a one-to-one relationship with it.

Preferences are optional and independent of the rest of the catalog — you can ship shopperGPT with no preferences at all and still get a great baseline search experience.

Fields

FieldTypeRequiredNotes
idintegerserverAuto-generated primary key.
user_profile_idintegeryesForeign key to user_profile.id. Unique — one preferences row per user.
conversational_stylestringoptionalFree text (concise, friendly, technical, …). Biases recommendation tone.
personal_fashion_stylestringoptionalFree text (minimalist, streetwear, classic, …).
favorite_colorsarray of stringsoptionalE.g. ["navy", "olive"]. Influences color-aware ranking.
fashion_material_preferarray of stringsoptionalE.g. ["linen", "cotton"].
body_typestringoptionalFree text.
fashion_goalsstringoptionalFree text ("dress for job interviews", "beach vacation outfits").
age_groupstringoptionalFree text.
is_modifiedbooleanservertrue after a user has explicitly edited their preferences.
product_idsarray of integersserverInternal — used by the randomdata endpoint to seed cold-start results.

How preferences are used

When a search request is made with preferences: true and a user_profile_id, the API loads that profile's preferences and:

  1. Feeds conversational_style into the recommendation LLM.
  2. Adds favorite_colors and fashion_material_prefer as soft ranking signals — products matching preferred attributes are nudged up.
  3. Uses body_type, age_group, and personal_fashion_style as context in the recommendation text.

Preferences are never used as a hard filter — they influence ranking, they don't exclude products.

Pipeline status

pipeline_status tracks the progress of a merchant's ingest and enrichment pipeline — from initial catalog import all the way through embedding generation and extension deployment. It is populated by shopperGPT's internal ETL workers and read by admin UIs to display onboarding progress.

You don't usually write to this table directly — the POST /api/v1/pipeline/status endpoint initializes a row, and the internal pipeline workers update it as each step completes. Your admin UI reads it via GET /api/v1/pipeline/status/{merchant_id}.

Fields

FieldTypeNotes
merchant_idintegerThe merchant this pipeline run belongs to.
shop_domainstringOptional source-platform domain (e.g. store.myshopify.com). Informational.
current_stepstringThe step currently running. See Pipeline steps below.
completed_stepsarray of stringSteps that have completed successfully, in order.
failed_stepsarray of stringSteps that failed. Parallel to completed_steps.
statusstringrunning | completed | failed.
started_atdatetime (UTC)When the run started.
completed_atdatetime (UTC)When the run finished. null while running.
last_updateddatetime (UTC)When the row was most recently touched.
error_messagestringPopulated when status = failed.
configobjectSource-specific configuration carried through the pipeline.

Response-only fields

The PipelineStatusResponse returned by the API endpoints adds two computed fields that are not stored in the database:

FieldTypeNotes
progress_percentagefloat (0–100)completed_steps.length / total_steps * 100.
estimated_completiondatetime (UTC)Heuristic estimate based on average step duration.

Pipeline steps

The default dev pipeline has these steps, in order:

  1. shopify_etl_dev — initial catalog import (Shopify merchants only today)
  2. enrichment_dev — description enrichment, category classification, gender inference
  3. create_color_embeddings — extract and embed dominant colors
  4. add_color_names — map color embeddings to human-readable color names
  5. create_text_embeddings — generate semantic text embeddings
  6. create_img_embeddings — generate image similarity embeddings
  7. deploy_shoppergpt_extension — deploy the storefront extension (Shopify only)

Non-Shopify merchants use a modified pipeline that skips shopify_etl_dev and deploy_shoppergpt_extension.

CSV format

shopperGPT accepts UTF-8 CSV files whose columns map to the canonical products fields.

Requirements

  • Encoding: UTF-8 (no BOM required, but accepted).
  • Delimiter: comma (,).
  • Header row: required. Column names must match the canonical field names exactly, or a mapping must be supplied.
  • Quoting: RFC 4180 — wrap any field containing commas, quotes, or newlines in double quotes, with interior quotes doubled ("").
  • Row order: unimportant. Rows will be imported in the order given.
  • Max size: 100 MB per file for direct upload. For larger catalogs, split into multiple files or use NDJSON streaming.

Column mapping

If your source system uses different column names — and it almost certainly does — use the Column Mapper to paste your headers and produce a mapping.json file. You can then either:

  1. Run the mapping yourself (Node or Python snippets) to produce a canonical CSV, or
  2. Ship mapping.json alongside the raw CSV (once the push-ingest endpoint is GA).

Sample file

A minimal canonical CSV looks like this:

product_id,product_name,handle,product_details,brand,category,color,tags,price,price_numeric,gender,image1,status
SKU-1001,Relaxed Linen Button-Up,relaxed-linen-button-up,"Breezy 100% linen with a relaxed cut.",Cove & Coast,tops,white,"linen,summer",69.00,69.00,mens,https://cdn.example.com/images/relaxed-linen.jpg,active
SKU-1002,Classic Chinos,classic-chinos,"Midweight cotton chinos with a tailored leg.",Cove & Coast,bottoms,khaki,"cotton,classic",89.00,89.00,mens,https://cdn.example.com/images/classic-chinos.jpg,active
SKU-1003,Midi Wrap Dress,midi-wrap-dress,"Floaty midi wrap dress in a soft rayon blend.",Sand Coast,dresses,coral,"rayon,summer",119.00,119.00,womens,https://cdn.example.com/images/midi-wrap.jpg,active

Download the full sample: products.csv

Validation rules

During ingest shopperGPT applies the following rules to every row:

  • product_id, product_name, price, price_numeric, and image1 must be present and non-empty.
  • price_numeric must parse as a non-negative float.
  • image1 must be a valid HTTPS URL.
  • status, if provided, must be one of active, draft, archived.
  • display_flag, if provided, must be true or false (case-insensitive).
  • gender, if provided, must be one of mens, womens, unisex, kids.
  • Any additional columns not in the canonical schema are placed into metadata_json as string values.

Rows that fail validation are skipped with a line number and a reason in the ingest log; valid rows in the same file still import.

JSON format

shopperGPT accepts two JSON shapes for bulk ingest: newline-delimited JSON (NDJSON) and an array of objects. For any catalog over a few thousand rows, prefer NDJSON — it streams and doesn't require the whole catalog to live in memory on either side.

NDJSON (preferred for large catalogs)

One JSON object per line. No array brackets, no trailing commas. UTF-8. Content-type application/x-ndjson.

{"product_id":"SKU-1001","product_name":"Relaxed Linen Button-Up","handle":"relaxed-linen-button-up","product_details":"Breezy 100% linen with a relaxed cut.","brand":"Cove & Coast","category":"tops","color":"white","tags":"linen,summer","price":"69.00","price_numeric":69.0,"gender":"mens","image1":"https://cdn.example.com/images/relaxed-linen.jpg","status":"active"}
{"product_id":"SKU-1002","product_name":"Classic Chinos","handle":"classic-chinos","product_details":"Midweight cotton chinos with a tailored leg.","brand":"Cove & Coast","category":"bottoms","color":"khaki","tags":"cotton,classic","price":"89.00","price_numeric":89.0,"gender":"mens","image1":"https://cdn.example.com/images/classic-chinos.jpg","status":"active"}

Download the full sample: products.ndjson

Array-of-objects (small batches)

For small imports (under a few thousand products), a standard JSON array works fine. Content-type application/json.

[
  {
    "product_id": "SKU-1001",
    "product_name": "Relaxed Linen Button-Up",
    "handle": "relaxed-linen-button-up",
    "product_details": "Breezy 100% linen with a relaxed cut.",
    "brand": "Cove & Coast",
    "category": "tops",
    "color": "white",
    "tags": "linen,summer",
    "price": "69.00",
    "price_numeric": 69.0,
    "gender": "mens",
    "image1": "https://cdn.example.com/images/relaxed-linen.jpg",
    "status": "active"
  },
  {
    "product_id": "SKU-1002",
    "product_name": "Classic Chinos",
    "handle": "classic-chinos",
    "product_details": "Midweight cotton chinos with a tailored leg.",
    "brand": "Cove & Coast",
    "category": "bottoms",
    "color": "khaki",
    "tags": "cotton,classic",
    "price": "89.00",
    "price_numeric": 89.0,
    "gender": "mens",
    "image1": "https://cdn.example.com/images/classic-chinos.jpg",
    "status": "active"
  }
]

Type coercion

  • Strings vs numbers: price must be a JSON string; price_numeric must be a JSON number. These are separate fields for a reason — the numeric version is what search uses, the string version is what the UI renders.
  • Booleans: display_flag must be a JSON boolean, not "true" as a string.
  • Arrays: tags must be a string (comma-separated). It is stored that way in the database and split at query time only when needed.
  • Unknown fields: any field not in the canonical schema is written to metadata_json.

Fields you must not include

Never include these computed fields on ingest — the server will reject them:

  • merchant_id (set server-side from auth)
  • id, last_updated (database-managed)
  • text_embedding, text_embedding_1, img_embedding, image_desc_embed
  • color_embedding, color_embedding_1, color_embedding_2, color_embedding_3
  • color_hsv_embedding_1, color_hsv_embedding_2, color_hsv_embedding_3
  • percent_1, percent_2, percent_3

See products → Fields we compute.

Ingesting Your Data

shopperGPT supports two ingest models: pull (shopperGPT reaches into your commerce platform on a schedule) and push (you send us your catalog). Pull is available today for Shopify; push is in private beta.

Shopify (Pull)

The Shopify integration is the most common ingest path and the one available today. It is also the only ingest path that requires zero custom code on your side.

What it does

  1. You install the shopperGPT app from the Shopify App Store.
  2. You grant the app read access to your products and storefront.
  3. shopperGPT pulls your product catalog via the Shopify Admin API and populates the canonical products table.
  4. The enrichment pipeline runs: categories, color extraction, embeddings.
  5. shopperGPT installs a theme extension that wires the search UI into your storefront. Users immediately start seeing semantic search.
  6. Ongoing updates stream in via Shopify webhooks — product creates, updates, and deletes are reflected in the canonical table within seconds.

Field mapping (Shopify → canonical)

Shopify field (Admin API)Canonical products field
idproduct_id
titleproduct_name
handlehandle
body_htmlproduct_details (HTML stripped on enrich)
vendorbrand
product_typecategory
tagstags
variants[0].priceprice / price_numeric
variants[0].compare_at_pricevariant_compare_at_price
options[0].nameoption1_name
images[0].srcimage1
statusstatus

Gender is inferred during enrichment from product_name, product_type, and body_html. Color is extracted from image1 if not present in tags.

Onboarding status

Pipeline progress (shopify_etl_dev → enrichment_dev → ... → deploy_shoppergpt_extension) is tracked in pipeline_status and can be read via GET /api/v1/pipeline/status/{merchant_id}.

Merchant ID

Your shopperGPT merchant_id is assigned during install and happens to equal your Shopify Shop ID for today's installs. Always treat it as an opaque integer — future non-Shopify merchant IDs will come from a separate range.

Uninstall

Uninstalling the Shopify app removes the theme extension and stops the webhook stream. Product rows remain in the canonical table by default (so reinstalling a week later doesn't require a full re-import).

For full data deletion on uninstall, see DELETE /api/v1/merchant/{merchant_id}/data.

Push ingest

Status: Coming soon

The push-ingest endpoint is in private beta. The contract below is final but the endpoint is not yet publicly reachable. Email enterprise@vairetail.com to join the beta. Once GA, this page will flip to Status: Generally available.

Push ingest lets you send catalog rows to shopperGPT from any source — BigCommerce, WooCommerce, Magento, a custom PIM, a Python script reading from Snowflake, whatever.

Contract

POST /api/v1/ingest/products HTTP/1.1
Host: shoppergpt-api-71971351019.us-central1.run.app
Content-Type: application/x-ndjson
X-Merchant-ID: 12345
Authorization: Bearer sk_live_...

Body: newline-delimited JSON matching the canonical products schema.

One row per line, no array wrapping. See JSON format → NDJSON.

Request parameters (form fields / query string, optional)
FieldTypeDescription
modestringupsert (default) or replace. replace deletes rows absent from the payload.
dry_runbooleanIf true, validate without writing and return a per-row report.
mappingobjectA JSON object matching the Column Mapper output format. Applied server-side if you're sending non-canonical field names.
Response
{
  "status": "accepted",
  "merchant_id": 12345,
  "rows_received": 4821,
  "rows_upserted": 4810,
  "rows_skipped": 11,
  "errors": [
    { "line": 38, "reason": "price_numeric must be a non-negative number" },
    { "line": 102, "reason": "image1 must be an HTTPS URL" }
  ],
  "pipeline_run_id": "pr_8f3d2..."
}

Pipeline enrichment kicks off automatically after ingest. Track progress via GET /api/v1/pipeline/status/{merchant_id}.

Running a mapping client-side

If you'd rather produce canonical NDJSON yourself before calling the ingest endpoint — useful today, since push ingest is still in beta — see:

Example

### Coming soon — this will become runnable once push ingest is GA.
curl -X POST \
  https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/ingest/products \
  -H "X-Merchant-ID: 12345" \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/x-ndjson" \
  --data-binary @products.ndjson

Other platforms

If your catalog lives somewhere other than Shopify, the path today is:

  1. Export your catalog to CSV or NDJSON.
  2. Use the Column Mapper to map your source columns to the canonical products schema.
  3. Apply the mapping to produce a canonical file (Node or Python snippets).
  4. Send it to us via push ingest (in beta) or share it with the VisualAI team during onboarding.

Below are the typical field mappings for the common platforms. These are seeded into the Column Mapper's auto-suggest.

BigCommerce

BigCommerce fieldCanonical products field
id or skuproduct_id
nameproduct_name
custom_url.urlhandle
descriptionproduct_details
brand_namebrand
categoriescategory
priceprice_numeric
retail_pricevariant_compare_at_price
primary_image.url_standardimage1
is_visibledisplay_flag

WooCommerce

WooCommerce fieldCanonical products field
id or skuproduct_id
nameproduct_name
slughandle
descriptionproduct_details
(attribute) brandbrand
categories[0].namecategory
priceprice_numeric
regular_pricevariant_compare_at_price
tags[].nametags (comma-joined)
images[0].srcimage1
statusstatus

Magento

Magento fieldCanonical products field
skuproduct_id
nameproduct_name
url_keyhandle
descriptionproduct_details
brand (custom attr)brand
category_namescategory
priceprice_numeric
base_imageimage1
statusstatus

Custom PIM / flat export

If you own the source format, the fastest path is to export a CSV whose headers already match the canonical schema. See CSV format for the required columns.

If you can't rename columns at the source, use the Column Mapper — paste your headers, accept the auto-suggestions, download mapping.json, and run it through the Node or Python transformer.

Column Mapper

The Column Mapper is an interactive tool that maps your source catalog columns to the canonical products schema. It runs entirely in your browser — nothing is uploaded. Once you've exported a mapping.json, use one of the recipes below to transform your raw CSV into canonical NDJSON.

Apply a mapping (Node.js)

This snippet takes a mapping.json file (downloaded from the Column Mapper) and uses it to convert a raw source CSV into canonical NDJSON ready for push ingest.

Install dependencies

npm install csv-parse

Script

import fs from 'node:fs';
import { parse } from 'csv-parse/sync';

// 1. Load the mapping file produced by the Column Mapper.
const mappingConfig = JSON.parse(fs.readFileSync('mapping.json', 'utf8'));
const { mapping } = mappingConfig;

// 2. Read the raw CSV.
const rawCsv = fs.readFileSync('raw-products.csv', 'utf8');
const rows = parse(rawCsv, { columns: true, skip_empty_lines: true });

// 3. Transform each row into the canonical shape.
const canonical = rows.map((row) => {
  const out = {};
  for (const [canonicalField, spec] of Object.entries(mapping)) {
    if (typeof spec === 'string') {
      out[canonicalField] = row[spec];
    } else {
      const value = row[spec.source];
      out[canonicalField] = applyTransform(value, spec.transform);
    }
  }
  return out;
});

// 4. Write as NDJSON.
const ndjson = canonical.map((r) => JSON.stringify(r)).join('\n') + '\n';
fs.writeFileSync('products.ndjson', ndjson);

console.log(`Wrote ${canonical.length} canonical products`);

function applyTransform(value, transform) {
  if (value == null || value === '') return null;
  switch (transform) {
    case 'parse_float': {
      const n = parseFloat(String(value).replace(/[^0-9.-]/g, ''));
      return Number.isFinite(n) ? n : null;
    }
    case 'trim':
      return String(value).trim();
    case 'uppercase':
      return String(value).toUpperCase();
    case 'lowercase':
      return String(value).toLowerCase();
    case 'strip_html':
      return String(value).replace(/<[^>]*>/g, '');
    default:
      return value;
  }
}

Output

products.ndjson will contain one canonical product per line, ready to be POSTed to the push-ingest endpoint once it is GA.

Validating before sending

Once push ingest is available, use dry_run=true on the first attempt:

curl -X POST \
  "https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/ingest/products?dry_run=true" \
  -H "X-Merchant-ID: 12345" \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/x-ndjson" \
  --data-binary @products.ndjson

A dry_run request returns per-row validation results without writing anything to your catalog.

Apply a mapping (Python)

This snippet takes a mapping.json file (downloaded from the Column Mapper) and uses it to convert a raw source CSV into canonical NDJSON, ready for push ingest.

Standard library only — no dependencies.

Script

import csv
import json
import re
from pathlib import Path


def apply_transform(value, transform):
    if value is None or value == "":
        return None
    if transform == "parse_float":
        cleaned = re.sub(r"[^0-9.\-]", "", str(value))
        try:
            return float(cleaned)
        except ValueError:
            return None
    if transform == "trim":
        return str(value).strip()
    if transform == "uppercase":
        return str(value).upper()
    if transform == "lowercase":
        return str(value).lower()
    if transform == "strip_html":
        return re.sub(r"<[^>]*>", "", str(value))
    return value


def transform_row(row, mapping):
    out = {}
    for canonical_field, spec in mapping.items():
        if isinstance(spec, str):
            out[canonical_field] = row.get(spec)
        else:
            value = row.get(spec["source"])
            out[canonical_field] = apply_transform(value, spec.get("transform"))
    return out


def main():
    mapping_config = json.loads(Path("mapping.json").read_text())
    mapping = mapping_config["mapping"]

    with (
        open("raw-products.csv", newline="", encoding="utf-8") as src,
        open("products.ndjson", "w", encoding="utf-8") as dst,
    ):
        reader = csv.DictReader(src)
        count = 0
        for row in reader:
            canonical = transform_row(row, mapping)
            dst.write(json.dumps(canonical) + "\n")
            count += 1

    print(f"Wrote {count} canonical products")


if __name__ == "__main__":
    main()

Output

products.ndjson will contain one canonical product per line, ready to be POSTed to the push-ingest endpoint once it is GA.

Validating before sending

Once push ingest is available, use dry_run=true on the first attempt:

import httpx

with open("products.ndjson", "rb") as f:
    r = httpx.post(
        "https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/ingest/products",
        params={"dry_run": "true"},
        headers={
            "X-Merchant-ID": "12345",
            "Authorization": "Bearer sk_test_...",
            "Content-Type": "application/x-ndjson",
        },
        content=f.read(),
    )

print(r.json())

Search API

Query response

POST /api/v0/product/search/queryresponse

Generates an LLM-driven natural-language recommendation for a shopper's query — the conversational text shown above product results. This endpoint does not return products; it returns a single recommendation string.

The theme extension calls this endpoint first, then calls /api/v0/product/search/text or /api/v0/product/search/textandcolor to fetch the actual matching products.

Results are cached per (user_profile_id, normalized_query[, color]) so repeat queries return the same recommendation instantly.

Auth modes supported

Today: Shopify-embedded only (CORS-enforced, X-Merchant-ID header). API key support coming soon.

Headers

HeaderRequiredValue
X-Merchant-IDyesYour merchant ID (integer)
Content-Typeyesapplication/json

Request body

FieldTypeRequiredDescription
query_strstringyesThe shopper's search query in plain language.
preferencesbooleannoIf true, merge the user's stored preferences into the prompt.
userPrefsNotPresentbooleannoHint that the user profile has no preferences yet.
current_hourintegernoLocal hour (0–23) — used to pick a time-of-day greeting.
user_namestringnoShopper first name.
color_inputstring or objectnoOptional color (named or hex). When set, the recommendation is color-aware.
user_profile_idintegernoAttach this query to a known user profile.
conversational_stylestringnoStyle hint for the LLM (friendly, concise, technical, …).
personal_fashion_stylestringnoFashion style hint.
favorite_colorsstring[]noSoft color preferences.
fashion_material_preferstring[]noPreferred materials.
body_typestringno
fashion_goalsstringno
age_groupstringno
session_idstringnoOpaque session identifier for analytics.
client_ipstringnoClient IP (otherwise inferred server-side).
locationstringnoCity/region string for locale hints.

Response — 200 OK

{
  "results": {
    "recommendation": "Here are a few breezy linen shirts under $80 that should feel great all day."
  }
}

Example

curl -X POST \
  https://shoppergpt-api-71971351019.us-central1.run.app/api/v0/product/search/queryresponse \
  -H "X-Merchant-ID: 12345" \
  -H "Content-Type: application/json" \
  -d '{
    "query_str": "comfortable linen shirt under $80",
    "user_name": "Alex",
    "current_hour": 14
  }'
const res = await fetch(
  'https://shoppergpt-api-71971351019.us-central1.run.app/api/v0/product/search/queryresponse',
  {
    method: 'POST',
    headers: {
      'X-Merchant-ID': '12345',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      query_str: 'comfortable linen shirt under $80',
      user_name: 'Alex',
    }),
  },
);
const data = await res.json();
console.log(data.results.recommendation);
import httpx

r = httpx.post(
    "https://shoppergpt-api-71971351019.us-central1.run.app/api/v0/product/search/queryresponse",
    headers={"X-Merchant-ID": "12345", "Content-Type": "application/json"},
    json={"query_str": "comfortable linen shirt under $80", "user_name": "Alex"},
)
print(r.json()["results"]["recommendation"])

Errors

StatusResponse
400{ "detail": "Missing or invalid X-Merchant-ID header" }
422Validation error — usually a missing query_str.
500Internal error reaching the LLM. Safe to retry with backoff.
POST /api/v0/product/search/text

The workhorse search endpoint. Takes a natural-language query, runs a semantic-similarity search over the merchant's catalog, filters by price, and returns ranked products plus a conversational recommendation.

Supports natural-language price constraints in the query string — "linen shirt under $80", "dress between $50 and $120", "sneakers around $200" — which are parsed out and applied as numeric filters.

Results are cached for 7 days, keyed on the normalized query plus any relevant preference fields.

Auth modes supported

Today: Shopify-embedded only (CORS-enforced, X-Merchant-ID header). API key support coming soon.

Headers

HeaderRequiredValue
X-Merchant-IDyesYour merchant ID (integer)
Content-Typeyesapplication/json

Request body

FieldTypeRequiredDefaultDescription
query_strstringyesThe shopper's search query. May contain price phrases like "under $80".
limitintegerno10Maximum number of results.
preferencesbooleannofalseIf true, blend the user's stored preferences into ranking.
user_profile_idintegernoAttach results to a user profile for analytics.
user_namestringnoShopper first name (used in the recommendation).
conversational_stylestringnoStyle hint for the recommendation LLM.
personal_fashion_stylestringnoFashion style hint.
favorite_colorsstring[]noSoft color preferences — nudge ranking toward matching colors.
fashion_material_preferstring[]noPreferred materials.
body_typestringno
fashion_goalsstringno
age_groupstringno
genderstring[]noFilter: any of mens, womens, unisex, kids.
min_pricenumbernoExplicit minimum price filter (takes precedence over in-query price phrases).
max_pricenumbernoExplicit maximum price filter.
session_id, client_ip, locationstringnoAnalytics fields.

Response — 200 OK

{
  "results": {
    "data": [
      {
        "product_id": "SKU-1001",
        "product_name": "Relaxed-Fit Linen Button-Up",
        "handle": "relaxed-fit-linen-button-up",
        "brand": "Cove & Coast",
        "category": "tops",
        "price": "69.00",
        "price_numeric": 69.0,
        "image1": "https://cdn.example.com/images/relaxed-linen.jpg",
        "distance": 0.213
      }
    ]
  },
  "recommendation": "Here are a few breezy linen shirts under $80 that should feel great all day.",
  "success": true
}

distance is the similarity score between the query and the product's embedding — lower is better. Each product also carries every canonical products field that was populated for the row.

Example

curl -X POST \
  https://shoppergpt-api-71971351019.us-central1.run.app/api/v0/product/search/text \
  -H "X-Merchant-ID: 12345" \
  -H "Content-Type: application/json" \
  -d '{
    "query_str": "comfortable linen shirt under $80",
    "limit": 10,
    "gender": ["mens", "unisex"]
  }'
const res = await fetch(
  'https://shoppergpt-api-71971351019.us-central1.run.app/api/v0/product/search/text',
  {
    method: 'POST',
    headers: {
      'X-Merchant-ID': '12345',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      query_str: 'comfortable linen shirt under $80',
      limit: 10,
    }),
  },
);
const data = await res.json();
console.log(`${data.results.data.length} products`);
import httpx

r = httpx.post(
    "https://shoppergpt-api-71971351019.us-central1.run.app/api/v0/product/search/text",
    headers={"X-Merchant-ID": "12345", "Content-Type": "application/json"},
    json={"query_str": "comfortable linen shirt under $80", "limit": 10},
)
print(len(r.json()["results"]["data"]))

Errors

StatusResponse
400{ "detail": "Missing or invalid X-Merchant-ID header" }
422Validation error — usually a missing query_str.
500{ "detail": "An error occurred during the search." }
POST /api/v0/product/search/textandcolor

Runs a combined text and color search. Either query_str or color can be omitted individually, but at least one must be provided. The endpoint determines its operation_type automatically:

  • Both text and color → textandcolor
  • Color only → color
  • Text only → text

Natural-language price constraints in query_str are parsed and applied, just like text search. Results are cached for 7 days on a normalized query|color cache key.

Auth modes supported

Today: Shopify-embedded only (CORS-enforced, X-Merchant-ID header). API key support coming soon.

Headers

HeaderRequiredValue
X-Merchant-IDyesYour merchant ID (integer)
Content-Typeyesapplication/json

Request body

FieldTypeRequiredDefaultDescription
query_strstringone of query_str / colorThe text query.
colorstring or objectone of query_str / colorNamed color ("navy"), hex ("#0a1f44"), or { "rgb": [r, g, b] }.
limitintegerno10Maximum results.
min_pricenumbernoExplicit minimum price.
max_pricenumbernoExplicit maximum price.
user_profile_idintegernoAttach for analytics and personalization.
user_namestringnoUsed in the recommendation text.
conversational_stylestringnoStyle hint for the recommendation LLM.
session_id, client_ip, locationstringnoAnalytics fields.

400 if both are empty

If both query_str and color are missing or empty, the endpoint returns 400: At least one of 'text' or 'color' must be provided.

Response — 200 OK

{
  "results": {
    "data": [
      {
        "product_id": "SKU-2003",
        "product_name": "Navy Linen Blazer",
        "price": "189.00",
        "price_numeric": 189.0,
        "image1": "https://cdn.example.com/images/navy-blazer.jpg",
        "color": "navy blue",
        "distance": 0.187
      }
    ]
  },
  "recommendation": "These navy pieces should layer beautifully for a smart-casual look.",
  "success": true
}

Example

curl -X POST \
  https://shoppergpt-api-71971351019.us-central1.run.app/api/v0/product/search/textandcolor \
  -H "X-Merchant-ID: 12345" \
  -H "Content-Type: application/json" \
  -d '{
    "query_str": "blazer for a wedding",
    "color": "navy",
    "limit": 8
  }'
const res = await fetch(
  'https://shoppergpt-api-71971351019.us-central1.run.app/api/v0/product/search/textandcolor',
  {
    method: 'POST',
    headers: {
      'X-Merchant-ID': '12345',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      query_str: 'blazer for a wedding',
      color: 'navy',
      limit: 8,
    }),
  },
);
import httpx

r = httpx.post(
    "https://shoppergpt-api-71971351019.us-central1.run.app/api/v0/product/search/textandcolor",
    headers={"X-Merchant-ID": "12345", "Content-Type": "application/json"},
    json={"query_str": "blazer for a wedding", "color": "navy", "limit": 8},
)

Errors

StatusResponse
400{ "detail": "Missing or invalid X-Merchant-ID header" }
400{ "detail": "At least one of 'text' or 'color' must be provided." }
422Validation error.
500Internal search error. Safe to retry with backoff.
POST /api/v1/product/search/image

Upload an image and get the closest visually matching products from the catalog. Internally, the image is embedded, its dominant colors extracted, and a hybrid vector + color similarity search is run.

Unlike the text endpoints, this is a multipart/form-data upload.

Auth modes supported

Today: Shopify-embedded only (CORS-enforced, X-Merchant-ID header). API key support coming soon.

Headers

HeaderRequiredValue
X-Merchant-IDyesYour merchant ID (integer)
Content-Typeyesmultipart/form-data (set by your client)

Form fields

FieldTypeRequiredDefaultDescription
filefileyesThe reference image (JPEG/PNG/WebP, up to 10 MB).
limitintegerno10Maximum number of results.
crop_valuesstringnoJSON string of {x, y, width, height} pixels to crop before embedding.
user_profile_idintegernoAttach results to a user profile.
user_namestringnoShopper first name, used in recommendation text.
conversational_stylestringnoStyle hint for the recommendation LLM.

Response — 200 OK

{
  "results": [
    {
      "product_id": "SKU-4471",
      "product_name": "Olive Utility Jacket",
      "price": "149.00",
      "price_numeric": 149.0,
      "image1": "https://cdn.example.com/images/olive-utility.jpg",
      "distance": 0.142
    }
  ],
  "recommendation": "Here are some olive-toned outerwear options with a similar silhouette.",
  "success": true
}

Note the shape difference: image search returns results as a flat array, not wrapped in results.data like the text endpoints. This is a legacy difference that will be reconciled in a future /api/v2/ namespace.

Example

curl -X POST \
  https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/product/search/image \
  -H "X-Merchant-ID: 12345" \
  -F "file=@reference.jpg" \
  -F "limit=8"
const form = new FormData();
form.append('file', fileInput.files[0]);
form.append('limit', '8');

const res = await fetch(
  'https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/product/search/image',
  {
    method: 'POST',
    headers: { 'X-Merchant-ID': '12345' },
    body: form,
  },
);
const data = await res.json();
console.log(`${data.results.length} visually similar products`);
import httpx

with open("reference.jpg", "rb") as f:
    r = httpx.post(
        "https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/product/search/image",
        headers={"X-Merchant-ID": "12345"},
        files={"file": ("reference.jpg", f, "image/jpeg")},
        data={"limit": 8},
    )
print(len(r.json()["results"]))

Errors

StatusResponse
400{ "detail": "Missing or invalid X-Merchant-ID header" }
422Validation error — usually a missing file.
500Internal error during image embedding. Safe to retry.

Text search cache check

POST /api/v0/product/search/text/cache-check

A lightweight companion to /api/v0/product/search/text that only checks whether a cached result exists for the given query — it does not run the search. Use this when you want to show a "loading…" state only if a real search is about to run.

Takes the same body as the text search endpoint so the cache key matches exactly.

Auth modes supported

Today: Shopify-embedded only. API key support coming soon.

Headers

HeaderRequiredValue
X-Merchant-IDyesYour merchant ID (integer)
Content-Typeyesapplication/json

Request body

Identical to /api/v0/product/search/text. The body is used only to compute the cache key — preferences, favorite_colors, price filters, and the query itself all contribute.

Response — 200 OK

Cache hit

{
  "cached": true,
  "result_count": 10,
  "cache_key": "comfortable linen shirt",
  "hits": 47
}

Cache miss

{
  "cached": false,
  "result_count": 0,
  "cache_key": "comfortable linen shirt"
}
FieldTypeDescription
cachedbooleanWhether a cached result exists.
result_countintegerNumber of products in the cached result set (0 on a miss).
cache_keystringThe cache key the server computed from your request body.
hitsintegerNumber of times the cached entry has been served (omitted on a miss).

Example

curl -X POST \
  https://shoppergpt-api-71971351019.us-central1.run.app/api/v0/product/search/text/cache-check \
  -H "X-Merchant-ID: 12345" \
  -H "Content-Type: application/json" \
  -d '{"query_str": "comfortable linen shirt"}'
const res = await fetch(
  'https://shoppergpt-api-71971351019.us-central1.run.app/api/v0/product/search/text/cache-check',
  {
    method: 'POST',
    headers: {
      'X-Merchant-ID': '12345',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ query_str: 'comfortable linen shirt' }),
  },
);
const { cached, result_count } = await res.json();
if (!cached) showLoadingState();
import httpx

r = httpx.post(
    "https://shoppergpt-api-71971351019.us-central1.run.app/api/v0/product/search/text/cache-check",
    headers={"X-Merchant-ID": "12345", "Content-Type": "application/json"},
    json={"query_str": "comfortable linen shirt"},
)
print(r.json())

Errors

StatusResponse
400{ "detail": "Missing or invalid X-Merchant-ID header" }
422Validation error — usually a missing query_str.

Text and color search cache check

POST /api/v0/product/search/textandcolor/cache-check

The cache-check companion to /api/v0/product/search/textandcolor. Returns whether a cached result exists for a given (query_str, color) combination without running the search.

Auth modes supported

Today: Shopify-embedded only. API key support coming soon.

Headers

HeaderRequiredValue
X-Merchant-IDyesYour merchant ID (integer)
Content-Typeyesapplication/json

Request body

Identical to /api/v0/product/search/textandcolor. At least one of query_str and color must be provided.

Response — 200 OK

Cache hit

{
  "cached": true,
  "result_count": 8,
  "cache_key": "blazer for a wedding|[0,0,128]",
  "operation_type": "textandcolor",
  "hits": 12
}

Cache miss

{
  "cached": false,
  "result_count": 0,
  "cache_key": "blazer for a wedding|[0,0,128]",
  "operation_type": "textandcolor"
}
FieldTypeDescription
cachedbooleanWhether a cached result exists.
result_countintegerNumber of products in the cached result set.
cache_keystringThe cache key the server computed (`normalized_query
operation_typestringtext, color, or textandcolor.
hitsintegerCached-entry hit count (omitted on a miss).

Example

curl -X POST \
  https://shoppergpt-api-71971351019.us-central1.run.app/api/v0/product/search/textandcolor/cache-check \
  -H "X-Merchant-ID: 12345" \
  -H "Content-Type: application/json" \
  -d '{"query_str": "blazer for a wedding", "color": "navy"}'
const res = await fetch(
  'https://shoppergpt-api-71971351019.us-central1.run.app/api/v0/product/search/textandcolor/cache-check',
  {
    method: 'POST',
    headers: {
      'X-Merchant-ID': '12345',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ query_str: 'blazer for a wedding', color: 'navy' }),
  },
);
const data = await res.json();
import httpx

r = httpx.post(
    "https://shoppergpt-api-71971351019.us-central1.run.app/api/v0/product/search/textandcolor/cache-check",
    headers={"X-Merchant-ID": "12345", "Content-Type": "application/json"},
    json={"query_str": "blazer for a wedding", "color": "navy"},
)
print(r.json())

Errors

StatusResponse
400{ "detail": "Missing or invalid X-Merchant-ID header" }
400{ "detail": "At least one of 'text' or 'color' must be provided." }
422Validation error.

Max product price

GET /api/v0/product/max-price

Returns the highest price_numeric value across the merchant's catalog. Used to size the upper bound of price range filters in the storefront UI.

Cached for 1 hour server-side.

Auth modes supported

Today: Shopify-embedded only. API key support coming soon.

Headers

HeaderRequiredValue
X-Merchant-IDyesYour merchant ID (integer)

Response — 200 OK

{
  "max_price": 799.0,
  "product_count": 4821
}
FieldTypeDescription
max_pricenumberHighest price_numeric across status = 'active' rows. 0 if empty.
product_countintegerTotal number of active products in the catalog.

Example

curl https://shoppergpt-api-71971351019.us-central1.run.app/api/v0/product/max-price \
  -H "X-Merchant-ID: 12345"
const res = await fetch(
  'https://shoppergpt-api-71971351019.us-central1.run.app/api/v0/product/max-price',
  { headers: { 'X-Merchant-ID': '12345' } },
);
const { max_price } = await res.json();
import httpx

r = httpx.get(
    "https://shoppergpt-api-71971351019.us-central1.run.app/api/v0/product/max-price",
    headers={"X-Merchant-ID": "12345"},
)
print(r.json()["max_price"])

Errors

StatusResponse
400{ "detail": "Missing or invalid X-Merchant-ID header" }
500Internal database error. Safe to retry with backoff.

User API

Create user profile

POST /api/v1/user/profile

Creates a new user profile for this merchant, or returns the existing row if one already exists with the same email. Profiles are unique on (merchant_id, email_id).

Auth modes supported

Today: Shopify-embedded only. API key support coming soon.

Headers

HeaderRequiredValue
X-Merchant-IDyesYour merchant ID (integer)
Content-Typeyesapplication/json

Request body

FieldTypeRequiredDescription
user_namestringyesDisplay name.
first_namestringyesFirst name.
last_namestringyesLast name.
email_idstringyesEmail address (unique per merchant).
profile_namestringnoOptional profile label.

Response — 200 OK

{
  "id": 812,
  "user_name": "alex",
  "first_name": "Alex",
  "last_name": "Martinez",
  "email_id": "alex@example.com",
  "profile_name": null
}

Example

curl -X POST \
  https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/user/profile \
  -H "X-Merchant-ID: 12345" \
  -H "Content-Type: application/json" \
  -d '{
    "user_name": "alex",
    "first_name": "Alex",
    "last_name": "Martinez",
    "email_id": "alex@example.com"
  }'
const res = await fetch(
  'https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/user/profile',
  {
    method: 'POST',
    headers: {
      'X-Merchant-ID': '12345',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      user_name: 'alex',
      first_name: 'Alex',
      last_name: 'Martinez',
      email_id: 'alex@example.com',
    }),
  },
);
const profile = await res.json();
import httpx

r = httpx.post(
    "https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/user/profile",
    headers={"X-Merchant-ID": "12345", "Content-Type": "application/json"},
    json={
        "user_name": "alex",
        "first_name": "Alex",
        "last_name": "Martinez",
        "email_id": "alex@example.com",
    },
)
print(r.json())

Errors

StatusResponse
400{ "detail": "Missing or invalid X-Merchant-ID header" }
422Validation error — missing required fields.
500Database error. Safe to retry.

Get profile by email

POST /api/v1/user/profile/email

Returns the user profile matching the supplied email within the authenticated merchant. This is a POST (not a GET) so the email can be passed in a JSON body rather than a query string — keeps shopper PII out of URLs and logs.

Returns 404 if no profile with that email exists.

Auth modes supported

Today: Shopify-embedded only. API key support coming soon.

Headers

HeaderRequiredValue
X-Merchant-IDyesYour merchant ID (integer)
Content-Typeyesapplication/json

Request body

FieldTypeRequiredDescription
email_idstringyesEmail address (valid format).

Response — 200 OK

{
  "id": 812,
  "user_name": "alex",
  "first_name": "Alex",
  "last_name": "Martinez",
  "email_id": "alex@example.com",
  "profile_name": null
}

Example

curl -X POST \
  https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/user/profile/email \
  -H "X-Merchant-ID: 12345" \
  -H "Content-Type: application/json" \
  -d '{"email_id": "alex@example.com"}'
const res = await fetch(
  'https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/user/profile/email',
  {
    method: 'POST',
    headers: {
      'X-Merchant-ID': '12345',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ email_id: 'alex@example.com' }),
  },
);
if (res.status === 404) {
  // Profile doesn't exist yet — create one.
}
import httpx

r = httpx.post(
    "https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/user/profile/email",
    headers={"X-Merchant-ID": "12345", "Content-Type": "application/json"},
    json={"email_id": "alex@example.com"},
)
if r.status_code == 404:
    ...  # Profile not found

Errors

StatusResponse
400{ "detail": "Missing or invalid X-Merchant-ID header" }
404{ "detail": "User profile not found" }
422Validation error — invalid email format.

Create preferences

POST /api/v1/user/preferences

Creates a user_preferences row for a user profile. There can be only one preferences row per user_profile_id.

If a row already exists, use POST /api/v1/user/preferences/update instead.

Auth modes supported

Today: Shopify-embedded only. API key support coming soon.

Headers

HeaderRequiredValue
X-Merchant-IDyesYour merchant ID (integer)
Content-Typeyesapplication/json

Request body

FieldTypeRequiredDescription
user_profile_idintegeryesForeign key to user_profile.id.
conversational_stylestringnoFree text (friendly, concise, technical, …).
personal_fashion_stylestringnoFree text (minimalist, streetwear, classic, …).
favorite_colorsstring[]noPreferred colors.
fashion_material_preferstring[]noPreferred materials.
body_typestringno
fashion_goalsstringno
age_groupstringno

Response — 200 OK

Returns the stored preferences row including server-managed fields (id, is_modified, product_ids).

{
  "id": 43,
  "user_profile_id": 812,
  "conversational_style": "friendly",
  "personal_fashion_style": "classic",
  "favorite_colors": ["navy", "olive"],
  "fashion_material_prefer": ["linen", "cotton"],
  "body_type": null,
  "fashion_goals": "beach vacation outfits",
  "age_group": null,
  "is_modified": false,
  "product_ids": null
}

Example

curl -X POST \
  https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/user/preferences \
  -H "X-Merchant-ID: 12345" \
  -H "Content-Type: application/json" \
  -d '{
    "user_profile_id": 812,
    "conversational_style": "friendly",
    "personal_fashion_style": "classic",
    "favorite_colors": ["navy", "olive"],
    "fashion_material_prefer": ["linen", "cotton"],
    "fashion_goals": "beach vacation outfits"
  }'
await fetch(
  'https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/user/preferences',
  {
    method: 'POST',
    headers: {
      'X-Merchant-ID': '12345',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      user_profile_id: 812,
      favorite_colors: ['navy', 'olive'],
      fashion_material_prefer: ['linen', 'cotton'],
    }),
  },
);
import httpx

httpx.post(
    "https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/user/preferences",
    headers={"X-Merchant-ID": "12345", "Content-Type": "application/json"},
    json={
        "user_profile_id": 812,
        "favorite_colors": ["navy", "olive"],
        "fashion_material_prefer": ["linen", "cotton"],
    },
)

Errors

StatusResponse
400{ "detail": "Missing or invalid X-Merchant-ID header" }
422Validation error — missing user_profile_id.
500Database error, likely a unique-constraint violation (preferences already exist for this profile — use update instead).

Update preferences

POST /api/v1/user/preferences/update

Updates an existing user_preferences row. Fields omitted from the request are left unchanged.

Sets is_modified = true to mark that the user has explicitly edited their preferences — useful for distinguishing user-set from defaulted values.

Auth modes supported

Today: Shopify-embedded only. API key support coming soon.

Headers

HeaderRequiredValue
X-Merchant-IDyesYour merchant ID (integer)
Content-Typeyesapplication/json

Request body

Same shape as POST /api/v1/user/preferences. user_profile_id is required — everything else is optional.

Response — 200 OK

Returns the updated row.

{
  "id": 43,
  "user_profile_id": 812,
  "conversational_style": "concise",
  "favorite_colors": ["forest green", "charcoal"],
  "is_modified": true,
  ...
}

Example

curl -X POST \
  https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/user/preferences/update \
  -H "X-Merchant-ID: 12345" \
  -H "Content-Type: application/json" \
  -d '{
    "user_profile_id": 812,
    "conversational_style": "concise",
    "favorite_colors": ["forest green", "charcoal"]
  }'
await fetch(
  'https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/user/preferences/update',
  {
    method: 'POST',
    headers: {
      'X-Merchant-ID': '12345',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      user_profile_id: 812,
      conversational_style: 'concise',
    }),
  },
);
import httpx

httpx.post(
    "https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/user/preferences/update",
    headers={"X-Merchant-ID": "12345", "Content-Type": "application/json"},
    json={"user_profile_id": 812, "conversational_style": "concise"},
)

Errors

StatusResponse
400{ "detail": "Missing or invalid X-Merchant-ID header" }
404Preferences row not found for user_profile_id.
422Validation error.

Get preferences

GET /api/v1/user/preferences/{user_profile_id}

Returns the user_preferences row for the given user_profile_id. Returns 404 if the profile has no preferences row yet.

Cached for 5 minutes per user_profile_id.

Auth modes supported

Today: Shopify-embedded only. API key support coming soon.

Headers

HeaderRequiredValue
X-Merchant-IDyesYour merchant ID (integer)

Path parameters

ParameterTypeDescription
user_profile_idintegerThe user profile's primary key.

Response — 200 OK

{
  "id": 43,
  "user_profile_id": 812,
  "conversational_style": "friendly",
  "personal_fashion_style": "classic",
  "favorite_colors": ["navy", "olive"],
  "fashion_material_prefer": ["linen", "cotton"],
  "body_type": null,
  "fashion_goals": "beach vacation outfits",
  "age_group": null,
  "is_modified": true,
  "product_ids": null
}

Example

curl https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/user/preferences/812 \
  -H "X-Merchant-ID: 12345"
const res = await fetch(
  `https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/user/preferences/${userProfileId}`,
  { headers: { 'X-Merchant-ID': '12345' } },
);
if (res.status === 404) {
  // No preferences set yet — show the onboarding form.
}
import httpx

r = httpx.get(
    f"https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/user/preferences/{user_profile_id}",
    headers={"X-Merchant-ID": "12345"},
)
if r.status_code == 404:
    ...  # no preferences yet

Errors

StatusResponse
400{ "detail": "Missing or invalid X-Merchant-ID header" }
404{ "detail": "User preferences not found" }

Random data by preferences

POST /api/v1/user/preferences/randomdata

Returns a batch of products seeded from a user's stored preferences, plus a time-of-day appropriate greeting. Used to populate the cold-start state of the storefront chat widget (before the shopper has typed a query).

Auth modes supported

Today: Shopify-embedded only. API key support coming soon.

Headers

HeaderRequiredValue
X-Merchant-IDyesYour merchant ID (integer)
Content-Typeyesapplication/json

Request body

FieldTypeRequiredDescription
user_profile_idintegeryesThe user profile whose preferences should seed the results.
userPrefsNotPresentbooleanyesHint the server that the user has no stored preferences.
current_hourintegeryesLocal hour (0–23), used to pick a time-of-day greeting.
user_namestringyesShopper first name.
limitintegeryesMaximum number of products to return.
conversational_stylestringnoStyle hint.
personal_fashion_stylestringno
favorite_colorsstring[]no
fashion_material_preferstring[]no
body_typestringno
fashion_goalsstringno
age_groupstringno

Response — 200 OK

{
  "results": {
    "greeting": "Good afternoon, Alex",
    "data": [
      {
        "product_id": "SKU-2301",
        "product_name": "Oversized Boyfriend Tee",
        "price_numeric": 42.0,
        "image1": "https://cdn.example.com/images/boyfriend-tee.jpg"
      }
    ]
  }
}
FieldTypeDescription
results.greetingstringA time-of-day greeting. null if no greeting template matched.
results.dataobject[]Randomized preference-aware products.

Example

curl -X POST \
  https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/user/preferences/randomdata \
  -H "X-Merchant-ID: 12345" \
  -H "Content-Type: application/json" \
  -d '{
    "user_profile_id": 812,
    "userPrefsNotPresent": false,
    "current_hour": 14,
    "user_name": "Alex",
    "limit": 12
  }'
const res = await fetch(
  'https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/user/preferences/randomdata',
  {
    method: 'POST',
    headers: {
      'X-Merchant-ID': '12345',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      user_profile_id: 812,
      userPrefsNotPresent: false,
      current_hour: new Date().getHours(),
      user_name: 'Alex',
      limit: 12,
    }),
  },
);
import httpx

httpx.post(
    "https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/user/preferences/randomdata",
    headers={"X-Merchant-ID": "12345", "Content-Type": "application/json"},
    json={
        "user_profile_id": 812,
        "userPrefsNotPresent": False,
        "current_hour": 14,
        "user_name": "Alex",
        "limit": 12,
    },
)

Errors

StatusResponse
400{ "detail": "Missing or invalid X-Merchant-ID header" }
422Validation error — missing required fields.

Pipeline API

Create pipeline status

POST /api/v1/pipeline/status

Creates a new pipeline_status row for this merchant. If a row already exists and current_step is the first step (shopify_etl_dev), the existing row is resetcompleted_steps and failed_steps are cleared and status returns to running. This is how reinstalls are handled.

The merchant_id in the request body must match the authenticated X-Merchant-ID header.

Auth modes supported

Today: Shopify-embedded only. API key support coming soon.

Headers

HeaderRequiredValue
X-Merchant-IDyesYour merchant ID (integer)
Content-Typeyesapplication/json

Request body

FieldTypeRequiredDescription
merchant_idintegeryesMust match X-Merchant-ID header.
current_stepstringnoInitial step name (default initialized). Use shopify_etl_dev for a fresh install.
shop_domainstringnoOptional source-platform domain (e.g. store.myshopify.com).
configobjectnoArbitrary configuration carried through the pipeline.

Response — 200 OK

Returns the full PipelineStatusResponse.

{
  "merchant_id": 12345,
  "shop_domain": "store.myshopify.com",
  "current_step": "shopify_etl_dev",
  "completed_steps": [],
  "failed_steps": [],
  "status": "running",
  "started_at": "2026-04-10T16:14:22.109Z",
  "completed_at": null,
  "last_updated": "2026-04-10T16:14:22.109Z",
  "error_message": null,
  "progress_percentage": 0.0,
  "estimated_completion": null
}

Example

curl -X POST \
  https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/pipeline/status \
  -H "X-Merchant-ID: 12345" \
  -H "Content-Type: application/json" \
  -d '{
    "merchant_id": 12345,
    "current_step": "shopify_etl_dev",
    "shop_domain": "store.myshopify.com"
  }'
await fetch(
  'https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/pipeline/status',
  {
    method: 'POST',
    headers: {
      'X-Merchant-ID': '12345',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      merchant_id: 12345,
      current_step: 'shopify_etl_dev',
    }),
  },
);
import httpx

httpx.post(
    "https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/pipeline/status",
    headers={"X-Merchant-ID": "12345", "Content-Type": "application/json"},
    json={"merchant_id": 12345, "current_step": "shopify_etl_dev"},
)

Errors

StatusResponse
400{ "detail": "Missing or invalid X-Merchant-ID header" }
400{ "detail": "Merchant ID in header must match merchant_id in request body" }
500Database error. Safe to retry.

Get pipeline status

GET /api/v1/pipeline/status/{merchant_id}

Returns the current pipeline_status for this merchant — current step, completed steps, failure state, and a calculated progress_percentage.

The merchant_id in the path must match the authenticated X-Merchant-ID header. Cross-merchant access returns 403.

Auth modes supported

Today: Shopify-embedded only. API key support coming soon.

Headers

HeaderRequiredValue
X-Merchant-IDyesYour merchant ID (integer)

Path parameters

ParameterTypeDescription
merchant_idintegerThe merchant whose status to read. Must match header.

Response — 200 OK

{
  "merchant_id": 12345,
  "shop_domain": "store.myshopify.com",
  "current_step": "create_text_embeddings",
  "completed_steps": [
    "shopify_etl_dev",
    "enrichment_dev",
    "create_color_embeddings",
    "add_color_names"
  ],
  "failed_steps": [],
  "status": "running",
  "started_at": "2026-04-10T16:14:22.109Z",
  "completed_at": null,
  "last_updated": "2026-04-10T16:21:03.447Z",
  "error_message": null,
  "progress_percentage": 57.14,
  "estimated_completion": "2026-04-10T16:28:40.000Z"
}

Example

curl https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/pipeline/status/12345 \
  -H "X-Merchant-ID: 12345"
const res = await fetch(
  `https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/pipeline/status/${merchantId}`,
  { headers: { 'X-Merchant-ID': String(merchantId) } },
);
const status = await res.json();
console.log(`${status.progress_percentage.toFixed(0)}% complete`);
import httpx

r = httpx.get(
    f"https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/pipeline/status/{merchant_id}",
    headers={"X-Merchant-ID": str(merchant_id)},
)
status = r.json()

Errors

StatusResponse
400{ "detail": "Missing or invalid X-Merchant-ID header" }
403{ "detail": "Access denied to this merchant's pipeline status" }
404{ "detail": "Pipeline status not found for this merchant" }
500Database error. Safe to retry with backoff.

Update pipeline status

PUT /api/v1/pipeline/status/{merchant_id}

Updates a merchant's pipeline_status row. Any non-null field in the request body is applied; omitted fields are left unchanged.

The merchant_id in the path must match the authenticated X-Merchant-ID.

Auth modes supported

Today: Shopify-embedded only. API key support coming soon.

Typical caller

This endpoint is primarily called by internal pipeline workers as they complete steps. Admin UIs read status via GET /api/v1/pipeline/status/{merchant_id} but rarely write here directly.

Headers

HeaderRequiredValue
X-Merchant-IDyesYour merchant ID (integer)
Content-Typeyesapplication/json

Path parameters

ParameterTypeDescription
merchant_idintegerThe merchant whose status to update. Must match header.

Request body

FieldTypeRequiredDescription
current_stepstringnoNew current step. Also appended to completed_steps if not already there.
completed_stepstringnoExplicitly append a step to completed_steps (legacy path from theme extension).
statusstringnorunning, completed, or failed. Setting completed stamps completed_at.
error_messagestringnoError detail. Setting this automatically switches status to failed.
configobjectnoPartial config patch — merged onto existing config.

Response — 200 OK

Returns the updated PipelineStatusResponse.

Example

curl -X PUT \
  https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/pipeline/status/12345 \
  -H "X-Merchant-ID: 12345" \
  -H "Content-Type: application/json" \
  -d '{"current_step": "create_img_embeddings"}'
await fetch(
  `https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/pipeline/status/${merchantId}`,
  {
    method: 'PUT',
    headers: {
      'X-Merchant-ID': String(merchantId),
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ current_step: 'create_img_embeddings' }),
  },
);
import httpx

httpx.put(
    f"https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/pipeline/status/{merchant_id}",
    headers={"X-Merchant-ID": str(merchant_id), "Content-Type": "application/json"},
    json={"current_step": "create_img_embeddings"},
)

Errors

StatusResponse
400{ "detail": "Missing or invalid X-Merchant-ID header" }
403{ "detail": "Access denied to this merchant's pipeline status" }
404{ "detail": "Pipeline status not found for this merchant" }
500Database error. Safe to retry.

Delete pipeline status

DELETE /api/v1/pipeline/status/{merchant_id}

Deletes a merchant's pipeline_status row. Typically used during uninstall or when forcing a clean reset before reinstalling.

The merchant_id in the path must match the authenticated X-Merchant-ID.

Auth modes supported

Today: Shopify-embedded only. API key support coming soon.

Does not delete catalog data

This endpoint only removes the pipeline tracking row. It does not delete products, user profiles, or preferences. For that, see DELETE /api/v1/merchant/{merchant_id}/data.

Headers

HeaderRequiredValue
X-Merchant-IDyesYour merchant ID (integer)

Path parameters

ParameterTypeDescription
merchant_idintegerThe merchant whose status to delete. Must match header.

Response — 200 OK

When a row existed

{
  "status": "deleted",
  "message": "Pipeline status deleted for merchant 12345"
}

When no row existed

{
  "status": "not_found",
  "message": "No pipeline status found for merchant 12345"
}

Example

curl -X DELETE \
  https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/pipeline/status/12345 \
  -H "X-Merchant-ID: 12345"
await fetch(
  `https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/pipeline/status/${merchantId}`,
  {
    method: 'DELETE',
    headers: { 'X-Merchant-ID': String(merchantId) },
  },
);
import httpx

httpx.delete(
    f"https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/pipeline/status/{merchant_id}",
    headers={"X-Merchant-ID": str(merchant_id)},
)

Errors

StatusResponse
400{ "detail": "Missing or invalid X-Merchant-ID header" }
403{ "detail": "Access denied to this merchant's pipeline status" }
500Database error. Safe to retry.

Merchant API

Delete merchant data

DELETE /api/v1/merchant/{merchant_id}/data

Deletes all merchant-specific rows from the product database — products, user profiles, user preferences, user greetings, product synonyms, and pipeline status. This is used during an uninstall when the merchant has explicitly requested data deletion.

Auth modes supported

Today: Shopify-embedded only. API key support coming soon.

Disabled by default

This endpoint is gated by the server-side DELETE_MERCHANT_DATA_ON_UNINSTALL environment flag. If that flag is not true, every call returns 403 without deleting anything. Contact enterprise@vairetail.com to enable deletion for your merchant account.

What is NOT deleted

  • Merchant metadata (merchant_info, credentials) is preserved so reinstalling is zero-friction.
  • search_results rows are preserved for analytics and historical reporting. They are query logs, not personal data.

Headers

HeaderRequiredValue
X-Merchant-IDyesYour merchant ID (integer)

Path parameters

ParameterTypeDescription
merchant_idintegerThe merchant whose data to delete. Must match header.

Response — 200 OK

{
  "status": "deleted",
  "message": "All merchant data deleted for merchant 12345",
  "deleted_counts": {
    "user_preferences": 42,
    "user_greetings": 6,
    "user_profile": 41,
    "product_synonyms": 188,
    "products": 4821,
    "pipeline_status": 1
  }
}

Example

curl -X DELETE \
  https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/merchant/12345/data \
  -H "X-Merchant-ID: 12345"
const res = await fetch(
  `https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/merchant/${merchantId}/data`,
  {
    method: 'DELETE',
    headers: { 'X-Merchant-ID': String(merchantId) },
  },
);
import httpx

httpx.delete(
    f"https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/merchant/{merchant_id}/data",
    headers={"X-Merchant-ID": str(merchant_id)},
)

Errors

StatusResponse
400{ "detail": "Missing or invalid X-Merchant-ID header" }
400{ "detail": "Invalid merchant_id: <id>" } — non-positive ID
403{ "detail": "Merchant data deletion is disabled. Set DELETE_MERCHANT_DATA_ON_UNINSTALL=true…" }
403{ "detail": "Access denied to this merchant's data" }
500{ "detail": "RLS context setup failed: …" } — safety check tripped, nothing was deleted

Utilities

Image proxy

GET /api/v1/proxy-image

Fetches an image URL server-side and returns its bytes with the original Content-Type. Used by the storefront extension to load product images into canvas elements without running into cross-origin restrictions on the CDN.

Auth modes supported

No auth required. This endpoint is a stateless utility — no X-Merchant-ID header, no API key.

Query parameters

ParameterTypeRequiredDescription
urlstringyesThe image URL to fetch. Must be URL-encoded.

Response — 200 OK

The raw image bytes with the upstream Content-Type (e.g. image/jpeg, image/png, image/webp). No JSON wrapping.

Example

curl "https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/proxy-image?url=https%3A%2F%2Fcdn.example.com%2Fimages%2Flinen-shirt.jpg" \
  --output linen-shirt.jpg
const imageUrl = 'https://cdn.example.com/images/linen-shirt.jpg';
const proxied = `https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/proxy-image?url=${encodeURIComponent(imageUrl)}`;
const res = await fetch(proxied);
const blob = await res.blob();
const objectUrl = URL.createObjectURL(blob);
document.getElementById('preview').src = objectUrl;
import httpx

r = httpx.get(
    "https://shoppergpt-api-71971351019.us-central1.run.app/api/v1/proxy-image",
    params={"url": "https://cdn.example.com/images/linen-shirt.jpg"},
)
with open("linen-shirt.jpg", "wb") as f:
    f.write(r.content)

Errors

StatusResponse
502{ "detail": "Failed to fetch image from source." }

Health check

GET /health

Returns { "status": "ok" } as soon as the API is ready to serve requests. Models are loaded at process startup (not lazily), so a successful /health response implies the text and image embedding services are warm.

Auth modes supported

No auth required. Intended for uptime monitors and load balancer health probes — no X-Merchant-ID header, no API key.

Response — 200 OK

{ "status": "ok" }

Example

curl https://shoppergpt-api-71971351019.us-central1.run.app/health
const res = await fetch(
  'https://shoppergpt-api-71971351019.us-central1.run.app/health',
);
if (!res.ok) alert('shopperGPT API is down');
import httpx

r = httpx.get("https://shoppergpt-api-71971351019.us-central1.run.app/health")
assert r.status_code == 200

Errors

Any non-200 response (5xx, network error, timeout) means the API is not ready or is experiencing an outage. Exponentially back off retries.

Changelog

2026-04-10 — Initial public docs

  • Launched docs.vairetail.com.
  • Documented 20 in-service endpoints across Search, User, Pipeline, Merchant, and Utilities.
  • Published the canonical Data Model and accepted CSV / NDJSON formats.
  • Shipped the client-side Column Mapper.

Coming soon

  • POST /api/v1/ingest/products — push catalog ingest (private beta).
  • API key authentication alongside X-Merchant-ID.
  • Non-Shopify merchant onboarding self-service.

Support

Email

enterprise@vairetail.com — integration questions, onboarding, private-beta access to push ingest and API keys, bug reports.

When reporting an issue, include:

  1. The X-Request-ID header from the failing response (every response includes one).
  2. The full request body you sent (with PII redacted).
  3. The response status code and body.
  4. Your merchant ID.

Marketing site

vairetail.com — product overview, case studies, and scheduling a demo.