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 key

Building 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_code and currency_symbol together 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 with storefront_visible: true: definitions are private by default) and another for internal data (internal, with storefront_visible: false). Keys are lowercase: specs.ram.
  • Pick the most specific type: weight, dimension or volume for measures (filters convert units), integer with validations.unit for values like "16 GB", list.single_line_text for several values (connectivity, colours), boolean for 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 and vendor (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) including metafields, or set values afterwards with metafields/set/ (up to 250, atomic).
  • Moving from free-form technical_specs? Run metafield-definitions/suggest/, create the definitions, then call import/ with dry_run: true first and review skipped.

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-Match with 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 409 with the existing id in REST, and returns the existing product with already_existed: true in 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) plus products/deleted/?since= (deletions) instead of downloading the whole catalogue again.
  • Build filter sidebars from facets=1 and show values with their display field. Always escape text from the API before inserting it into HTML.
  • Respect Cache-Control and, on a 503, wait the Retry-After seconds 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.

FeatureHow it works
PATCHPartial update on products, variants and collections: send only the fields you change.
ETagDetail 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-MatchOn 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_sinceProducts 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

GET /api/v1/projects/{project_id}/products/

List products.

ParamMeaning
search (or q)Full-text search plus name, SKU and slug, so you can look a product up before creating it
categoryCategory id or slug (includes subcategories; exact=1 for direct ones only)
active, featured, in_stocktrue to filter. A product with variants is in stock when one of its active variants is
min_price, max_pricePrice 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_sinceChanged at or after this instant
sortprice_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
}
POST /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"}
  }
}
GET PUT PATCH DELETE /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"}}}'
POST /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.

POST /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

GET POST /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"}
}
GET PUT PATCH DELETE /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

GET POST /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
}
PUT DELETE /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.

GET POST /api/v1/projects/{project_id}/metafield-definitions/

List (?owner_type=product|variant|collection) or create definitions.

FieldMeaning
owner_typeproduct (default), variant or collection
namespace, keyThe address, e.g. specs.ram: lowercase letters, digits and _, starting with a letter, up to 50 characters each
nameLabel shoppers see ("RAM")
typeSee the types below; list.<type> holds several values
validationsOptional: 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_visibleShown in the storefront API and the specifications table. false = internal only
filterableUsable as a storefront filter and facet (also needs storefront_visible)
positionDisplay 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

TypeSendShown as (es)
single_line_text, multi_line_text, rich_text"LGA1700"LGA1700
integer, decimal16, 2.516 GB, 2,5
booleantrueSí / 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
rating4.54,5 / 5
weight, dimension, volume{"value": 1.09, "unit": "kg"}1,09 kg
product_reference, collection_referenceId of a product or collection of the same shopid
jsonAny JSON up to 16 KBnot shown
GET PATCH DELETE /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.

POST /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"}}
  }
}
GET /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.

POST /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

GET POST /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}
  ]}
}
GET PUT PATCH DELETE /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).

GET /api/v1/public/{project_id}/products/
ParamMeaning
qSearch
category, collectionSlug (a category includes its subcategories; a collection keeps its own sort order)
featured1 for featured products only
mf.<ns>.<key>=vEquals; 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=1Adds filter counts over all matching products
include=metafieldsAdds custom fields to each product in the list
updated_sinceOnly products changed since then (ISO 8601)
limit, offsetPagination

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).

GET /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"}
  }
}
GET /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.

EndpointWhat 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

GET PUT /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.

POST 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}'
GET 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.

POST https://{subdomain}.escalateflow.com/shop/cart/update/{product_id}/

Update item quantity. Body: {"quantity": 3}

POST https://{subdomain}.escalateflow.com/shop/cart/remove/{product_id}/

Remove an item from the cart.

Shop Hero Slides

GET POST /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"
}
POST /api/v1/projects/{project_id}/shop/hero-slides/generate/

AI-generate hero slides.

PUT DELETE /api/v1/projects/{project_id}/shop/hero-slides/{slide_id}/

Update or delete a hero slide.