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
Quickstart
Make your first search request in under a minute with curl.
Authentication
Learn the X-Merchant-ID header and how merchant IDs are issued.
Data Model
The canonical products schema — what fields we expect from any source.
API Reference
Every in-service endpoint, with request/response schemas and code samples.
Base URL
All API requests go to:
https://shoppergpt-api-71971351019.us-central1.run.appThis 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
Authentication
Full details on X-Merchant-ID, trust model, and future API keys.
API Reference
Every endpoint, request/response schemas, and code samples.
Errors
Error shapes and how to handle them.
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: 12345If 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 checkGET /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...redactedAPI keys will be:
- Issued per-merchant via a developer console
- Scoped per environment (
sk_test_…vssk_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
| Status | Meaning |
|---|---|
| 200 | Success. |
| 400 | Bad request. Missing or malformed X-Merchant-ID, missing required body fields, invalid color input, or a search body with neither text nor color. |
| 403 | Forbidden. The authenticated merchant does not have access to the target resource (e.g. calling another merchant's pipeline status). |
| 404 | Not found. A referenced user profile, pipeline status, or resource does not exist for this merchant. |
| 422 | Unprocessable entity. FastAPI schema validation failed (wrong field types, missing required fields). |
| 500 | Internal server error. A bug or downstream failure. Retry with backoff and contact support if it persists. |
| 502 | Bad 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-b1d3a2a0f0abData 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
| Field | Type | Required | Constraints | Notes |
|---|---|---|---|---|
product_id | string | yes | Unique per merchant, ≤ 2048 chars | Opaque stable ID from your source system — SKU, UUID, slug, or numeric ID, your choice. |
product_name | string | yes | Plaintext | Shopify title, Woo name, custom productName, etc. |
handle | string | recommended | URL-safe slug, lowercase, hyphenated | Storefront URL slug. |
product_details | string | recommended | Plaintext or HTML (HTML is stripped during enrich) | Long description. Drives semantic text search. |
brand | string | optional | Plaintext | Shopify vendor. |
category | string | recommended | Free text (tops, dresses, accessories) | Enrichment fills this in if missing. |
sub_category | string | optional | Free text | |
color | string | optional | Free text (navy blue) or hex (#0a1f44) | The color pipeline will derive color from image1 if this is missing. |
tags | string | optional | Comma-separated | |
price | string | yes | Currency-formatted string, e.g. "29.99" | Display field. Must agree with price_numeric. |
price_numeric | float | yes | Non-negative decimal | Used for range filters, min/max price queries, and price-aware ranking. |
variant_compare_at_price | string | optional | Currency string | Original / MSRP price for sale display. |
option1_name | string | optional | e.g. "Size", "Color" | First variant option label. |
gender | enum | optional | mens | womens | unisex | kids | Enrichment will fill this from product_name / product_details if missing. |
image1 | string | yes | Publicly reachable HTTPS URL, ≤ 10 MB, CORS-readable | The primary product image. All image embeddings and color extraction are computed from this URL. |
image1_description | string | optional | Plaintext | If absent, enrichment generates a description via a vision model. |
status | enum | optional | active | draft | archived (default active) | Only active rows are searchable. |
display_flag | boolean | optional | Default true | Soft-hide: set to false to exclude from search without deleting. |
metadata_json | object | optional | Arbitrary JSON | Free-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.
| Field | Populated by |
|---|---|
text_embedding | Text embedding pipeline (create_text_embeddings) |
text_embedding_1 | Text embedding pipeline |
img_embedding | Image embedding pipeline (create_img_embeddings) |
image_desc_embed | Vision description pipeline |
color_embedding | Color pipeline (create_color_embeddings) |
color_embedding_1..3 | Color pipeline |
color_hsv_embedding_1..3 | Color pipeline |
percent_1..3 | Color pipeline (dominant-color coverage percentages) |
merchant_id | Set server-side from the authenticated identity. |
last_updated | Database-managed timestamp. |
id | Database-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.
Related reading
- CSV format — headers, sample file, type rules.
- JSON / NDJSON format — shapes for HTTP ingest.
- Column Mapper — paste your source headers, get a canonical mapping.
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
| Field | Type | Required | Notes |
|---|---|---|---|
id | integer | server | Auto-generated primary key. |
user_name | string | yes | Display name (first name is acceptable). |
first_name | string | yes | First name. |
last_name | string | yes | Last name. |
email_id | string | yes | Email address. Unique within a merchant. |
profile_name | string | optional | A 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.
Related endpoints
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
| Field | Type | Required | Notes |
|---|---|---|---|
id | integer | server | Auto-generated primary key. |
user_profile_id | integer | yes | Foreign key to user_profile.id. Unique — one preferences row per user. |
conversational_style | string | optional | Free text (concise, friendly, technical, …). Biases recommendation tone. |
personal_fashion_style | string | optional | Free text (minimalist, streetwear, classic, …). |
favorite_colors | array of strings | optional | E.g. ["navy", "olive"]. Influences color-aware ranking. |
fashion_material_prefer | array of strings | optional | E.g. ["linen", "cotton"]. |
body_type | string | optional | Free text. |
fashion_goals | string | optional | Free text ("dress for job interviews", "beach vacation outfits"). |
age_group | string | optional | Free text. |
is_modified | boolean | server | true after a user has explicitly edited their preferences. |
product_ids | array of integers | server | Internal — 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:
- Feeds
conversational_styleinto the recommendation LLM. - Adds
favorite_colorsandfashion_material_preferas soft ranking signals — products matching preferred attributes are nudged up. - Uses
body_type,age_group, andpersonal_fashion_styleas context in the recommendation text.
Preferences are never used as a hard filter — they influence ranking, they don't exclude products.
Related endpoints
POST /api/v1/user/preferencesPOST /api/v1/user/preferences/updateGET /api/v1/user/preferences/{user_profile_id}POST /api/v1/user/preferences/randomdata
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
| Field | Type | Notes |
|---|---|---|
merchant_id | integer | The merchant this pipeline run belongs to. |
shop_domain | string | Optional source-platform domain (e.g. store.myshopify.com). Informational. |
current_step | string | The step currently running. See Pipeline steps below. |
completed_steps | array of string | Steps that have completed successfully, in order. |
failed_steps | array of string | Steps that failed. Parallel to completed_steps. |
status | string | running | completed | failed. |
started_at | datetime (UTC) | When the run started. |
completed_at | datetime (UTC) | When the run finished. null while running. |
last_updated | datetime (UTC) | When the row was most recently touched. |
error_message | string | Populated when status = failed. |
config | object | Source-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:
| Field | Type | Notes |
|---|---|---|
progress_percentage | float (0–100) | completed_steps.length / total_steps * 100. |
estimated_completion | datetime (UTC) | Heuristic estimate based on average step duration. |
Pipeline steps
The default dev pipeline has these steps, in order:
shopify_etl_dev— initial catalog import (Shopify merchants only today)enrichment_dev— description enrichment, category classification, gender inferencecreate_color_embeddings— extract and embed dominant colorsadd_color_names— map color embeddings to human-readable color namescreate_text_embeddings— generate semantic text embeddingscreate_img_embeddings— generate image similarity embeddingsdeploy_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:
- Run the mapping yourself (Node or Python snippets) to produce a canonical CSV, or
- Ship
mapping.jsonalongside 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,activeDownload the full sample: products.csv
Validation rules
During ingest shopperGPT applies the following rules to every row:
product_id,product_name,price,price_numeric, andimage1must be present and non-empty.price_numericmust parse as a non-negative float.image1must be a valid HTTPS URL.status, if provided, must be one ofactive,draft,archived.display_flag, if provided, must betrueorfalse(case-insensitive).gender, if provided, must be one ofmens,womens,unisex,kids.- Any additional columns not in the canonical schema are placed into
metadata_jsonas 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:
pricemust be a JSON string;price_numericmust 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_flagmust be a JSON boolean, not"true"as a string. - Arrays:
tagsmust 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_embedcolor_embedding,color_embedding_1,color_embedding_2,color_embedding_3color_hsv_embedding_1,color_hsv_embedding_2,color_hsv_embedding_3percent_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
- You install the shopperGPT app from the Shopify App Store.
- You grant the app read access to your products and storefront.
- shopperGPT pulls your product catalog via the Shopify Admin API and
populates the canonical
productstable. - The enrichment pipeline runs: categories, color extraction, embeddings.
- shopperGPT installs a theme extension that wires the search UI into your storefront. Users immediately start seeing semantic search.
- 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 |
|---|---|
id | product_id |
title | product_name |
handle | handle |
body_html | product_details (HTML stripped on enrich) |
vendor | brand |
product_type | category |
tags | tags |
variants[0].price | price / price_numeric |
variants[0].compare_at_price | variant_compare_at_price |
options[0].name | option1_name |
images[0].src | image1 |
status | status |
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)
| Field | Type | Description |
|---|---|---|
mode | string | upsert (default) or replace. replace deletes rows absent from the payload. |
dry_run | boolean | If true, validate without writing and return a per-row report. |
mapping | object | A 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.ndjsonOther platforms
If your catalog lives somewhere other than Shopify, the path today is:
- Export your catalog to CSV or NDJSON.
- Use the Column Mapper to map your source columns to the
canonical
productsschema. - Apply the mapping to produce a canonical file (Node or Python snippets).
- 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 field | Canonical products field |
|---|---|
id or sku | product_id |
name | product_name |
custom_url.url | handle |
description | product_details |
brand_name | brand |
categories | category |
price | price_numeric |
retail_price | variant_compare_at_price |
primary_image.url_standard | image1 |
is_visible | display_flag |
WooCommerce
| WooCommerce field | Canonical products field |
|---|---|
id or sku | product_id |
name | product_name |
slug | handle |
description | product_details |
| (attribute) brand | brand |
categories[0].name | category |
price | price_numeric |
regular_price | variant_compare_at_price |
tags[].name | tags (comma-joined) |
images[0].src | image1 |
status | status |
Magento
| Magento field | Canonical products field |
|---|---|
sku | product_id |
name | product_name |
url_key | handle |
description | product_details |
brand (custom attr) | brand |
category_names | category |
price | price_numeric |
base_image | image1 |
status | status |
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-parseScript
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.ndjsonA 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/queryresponseGenerates 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
| Header | Required | Value |
|---|---|---|
X-Merchant-ID | yes | Your merchant ID (integer) |
Content-Type | yes | application/json |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
query_str | string | yes | The shopper's search query in plain language. |
preferences | boolean | no | If true, merge the user's stored preferences into the prompt. |
userPrefsNotPresent | boolean | no | Hint that the user profile has no preferences yet. |
current_hour | integer | no | Local hour (0–23) — used to pick a time-of-day greeting. |
user_name | string | no | Shopper first name. |
color_input | string or object | no | Optional color (named or hex). When set, the recommendation is color-aware. |
user_profile_id | integer | no | Attach this query to a known user profile. |
conversational_style | string | no | Style hint for the LLM (friendly, concise, technical, …). |
personal_fashion_style | string | no | Fashion style hint. |
favorite_colors | string[] | no | Soft color preferences. |
fashion_material_prefer | string[] | no | Preferred materials. |
body_type | string | no | |
fashion_goals | string | no | |
age_group | string | no | |
session_id | string | no | Opaque session identifier for analytics. |
client_ip | string | no | Client IP (otherwise inferred server-side). |
location | string | no | City/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
| Status | Response |
|---|---|
| 400 | { "detail": "Missing or invalid X-Merchant-ID header" } |
| 422 | Validation error — usually a missing query_str. |
| 500 | Internal error reaching the LLM. Safe to retry with backoff. |
Text search
POST /api/v0/product/search/textThe 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
| Header | Required | Value |
|---|---|---|
X-Merchant-ID | yes | Your merchant ID (integer) |
Content-Type | yes | application/json |
Request body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
query_str | string | yes | — | The shopper's search query. May contain price phrases like "under $80". |
limit | integer | no | 10 | Maximum number of results. |
preferences | boolean | no | false | If true, blend the user's stored preferences into ranking. |
user_profile_id | integer | no | — | Attach results to a user profile for analytics. |
user_name | string | no | — | Shopper first name (used in the recommendation). |
conversational_style | string | no | — | Style hint for the recommendation LLM. |
personal_fashion_style | string | no | — | Fashion style hint. |
favorite_colors | string[] | no | — | Soft color preferences — nudge ranking toward matching colors. |
fashion_material_prefer | string[] | no | — | Preferred materials. |
body_type | string | no | — | |
fashion_goals | string | no | — | |
age_group | string | no | — | |
gender | string[] | no | — | Filter: any of mens, womens, unisex, kids. |
min_price | number | no | — | Explicit minimum price filter (takes precedence over in-query price phrases). |
max_price | number | no | — | Explicit maximum price filter. |
session_id, client_ip, location | string | no | — | Analytics 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
| Status | Response |
|---|---|
| 400 | { "detail": "Missing or invalid X-Merchant-ID header" } |
| 422 | Validation error — usually a missing query_str. |
| 500 | { "detail": "An error occurred during the search." } |
Text and color search
POST /api/v0/product/search/textandcolorRuns 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
| Header | Required | Value |
|---|---|---|
X-Merchant-ID | yes | Your merchant ID (integer) |
Content-Type | yes | application/json |
Request body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
query_str | string | one of query_str / color | — | The text query. |
color | string or object | one of query_str / color | — | Named color ("navy"), hex ("#0a1f44"), or { "rgb": [r, g, b] }. |
limit | integer | no | 10 | Maximum results. |
min_price | number | no | — | Explicit minimum price. |
max_price | number | no | — | Explicit maximum price. |
user_profile_id | integer | no | — | Attach for analytics and personalization. |
user_name | string | no | — | Used in the recommendation text. |
conversational_style | string | no | — | Style hint for the recommendation LLM. |
session_id, client_ip, location | string | no | — | Analytics 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
| Status | Response |
|---|---|
| 400 | { "detail": "Missing or invalid X-Merchant-ID header" } |
| 400 | { "detail": "At least one of 'text' or 'color' must be provided." } |
| 422 | Validation error. |
| 500 | Internal search error. Safe to retry with backoff. |
Image search
POST /api/v1/product/search/imageUpload 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
| Header | Required | Value |
|---|---|---|
X-Merchant-ID | yes | Your merchant ID (integer) |
Content-Type | yes | multipart/form-data (set by your client) |
Form fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
file | file | yes | — | The reference image (JPEG/PNG/WebP, up to 10 MB). |
limit | integer | no | 10 | Maximum number of results. |
crop_values | string | no | — | JSON string of {x, y, width, height} pixels to crop before embedding. |
user_profile_id | integer | no | — | Attach results to a user profile. |
user_name | string | no | — | Shopper first name, used in recommendation text. |
conversational_style | string | no | — | Style 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
| Status | Response |
|---|---|
| 400 | { "detail": "Missing or invalid X-Merchant-ID header" } |
| 422 | Validation error — usually a missing file. |
| 500 | Internal error during image embedding. Safe to retry. |
Text search cache check
POST /api/v0/product/search/text/cache-checkA 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
| Header | Required | Value |
|---|---|---|
X-Merchant-ID | yes | Your merchant ID (integer) |
Content-Type | yes | application/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"
}| Field | Type | Description |
|---|---|---|
cached | boolean | Whether a cached result exists. |
result_count | integer | Number of products in the cached result set (0 on a miss). |
cache_key | string | The cache key the server computed from your request body. |
hits | integer | Number 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
| Status | Response |
|---|---|
| 400 | { "detail": "Missing or invalid X-Merchant-ID header" } |
| 422 | Validation error — usually a missing query_str. |
Text and color search cache check
POST /api/v0/product/search/textandcolor/cache-checkThe 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
| Header | Required | Value |
|---|---|---|
X-Merchant-ID | yes | Your merchant ID (integer) |
Content-Type | yes | application/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"
}| Field | Type | Description |
|---|---|---|
cached | boolean | Whether a cached result exists. |
result_count | integer | Number of products in the cached result set. |
cache_key | string | The cache key the server computed (`normalized_query |
operation_type | string | text, color, or textandcolor. |
hits | integer | Cached-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
| Status | Response |
|---|---|
| 400 | { "detail": "Missing or invalid X-Merchant-ID header" } |
| 400 | { "detail": "At least one of 'text' or 'color' must be provided." } |
| 422 | Validation error. |
Max product price
GET /api/v0/product/max-priceReturns 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
| Header | Required | Value |
|---|---|---|
X-Merchant-ID | yes | Your merchant ID (integer) |
Response — 200 OK
{
"max_price": 799.0,
"product_count": 4821
}| Field | Type | Description |
|---|---|---|
max_price | number | Highest price_numeric across status = 'active' rows. 0 if empty. |
product_count | integer | Total 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
| Status | Response |
|---|---|
| 400 | { "detail": "Missing or invalid X-Merchant-ID header" } |
| 500 | Internal database error. Safe to retry with backoff. |
User API
Create user profile
POST /api/v1/user/profileCreates 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
| Header | Required | Value |
|---|---|---|
X-Merchant-ID | yes | Your merchant ID (integer) |
Content-Type | yes | application/json |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
user_name | string | yes | Display name. |
first_name | string | yes | First name. |
last_name | string | yes | Last name. |
email_id | string | yes | Email address (unique per merchant). |
profile_name | string | no | Optional 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
| Status | Response |
|---|---|
| 400 | { "detail": "Missing or invalid X-Merchant-ID header" } |
| 422 | Validation error — missing required fields. |
| 500 | Database error. Safe to retry. |
Get profile by email
POST /api/v1/user/profile/emailReturns 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
| Header | Required | Value |
|---|---|---|
X-Merchant-ID | yes | Your merchant ID (integer) |
Content-Type | yes | application/json |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
email_id | string | yes | Email 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 foundErrors
| Status | Response |
|---|---|
| 400 | { "detail": "Missing or invalid X-Merchant-ID header" } |
| 404 | { "detail": "User profile not found" } |
| 422 | Validation error — invalid email format. |
Create preferences
POST /api/v1/user/preferencesCreates 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
| Header | Required | Value |
|---|---|---|
X-Merchant-ID | yes | Your merchant ID (integer) |
Content-Type | yes | application/json |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
user_profile_id | integer | yes | Foreign key to user_profile.id. |
conversational_style | string | no | Free text (friendly, concise, technical, …). |
personal_fashion_style | string | no | Free text (minimalist, streetwear, classic, …). |
favorite_colors | string[] | no | Preferred colors. |
fashion_material_prefer | string[] | no | Preferred materials. |
body_type | string | no | |
fashion_goals | string | no | |
age_group | string | no |
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
| Status | Response |
|---|---|
| 400 | { "detail": "Missing or invalid X-Merchant-ID header" } |
| 422 | Validation error — missing user_profile_id. |
| 500 | Database error, likely a unique-constraint violation (preferences already exist for this profile — use update instead). |
Update preferences
POST /api/v1/user/preferences/updateUpdates 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
| Header | Required | Value |
|---|---|---|
X-Merchant-ID | yes | Your merchant ID (integer) |
Content-Type | yes | application/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
| Status | Response |
|---|---|
| 400 | { "detail": "Missing or invalid X-Merchant-ID header" } |
| 404 | Preferences row not found for user_profile_id. |
| 422 | Validation 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
| Header | Required | Value |
|---|---|---|
X-Merchant-ID | yes | Your merchant ID (integer) |
Path parameters
| Parameter | Type | Description |
|---|---|---|
user_profile_id | integer | The 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 yetErrors
| Status | Response |
|---|---|
| 400 | { "detail": "Missing or invalid X-Merchant-ID header" } |
| 404 | { "detail": "User preferences not found" } |
Random data by preferences
POST /api/v1/user/preferences/randomdataReturns 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
| Header | Required | Value |
|---|---|---|
X-Merchant-ID | yes | Your merchant ID (integer) |
Content-Type | yes | application/json |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
user_profile_id | integer | yes | The user profile whose preferences should seed the results. |
userPrefsNotPresent | boolean | yes | Hint the server that the user has no stored preferences. |
current_hour | integer | yes | Local hour (0–23), used to pick a time-of-day greeting. |
user_name | string | yes | Shopper first name. |
limit | integer | yes | Maximum number of products to return. |
conversational_style | string | no | Style hint. |
personal_fashion_style | string | no | |
favorite_colors | string[] | no | |
fashion_material_prefer | string[] | no | |
body_type | string | no | |
fashion_goals | string | no | |
age_group | string | no |
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"
}
]
}
}| Field | Type | Description |
|---|---|---|
results.greeting | string | A time-of-day greeting. null if no greeting template matched. |
results.data | object[] | 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
| Status | Response |
|---|---|
| 400 | { "detail": "Missing or invalid X-Merchant-ID header" } |
| 422 | Validation error — missing required fields. |
Pipeline API
Create pipeline status
POST /api/v1/pipeline/statusCreates 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 reset — completed_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
| Header | Required | Value |
|---|---|---|
X-Merchant-ID | yes | Your merchant ID (integer) |
Content-Type | yes | application/json |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
merchant_id | integer | yes | Must match X-Merchant-ID header. |
current_step | string | no | Initial step name (default initialized). Use shopify_etl_dev for a fresh install. |
shop_domain | string | no | Optional source-platform domain (e.g. store.myshopify.com). |
config | object | no | Arbitrary 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
| Status | Response |
|---|---|
| 400 | { "detail": "Missing or invalid X-Merchant-ID header" } |
| 400 | { "detail": "Merchant ID in header must match merchant_id in request body" } |
| 500 | Database 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
| Header | Required | Value |
|---|---|---|
X-Merchant-ID | yes | Your merchant ID (integer) |
Path parameters
| Parameter | Type | Description |
|---|---|---|
merchant_id | integer | The 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
| Status | Response |
|---|---|
| 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" } |
| 500 | Database 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
| Header | Required | Value |
|---|---|---|
X-Merchant-ID | yes | Your merchant ID (integer) |
Content-Type | yes | application/json |
Path parameters
| Parameter | Type | Description |
|---|---|---|
merchant_id | integer | The merchant whose status to update. Must match header. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
current_step | string | no | New current step. Also appended to completed_steps if not already there. |
completed_step | string | no | Explicitly append a step to completed_steps (legacy path from theme extension). |
status | string | no | running, completed, or failed. Setting completed stamps completed_at. |
error_message | string | no | Error detail. Setting this automatically switches status to failed. |
config | object | no | Partial 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
| Status | Response |
|---|---|
| 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" } |
| 500 | Database 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
| Header | Required | Value |
|---|---|---|
X-Merchant-ID | yes | Your merchant ID (integer) |
Path parameters
| Parameter | Type | Description |
|---|---|---|
merchant_id | integer | The 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
| Status | Response |
|---|---|
| 400 | { "detail": "Missing or invalid X-Merchant-ID header" } |
| 403 | { "detail": "Access denied to this merchant's pipeline status" } |
| 500 | Database error. Safe to retry. |
Merchant API
Delete merchant data
DELETE /api/v1/merchant/{merchant_id}/dataDeletes 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_resultsrows are preserved for analytics and historical reporting. They are query logs, not personal data.
Headers
| Header | Required | Value |
|---|---|---|
X-Merchant-ID | yes | Your merchant ID (integer) |
Path parameters
| Parameter | Type | Description |
|---|---|---|
merchant_id | integer | The 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
| Status | Response |
|---|---|
| 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-imageFetches 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
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | yes | The 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.jpgconst 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
| Status | Response |
|---|---|
| 502 | { "detail": "Failed to fetch image from source." } |
Health check
GET /healthReturns { "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/healthconst 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 == 200Errors
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
enterprise@vairetail.com — integration questions, onboarding, private-beta access to push ingest and API keys, bug reports.
When reporting an issue, include:
- The
X-Request-IDheader from the failing response (every response includes one). - The full request body you sent (with PII redacted).
- The response status code and body.
- Your merchant ID.
Marketing site
vairetail.com — product overview, case studies, and scheduling a demo.