Shop API
Build a shop programmatically: catalogue, typed custom fields, collections, a storefront API with filters and facets, and the full order cycle.
Get your API keyBuilding a shop: best practices
This order avoids rework: settings and structure first, then the catalogue, then merchandising, then a real test order. It works the same through the REST API, the MCP tools or the dashboard.
1. Settings before products
- Set
currency_codeandcurrency_symboltogether in shop settings. Prices are stored as numbers in that currency. - Create shipping zones with their rates, and the tax rates by destination country, before the first order so checkout totals are right from day one.
- Choose the payment methods: card (Stripe Checkout) and/or bank transfer with
payment_instructions.
2. Structure: categories and custom fields
- Build the category tree first; 2–3 levels is enough for most shops. A product belongs to one category, and filtering by a parent includes its children.
- Create the custom field definitions before importing products: values for undefined keys are rejected.
- Use one namespace for the data shoppers compare (
specs, defined withstorefront_visible: true: definitions are private by default) and another for internal data (internal, withstorefront_visible: false). Keys are lowercase:specs.ram. - Pick the most specific type:
weight,dimensionorvolumefor measures (filters convert units),integerwithvalidations.unitfor values like "16 GB",list.single_line_textfor several values (connectivity, colours),booleanfor yes/no. - Mark as filterable only what shoppers really filter by (brand, size, main specs; usually 4–8 fields). Every filterable field becomes a facet in the filter sidebar.
- Never put private data (cost, supplier, margins) in a storefront-visible field: the storefront API is public.
3. The catalogue
- Give every product a stable slug and a unique SKU; URLs and integrations rely on them.
- Use variants for options of the same product (size, colour, capacity) instead of separate products. Stock is tracked per variant.
- Fill
meta_title,meta_description, a real description andvendor(brand): they feed SEO, structured data and the brand facet. - Upload a main image and a gallery with the same aspect ratio (square works best in grids), at least 1000 px wide.
- Import in batches with
products/bulk/(up to 100 per call) includingmetafields, or set values afterwards withmetafields/set/(up to 250, atomic). - Moving from free-form
technical_specs? Runmetafield-definitions/suggest/, create the definitions, then callimport/withdry_run: truefirst and reviewskipped.
4. Merchandising
- Prefer automatic collections (rules on price, vendor, tags, category or custom fields) over manual lists: they stay up to date as the catalogue changes.
- Feature a few products, and use automatic discounts or coupon codes for promotions instead of editing prices.
5. Integrations and custom storefronts
- Use PATCH with only the fields you change, and send
If-Matchwith the ETag you read when several people or systems edit the same product. - For calls that add or move an amount (gift card adjust, order edits, refunds, completing a draft), MCP clients pass an
idempotency_key(one unique string per intended operation): a retry with the same key returns the first result and is never applied twice. - Give every product a unique SKU. Creating a product whose SKU (or explicit slug) already exists answers
409with the existing id in REST, and returns the existing product withalready_existed: truein MCP, so retries and re-imports never duplicate it. Look products up with?search=(name, SKU or slug). - Keep external copies in sync with
updated_since(changes) plusproducts/deleted/?since=(deletions) instead of downloading the whole catalogue again. - Build filter sidebars from
facets=1and show values with theirdisplayfield. Always escape text from the API before inserting it into HTML. - Respect
Cache-Controland, on a503, wait theRetry-Afterseconds before retrying. - Subscribe to order webhooks instead of polling the orders list.
6. Before launch
- Place a real test order for each payment method and shipping zone, and check the emails, stock and totals (shipping and tax).
- Open a product page on a phone: images, specifications table, variant selector and add to cart.
- Publish the legal pages (terms, privacy, returns) and, if you accept returns, enable them in shop settings.
API conventions
Management endpoints live under https://escalateflow.com/api/v1/projects/{project_id}/ and need an API key: Authorization: Bearer ef_live_… (or X-API-Key). Every schema is in Swagger.
| Feature | How it works |
|---|---|
PATCH | Partial update on products, variants and collections: send only the fields you change. |
ETag | Detail responses carry an ETag: a hash of everything the response shows (a variant's new price changes its product's ETag too). If-None-Match with it returns 304 when nothing changed. |
If-Match | On PATCH / DELETE: if the resource changed since you read it, you get 412 with current_etag and nothing is saved. Checked under a lock: of two writes sent at once with the same ETag, one applies and the other gets 412. Weak forms (W/"…") are accepted. |
updated_since | Products list: only products changed at or after an ISO 8601 instant (a bare date means midnight UTC), ordered by last change for a stable sync. A change to a variant or to a custom field definition counts as a change of the product. Deleted products are listed by GET products/deleted/?since= (id, sku, slug, deleted_at; kept 90 days), so a copy synced with both stays exact. |
| Errors | {"success": false, "error": {"code": 400, "message": "…", "details": {…}}}. message names the first problem; details lists every field. |
412 example
{
"success": false,
"error": {
"code": 412,
"message": "The resource changed since you read it (If-Match does not match): GET it again, re-apply your change and retry with the new ETag",
"current_etag": "\"65e290601f600\""
}
}
Products
/api/v1/projects/{project_id}/products/
List products.
| Param | Meaning |
|---|---|
search (or q) | Full-text search plus name, SKU and slug, so you can look a product up before creating it |
category | Category id or slug (includes subcategories; exact=1 for direct ones only) |
active, featured, in_stock | true to filter. A product with variants is in stock when one of its active variants is |
min_price, max_price | Price range |
mf.<ns>.<key>, mf_min.…, mf_max.… | Custom field filters, on any defined field (visible or not). Same syntax as the storefront filters. |
updated_since | Changed at or after this instant |
sort | price_asc, price_desc, newest, name, name_desc, rating, updated, updated_desc |
page + page_size (or limit + offset) | Pagination, up to 500 per page |
{
"success": true,
"products": [
{
"id": "uuid",
"name": "Premium T-Shirt",
"slug": "premium-t-shirt",
"price": "29.99",
"formatted_price": "29,99 €",
"sku": "TSH-001",
"vendor": "Acme",
"stock_quantity": 100,
"is_in_stock": true,
"category": "uuid",
"category_name": "Apparel",
"metafields": {"specs.material": "Organic cotton", "specs.peso": {"value": 200, "unit": "g"}},
"has_variants": true,
"url": "https://my-store.escalateflow.com/shop/product/premium-t-shirt/",
"updated_at": "2026-10-19T10:02:11Z"
}
],
"total": 25,
"limit": 50,
"offset": 0
}
/api/v1/projects/{project_id}/products/
Create a product. metafields only accepts keys that have a definition.
{
"name": "Premium T-Shirt",
"slug": "premium-t-shirt",
"description": "100% organic cotton t-shirt",
"price": "29.99",
"compare_at_price": "39.99",
"sku": "TSH-001",
"vendor": "Acme",
"category_id": "uuid",
"track_inventory": true,
"stock_quantity": 100,
"meta_title": "Premium organic T-Shirt",
"meta_description": "Soft 200 g organic cotton tee.",
"is_active": true,
"metafields": {
"specs.material": "Organic cotton",
"specs.peso": {"value": 200, "unit": "g"}
}
}
/api/v1/projects/{project_id}/products/{product_id}/
Get, update or delete a product. GET returns an ETag; PATCH changes only what you send. Sent metafields are merged with the existing ones, and null removes a key.
curl -X PATCH "https://escalateflow.com/api/v1/projects/{project_id}/products/{product_id}/" \
-H "Authorization: Bearer ef_live_xxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H 'If-Match: "65e290601f600"' \
-d '{"price": "24.99", "metafields": {"specs.peso": {"value": 210, "unit": "g"}}}'
/api/v1/projects/{project_id}/products/bulk/
Create up to 100 products in one call (same fields as create, including metafields). The answer has one result per item and a summary.
/api/v1/projects/{project_id}/products/{product_id}/image/
Upload the main image (multipart/form-data or base64 JSON). Gallery: products/{product_id}/gallery/, reordered with gallery/reorder/.
Formats: JPEG, PNG, WebP (max 10MB)
Product Variants
/api/v1/projects/{project_id}/products/{product_id}/variants/
List or create variants for a product. Variants can have their own custom fields (definitions with owner_type: "variant"). Stock below 0 is only allowed when the product has allow_backorder.
Create Variant
{
"title": "Large / Blue",
"sku": "TSH-001-LG-BL",
"price": "29.99",
"stock_quantity": 25,
"metafields": {"specs.color_hex": "#1a3d8f"}
}
/api/v1/projects/{project_id}/products/{product_id}/variants/{variant_id}/
Get, update or delete a variant. Supports ETag and If-Match like products.
Categories
/api/v1/projects/{project_id}/categories/
List or create product categories. Send parent_id to nest one; the whole tree is at categories/tree/.
{
"name": "Apparel",
"slug": "apparel",
"description": "Clothing and accessories",
"parent_id": null
}
/api/v1/projects/{project_id}/categories/{category_id}/
Update or delete a category.
Custom fields (metafields)
Typed fields you define once and fill on products, variants or collections: RAM, weight, material, warranty… They are validated, shown in the product page's specifications table (custom fields first, in position order, then the free-form technical_specs rows they don't replace), and can be used as storefront filters and facets.
/api/v1/projects/{project_id}/metafield-definitions/
List (?owner_type=product|variant|collection) or create definitions.
| Field | Meaning |
|---|---|
owner_type | product (default), variant or collection |
namespace, key | The address, e.g. specs.ram: lowercase letters, digits and _, starting with a letter, up to 50 characters each |
name | Label shoppers see ("RAM") |
type | See the types below; list.<type> holds several values |
validations | Optional: min / max (numbers and measures), unit (display unit for integer and decimal), units (allowed units for a measure), choices, regex, min_length / max_length, min_items / max_items (lists), scale_min / scale_max (rating) |
storefront_visible | Shown in the storefront API and the specifications table. false = internal only |
filterable | Usable as a storefront filter and facet (also needs storefront_visible) |
position | Display order; new definitions go last |
curl -X POST "https://escalateflow.com/api/v1/projects/{project_id}/metafield-definitions/" \
-H "Authorization: Bearer ef_live_xxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"namespace": "specs", "key": "ram", "name": "RAM", "type": "integer",
"validations": {"unit": "GB", "min": 1}, "storefront_visible": true, "filterable": true}'
Types
| Type | Send | Shown as (es) |
|---|---|---|
single_line_text, multi_line_text, rich_text | "LGA1700" | LGA1700 |
integer, decimal | 16, 2.5 | 16 GB, 2,5 |
boolean | true | Sí / No |
date, date_time | "2026-10-19" | as is |
url, file_reference, color | "https://…", "#aabbcc" | as is |
money | {"amount": "12.5", "currency_code": "EUR"} | 12,50 EUR |
rating | 4.5 | 4,5 / 5 |
weight, dimension, volume | {"value": 1.09, "unit": "kg"} | 1,09 kg |
product_reference, collection_reference | Id of a product or collection of the same shop | id |
json | Any JSON up to 16 KB | not shown |
/api/v1/projects/{project_id}/metafield-definitions/{id}/
Name, validations, flags and position can always change, and the storefront shows the change at once. type, namespace, key and owner_type can't change while values exist (409). DELETE also deletes the values, unless you add ?keep_values=1.
/api/v1/projects/{project_id}/metafields/set/
Write up to 250 values in one call. It is atomic: if one value is invalid, nothing is saved and every problem is listed. null removes a value. You can also write values through the metafields field when you create or update a product, variant or collection.
{
"metafields": [
{"owner_type": "product", "owner_id": "uuid", "key": "specs.ram", "value": 32},
{"owner_type": "product", "owner_id": "uuid", "key": "specs.peso", "value": {"value": 1.09, "unit": "kg"}},
{"owner_type": "variant", "owner_id": "uuid", "key": "specs.color_hex", "value": "#1a1a1a"}
]
}
Invalid value
{
"success": false,
"error": {
"code": 400,
"message": "metafields: specs.ram: must be a number",
"details": {"metafields": {"specs.ram": "specs.ram: must be a number"}}
}
}
/api/v1/projects/{project_id}/metafield-definitions/suggest/
Proposes definitions (type, unit and sample values) from the free-form technical_specs or attributes your products already have.
/api/v1/projects/{project_id}/metafield-definitions/import/
Copies those values into custom fields. Values that don't fit the type are reported in skipped, never guessed.
{
"source": "technical_specs",
"mapping": {"socket": "specs.socket", "weight": "specs.peso"},
"overwrite": false,
"remove_source": false,
"dry_run": true
}
Limits: 200 definitions per owner type, 250 values per bulk call, 10 filters per request, 50 list items, 255 characters for single-line text. MCP tools: escalateflow_metafields_list_definitions, _define, _delete_definition, _set, _suggest, _import_specs.
Collections
/api/v1/projects/{project_id}/collections/
Groups of products with their own page at {shop}/collections/{slug}/. Manual collections list products by id; automatic ones use rules and stay up to date when products change.
Rule fields: tag, vendor, product_type, title, category (slug, includes subcategories), price, compare_at_price, on_sale, and metafield with a key (operators equals, not_equals, gt, lt, gte, lte; measures compare in base units). Sort orders: manual, newest, price_asc, price_desc, name.
{
"title": "Light mice",
"slug": "light-mice",
"kind": "automatic",
"rules": {"match": "all", "conditions": [
{"field": "category", "op": "equals", "value": "mice"},
{"field": "metafield", "key": "specs.peso", "op": "lte", "value": 80}
]}
}
/api/v1/projects/{project_id}/collections/{collection_id}/
Get, update or delete a collection. Collections can have custom fields too (owner_type: "collection").
Storefront API (public)
A read-only catalogue for custom storefronts, apps and search: GET https://{your-shop-domain}/api/v1/public/{project_id}/…. No key is needed, and it only returns active products and storefront-visible custom fields. Calls from another origin are allowed for the shop's own domains and the origins listed in api_allowed_origins (shop settings).
/api/v1/public/{project_id}/products/
| Param | Meaning |
|---|---|
q | Search |
category, collection | Slug (a category includes its subcategories; a collection keeps its own sort order) |
featured | 1 for featured products only |
mf.<ns>.<key>=v | Equals; a comma list means any of them; on list fields it means contains. mf.specs.socket=AM5,LGA1700 |
mf_min.… / mf_max.… | At least / at most (numbers, measures, ratings, dates). mf_max.specs.peso=1.5kg; without a unit, the base unit (g, mm, ml) |
facets=1 | Adds filter counts over all matching products |
include=metafields | Adds custom fields to each product in the list |
updated_since | Only products changed since then (ISO 8601) |
limit, offset | Pagination |
An unknown or non-filterable field, or a value of the wrong type, returns 400 with the reason instead of an empty list.
Response with facets=1
{
"products": [
{"id": "uuid", "name": "Superlight 2", "slug": "superlight-2", "url": "https://…", "price": "149.00",
"price_formatted": "149,00 €", "image_url": "https://…", "category": "Mice", "category_slug": "mice",
"vendor": "Logitech", "in_stock": true}
],
"count": 34,
"facets": {
"vendors": [{"name": "Logitech", "count": 12}],
"categories": [{"name": "Mice", "slug": "mice", "count": 34}],
"price": {"min": "19.00", "max": "179.00"},
"in_stock": 31,
"metafields": {
"specs.peso": {"name": "Peso", "type": "weight", "unit": "g", "min": 54, "max": 141,
"values": [{"value": {"value": 60, "unit": "g"}, "display": "60 g", "count": 3}]},
"specs.conexion": {"name": "Conexión", "type": "list.single_line_text",
"values": [{"value": "USB-C", "display": "USB-C", "count": 20}]}
}
}
}
Build checkboxes from values (send mf.<key>=<value>) and range sliders from min / max (send mf_min / mf_max).
/api/v1/public/{project_id}/products/{slug_or_id}/
One product, always with its custom fields (and each variant's), grouped by namespace and ready to display: use display to show a value and value to compare.
"metafields": {
"specs": {
"ram": {"name": "RAM", "type": "integer", "value": 32, "display": "32 GB", "unit": "GB"},
"peso": {"name": "Peso", "type": "weight", "value": {"value": 1.09, "unit": "kg"}, "display": "1,09 kg"}
}
}
/api/v1/public/{project_id}/categories/
· collections/
Categories with product counts, and published collections with their custom fields. Also categories/{slug}/ and products/{slug}/reviews/.
Caching and limits: responses send Cache-Control: public, max-age=0, s-maxage=30: browsers check again on every request and a CDN keeps a copy at most 30 seconds. Facet counts can lag up to 90 seconds after a value changes; changes to field definitions apply at once. Facet values are grouped the way filters compare them (1 kg and 1000 g are one value).
Each shop can run a limited number of catalogue-wide queries (facets, q, mf. filters) at the same time, so one shop's traffic never slows down another. If you get a 503, retry after the Retry-After seconds. For large catalogues, sync with updated_since instead of paging through deep offsets.
Orders & checkout
All under /api/v1/projects/{project_id}/. Request and response schemas are in Swagger, and each area also has MCP tools.
| Endpoint | What it does |
|---|---|
orders/, orders/{id}/ | List and read orders |
orders/{id}/update/ | Status, tracking number (sends the shipped email with a carrier link), notes, balance received |
orders/{id}/edit/ | Before shipping: change quantities, remove or add lines, change shipping; stock, totals and tax are recomputed |
orders/{id}/cancel/, refund/ | Cancel a pending or paid order (stock, coupon uses and gift card balance are given back, once) or refund. Setting the status to cancelled with update/ does the same, and moving a cancelled order back to paid takes them again |
shipping-zones/ (+ rates/, quote/) | Countries → zone → rates by subtotal or weight, with free shipping over an amount |
shop/tax-rates/ | Tax rates by destination country, applied at checkout |
coupons/ | Discount codes, automatic discounts and free-shipping discounts |
gift-cards/ (+ adjust/) | Codes with a balance, used as a means of payment at checkout |
draft-orders/ (+ complete/, send-invoice/) | Orders you prepare for a customer, with custom lines and prices, plus an invoice link to pay |
returns/ (+ approve/, decline/, receive/, refund/) | Return requests from the customer's order page, with restock and refund |
abandoned-checkouts/ | Abandoned checkouts and recovery stats (the recovery email is opt-in in shop settings) |
webhooks/ | Signed, retried notifications: order.created, order.paid, order.shipped, order.delivered, order.cancelled, order.refunded, order.edited, checkout.abandoned, return.*. Full list: /api/v1/webhooks/events/ |
shop/analytics/… | Overview, revenue, top products, status breakdown, recent orders |
Shop Settings & Pages
/api/v1/projects/{project_id}/shop/settings/
Get or update shop settings.
Response
{
"success": true,
"data": {
"is_enabled": true,
"currency_code": "PYG",
"currency_symbol": "Gs.",
"supported_currencies": [],
"layout": "grid",
"products_per_page": 12,
"show_categories": true,
"url_prefix": "shop",
"product_base": "product",
"category_base": "category",
"shop_url": "https://subdomain.escalateflow.com/shop/"
}
}
Update example
curl -X PUT "https://escalateflow.com/api/v1/projects/{id}/shop/settings/" \
-H "Authorization: Bearer ef_live_xxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"currency_code": "PYG",
"currency_symbol": "Gs.",
"is_enabled": true,
"shop_title": "Mi Tienda"
}'
Field names matter: Use currency_code (ISO 4217: PYG, EUR, USD) and currency_symbol (display: Gs., €, $). Sending currency without the _code suffix will be rejected.
The currency_code controls number formatting (thousand/decimal separators). The currency_symbol is the visible label on prices. Set both together.
Storefront Cart API
Session-based cart endpoints for the public storefront. These run on the shop subdomain, not the main API.
https://{subdomain}.escalateflow.com/shop/cart/add/{product_id}/
Add a product to the session cart.
curl -X POST "https://my-store.escalateflow.com/shop/cart/add/{product_id}/" \
-H "Content-Type: application/json" \
-d '{"quantity": 2}'
https://{subdomain}.escalateflow.com/shop/cartdata/
Get full cart contents with formatted prices. Used by the sidebar cart JavaScript.
Response
{
"success": true,
"items": [
{
"id": "uuid",
"name": "Smart TV Samsung 55\" 4K",
"slug": "smart-tv-samsung-55-4k",
"price": "4290000.00",
"price_formatted": "Gs. 4.290.000",
"image_url": "https://...",
"quantity": 2,
"line_total": "8580000.00",
"line_total_formatted": "Gs. 8.580.000"
}
],
"item_count": 1,
"cart_count": 2,
"subtotal": "8580000.00",
"subtotal_formatted": "Gs. 8.580.000",
"currency_symbol": "Gs."
}
The *_formatted fields include the currency symbol and locale-appropriate thousand/decimal separators based on the shop's currency_code setting. Raw decimal strings (price, subtotal) are also returned for backward compatibility.
https://{subdomain}.escalateflow.com/shop/cart/update/{product_id}/
Update item quantity. Body: {"quantity": 3}
https://{subdomain}.escalateflow.com/shop/cart/remove/{product_id}/
Remove an item from the cart.
Shop Hero Slides
/api/v1/projects/{project_id}/shop/hero-slides/
List or create hero slides.
{
"title": "Summer Collection",
"subtitle": "Up to 50% off",
"image_url": "https://...",
"cta_text": "Shop Now",
"cta_url": "/shop/summer"
}
/api/v1/projects/{project_id}/shop/hero-slides/generate/
AI-generate hero slides.
/api/v1/projects/{project_id}/shop/hero-slides/{slide_id}/
Update or delete a hero slide.