Skip to content
Online store

Menus and content

Build the navigation and content pages of your headless store.

You will build the shell of a store in Lima: the header menu, the "Polos" category, and the "Envíos" shipping page. All with the Storefront API, no token.

Requirements

  • The store slug or one of its domains, for x-Store-Domain. See Headless store.
  • Categories and pages published in the store.

Read the settings

GET /v1/settings returns the store, its branding, its SEO, its published pages (cms.pages), and its active menus (cms.menus) in one call. Fetch it once per request and reuse it. GET /v1/site-settings returns only cms.settings.

curl https://storefront.ecomiq.pe/v1/settings \
  -H "x-Store-Domain: mi-tienda"
Response (excerpt)
{
  "store": { "name": "Mi tienda", "currency": "PEN", "visibility": "public" },
  "cms": {
    "settings": {
      "branding": { "logoUrl": "https://…/logo.webp" },
      "seo": { "siteName": "Mi tienda", "robotsIndex": true },
      "contact": { "whatsapp": "+51987654321" }
    }
  }
}

The next steps reuse API and headers.

  • With visibility: "private", the store asks for a password. See Unlock a private store.
  • With robotsIndex: false, block the whole site in robots.txt.

Render the menu

Menus arrive in cms.menus and in GET /v1/menus. To fetch just one, use GET /v1/menus/{key}. A new store comes with main, footer, and account.

curl https://storefront.ecomiq.pe/v1/menus/main \
  -H "x-Store-Domain: mi-tienda"
Response (excerpt)
{
  "key": "main",
  "name": "Menú principal",
  "items": [
    {
      "label": "Polos",
      "url": "/categories/polos",
      "reference": { "type": "category", "id": "7204558912004327982" },
      "order": 1,
      "visible": true,
      "children": []
    }
  ]
}
  • Items arrive as saved, unfiltered and unsorted. Skip those with visible: false and sort by order, in children too.
  • Links to products and categories arrive with their current URL, even if the merchant changes it. reference tells you which one they point to.

Show a category

GET /v1/categories/{slug} returns the category with its SEO, its breadcrumb, and its subcategories. Then fetch its products with categoryId, as in Headless store.

curl https://storefront.ecomiq.pe/v1/categories/polos \
  -H "x-Store-Domain: mi-tienda"

curl "https://storefront.ecomiq.pe/v1/products?categoryId=7204558912004327982" \
  -H "x-Store-Domain: mi-tienda"
Category response (excerpt)
{
  "id": "7204558912004327982",
  "name": "Polos",
  "url": "/categories/polos",
  "metaTitle": "Polos de algodón pima",
  "productCount": 24,
  "ancestors": [
    {
      "id": "7204558912004329210",
      "name": "Hombre",
      "url": "/categories/hombre"
    }
  ],
  "children": []
}
  • ancestors goes from the root to the parent. categoryId skips subcategories.
  • GET /v1/categories returns all of them in a flat list sorted by order. Build the tree with parentId. GET /v1/brands lists the brands.
  • GET /v1/collections lists collections. For one, use GET /v1/collections/{slug} and collectionId. Without sort, products follow the order the merchant chose.

Show pages and articles

GET /v1/pages/{slug} returns the published version of a page. All of them also arrive in cms.pages.

curl https://storefront.ecomiq.pe/v1/pages/envios \
  -H "x-Store-Domain: mi-tienda"
Response (excerpt)
{
  "name": "Envíos",
  "slug": "envios",
  "layout": "content",
  "settings": {
    "title": "Envíos",
    "html": "<p>Entregamos en Lima en 24 a 48 horas.</p>",
    "seo": { "title": "Envíos", "description": "Tiempos y costos de envío." }
  },
  "sections": []
}
  • With layout: "content", the text comes in settings: title, html, and seo. The home page has the slug home and brings sections with blocks.
  • The merchant writes the html. Sanitize it before you insert it.

For the blog, GET /v1/articles lists published articles, newest first. GET /v1/articles/{slug} returns one with its body in HTML, which you also sanitize.

curl "https://storefront.ecomiq.pe/v1/articles?tag=pima&limit=12" \
  -H "x-Store-Domain: mi-tienda"

Filter with folder or tag (exact value) or search (title and excerpt). limit goes from 1 to 100 and defaults to 24; there are no further pages.

GET /v1/search/autocomplete suggests products while the shopper types. Send search, 2 to 80 characters, and limit, 1 to 20.

curl "https://storefront.ecomiq.pe/v1/search/autocomplete?search=polo&limit=6" \
  -H "x-Store-Domain: mi-tienda"
Response (excerpt)
[
  {
    "name": "Polo pima azul",
    "url": "/products/polo-pima-azul",
    "imageUrl": "https://…/polo.webp"
  }
]

For the results page, use GET /v1/products?search=polo.

Resolve a URL

Before you render a route, check whether the merchant redirected it.

curl "https://storefront.ecomiq.pe/v1/redirects/resolve?path=/ofertas" \
  -H "x-Store-Domain: mi-tienda"
Response
{
  "redirect": {
    "location": "/categories/polos",
    "statusCode": 301,
    "preserveQuery": true
  }
}

Respond with statusCode and location. With preserveQuery: true, append the request query. Without a redirect, redirect is null. The path must match exactly, including case; only a trailing / is ignored.

Then, in your catch-all route, ask what lives at that path. Send only the path, without domain or query.

curl "https://storefront.ecomiq.pe/v1/urls/resolve?path=/categories/polos" \
  -H "x-Store-Domain: mi-tienda"
Response (excerpt)
{
  "kind": "category",
  "url": "/categories/polos",
  "handle": "polos",
  "product": null,
  "category": { "id": "7204558912004327982", "name": "Polos" }
}
  • product or category: render the product or the category you get.
  • redirect: respond 301 to url. It happens with old URLs, uppercase paths, or a trailing /.
  • Only products and categories resolve. Otherwise: 404 Url.NotFound.

If you store links to products or categories in your own content, POST /v1/urls/links returns them with their current URL and reference, in order. It takes up to 100, keeps the query, and leaves external links as is.

curl -X POST https://storefront.ecomiq.pe/v1/urls/links \
  -H "x-Store-Domain: mi-tienda" \
  -H "Content-Type: application/json" \
  -d '{ "links": [{ "url": "/products/polo-azul?talla=m" }] }'

Publish the sitemap

GET /v1/sitemap lists the canonical URLs of visible products, categories, and collections. Each page holds up to 1000 products; the first one adds the categories and collections.

curl "https://storefront.ecomiq.pe/v1/sitemap?page=1" \
  -H "x-Store-Domain: mi-tienda"
Response (excerpt)
{
  "items": [{ "url": "/categories/polos", "updatedOn": null }],
  "nextPage": 2,
  "totalPages": 3
}

URLs are paths: prefix your store's domain. The home page, pages, and blog are not included; add them from cms.pages and GET /v1/articles. If totalPages is greater than 1, publish an index with one sitemap per page.

Show physical stores

GET /v1/store-locations lists active physical stores, by name. Filter with search by name or city. It returns 20 by default; use limit and cursor to get more. In a marketplace it returns the main store's; pass sellerId for a seller's. GET /v1/store-locations/{id} returns one.

curl "https://storefront.ecomiq.pe/v1/store-locations?search=Miraflores" \
  -H "x-Store-Domain: mi-tienda"
Response (excerpt)
{
  "data": [
    {
      "name": "Tienda Miraflores",
      "address": "Av. Larco 1234",
      "city": "Lima",
      "schedules": [
        {
          "dayOfWeek": "monday",
          "openTime": "10:00:00",
          "closeTime": "21:00:00"
        }
      ]
    }
  ],
  "hasNextPage": false,
  "nextCursor": null
}

Unlock a private store

A store with visibility: "private" answers 401 with Common.Unauthorized on every call except /v1/settings and /v1/access/unlock. Ask the visitor for the password and send it from your server.

curl -X POST https://storefront.ecomiq.pe/v1/access/unlock \
  -H "x-Store-Domain: mi-tienda" \
  -H "Content-Type: application/json" \
  -c cookies.txt \
  -d '{ "accessKey": "<password>" }'

It returns { "unlocked": true } and sets the ecomiq_store_access cookie, valid for 12 hours. Send it back on every following call (-b cookies.txt in cURL). If the merchant changes the password, the cookie stops working.

Errors

codeStatusWhat to do
Url.NotFound404Nothing is published at that path. Show your 404 page.
cms.page.not_found404The page does not exist or is not published.
cms.article.not_found404The article does not exist or is not published.
Category.NotFound404The category does not exist or is inactive.
cms.menu.not_found404The menu does not exist or is inactive. Skip it.
Common.NotFound404The collection, location, or store does not exist.
Common.Validation400Check errors. For example, a one-character search.
Common.Unauthorized401The store is private. Ask for the password.
Store.AccessDenied401The password does not match.
Common.ServiceUnavailable503Search is not responding. Retry in a few seconds.

Next steps

Last updated:

On this page