Products
Sales, margin, inventory and repurchase behavior for every product in your store, ready for BI and dashboards
What this analysis answers
The Products API shows how each product in the store performed over the period you choose: how much it sold, at what margin, how much stock is left, and whether customers who arrived through it come back to buy again. The numbers come consolidated, using the same rules as the V4MOS.AI platform, so you can load them straight into your BI tool without recomputing anything.
Questions you can answer:
- Which products sell the most, and at what margin?
- How much stock do I have, in units and in days of sales?
- Which products have a high return rate?
- Do customers who enter the store through this product buy again? How much are they worth over 180 days?
- How is a whole category performing?
- What is usually bought together with this product, and what do customers buy next?
- Which coupons and acquisition channels drive sales of this product?
Getting started
Base URL: https://data.v4mos.ai. Every call requires your API credentials and the organizationId — see Authentication.
1. Find the store identifier
Call GET /v1/products/integrations?organizationId=…. Each store of the organization comes with a sourceIntegrationId: that is the value you send as integrationId to every other Products endpoint.
Why not the VTEX App Key?
The App Key is a credential: it can be rotated at any time, and the same key can be linked to more than one store or organization. The sourceIntegrationId identifies the store in a stable way in V4MOS.AI data, so it is what the endpoints expect.
2. List the products
GET /v1/products returns one row per product with the period's metrics, paginated and sortable.
3. Get category totals
GET /v1/products/categories returns the totals of each category for the same selection as the listing.
4. Drill into one product
GET /v1/products/{productId} returns the full detail of one product, and GET /v1/products/{productId}/series returns its daily or weekly sales over time.
curl "https://data.v4mos.ai/v1/products?organizationId=YOUR_ORGANIZATION_ID&integrationId=YOUR_SOURCE_INTEGRATION_ID&dateFrom=2026-09-01&dateTo=2026-09-30&sortBy=netSales&pageSize=50" \
-H "x-client-id: your_client_id" \
-H "x-client-secret: your_client_secret"| Question | Endpoint |
|---|---|
| Which stores can I query, and which identifier do I use? | GET /v1/products/integrations |
| Which products sell the most, at what margin, with how much stock? | GET /v1/products |
| How is each category performing? | GET /v1/products/categories |
| Everything about one product: funnel, repurchase, baskets, coupons, channels | GET /v1/products/{productId} |
| How did a product's sales evolve day by day or week by week? | GET /v1/products/{productId}/series |
Metrics
Monetary values are in the store currency and exclude shipping, taxes, services, media, fees and reverse logistics. Rates and shares are decimals: 0.3 means 30%.
| Field | What it is | How it is calculated | Unit |
|---|---|---|---|
grossSales | Gross sales | Invoiced value of the product's items in the period, before returns | Currency |
netSales | Net sales | Gross sales minus recorded returns | Currency |
grossUnits / netUnits | Units sold | Invoiced units / invoiced units minus returned units | Units |
averageTicket | Average ticket | Net sales ÷ orders containing the product | Currency |
cogs | COGS | Cost of the units the customer kept (did not return) | Currency |
marginTotal | Margin | Net sales − COGS. Product margin before other variable expenses | Currency |
marginAverage | Margin % | Margin ÷ net sales | Decimal (0–1) |
newCustomers | New customers | Customers whose first purchase in the store happened in the period and included the product | Customers |
recurringCustomers | Returning customers | Customers who bought the product in the period and had bought from the store before | Customers |
entryCustomers | Entry customers | Customers who "entered" the store through this product in the period (first purchase containing the product) | Customers |
matureEntryCustomers | Mature entry customers | Entry customers who have completed 180 days since their first purchase | Customers |
ltv180 | 180-day LTV | Average net revenue over 180 days of mature entry customers | Currency |
repurchaseRate180 | 180-day repurchase | Share of mature entry customers who placed another order (of any product) within 180 days | Decimal (0–1) |
ltvToDate | LTV to date | Average net revenue accumulated so far by all entry customers, mature or not | Currency |
repurchaseRateToDate | Repurchase to date | Share of all entry customers who have already bought again | Decimal (0–1) |
cohortElapsedDaysAverage | Average cohort age | Average days since first purchase of the entry customers (capped at 180) | Days |
availableStock | Available stock | Current stock minus reserved stock, summed over the product's SKUs | Units |
velocity30d | Sales velocity | Units sold per day over the last 30 complete days (independent of the selected period) | Units/day |
daysOfStock | Days of stock | Available stock ÷ sales velocity | Days |
returnRate | Return rate | Returned units ÷ units sold | Decimal (0–1) |
Period and freshness
- Sales, customers and margin follow the
dateFrom–dateToperiod. - Catalog, status and stock show the latest snapshot, whatever the period.
asOfis the date and time the source data was last updated. Show it next to the numbers so readers know how recent they are.inventoryFreshnesssays whether stock is up to date (fresh), more than 24 hours old (stale) or was never read (missing).inventoryUpdatedAtholds the last reading time.
LTV and repurchase: the 180-day entry cohort
A customer "enters" through a product when their first purchase in the store contains that product. To measure LTV and repurchase fairly, each customer needs 180 days since that first purchase — until then they are still "maturing".
ltv180andrepurchaseRate180use only customers who have completed 180 days. Read them together withmatureEntryCustomers/entryCustomersto know how many customers stand behind the number.cohortStatesums it up:mature(everyone mature),partial(some mature),maturing(nobody mature yet) orno_base(no entry customers in the period).- For recent cohorts, use
ltvToDateandrepurchaseRateToDate, which count every customer. They grow over time, so always compare them alongsidecohortElapsedDaysAverage.
When a value is N/D
null means N/D (not available) — never zero. The ndReasons object explains why, and coverage shows how complete the source data was (from 0 to 1):
| Situation | Where it shows |
|---|---|
Margin and COGS withheld because some unit is missing its sale value (incomplete_gross_value) or its cost (incomplete_cost) | ndReasons.margin, coverage.grossValue, coverage.cost |
Margin % with no net sales in the period (no_net_sales) | ndReasons.averageMargin |
Days of stock with no recent sales (no_movement) or stock that is unmonitored/unlimited | ndReasons.daysOfStock, inventoryState, coverage.inventory |
LTV and repurchase with no mature customers (no_base, maturing) | ndReasons.ltv180, ndReasons.repurchaseRate180, coverage.matureCohort |
Return rate with no sales (no_sales_base) or in a store that has never recorded a return (no_returns_coverage) | ndReasons.returnRate |
Filters and sorting
GET /v1/products and GET /v1/products/categories accept the same filters:
- Search:
searchfinds products whose name contains the text (case-insensitive, up to 120 characters). - Status:
status=active,inactiveorall(default). - Categories:
categoryIdsaccepts up to 50 categories, comma-separated (categoryIds=1,12) or repeated (categoryIds=1&categoryIds=12). A product matches if it belongs to any of them, including through a subcategory.categoryId(a single category) still works, but prefercategoryIds. - Metric ranges (inclusive bounds):
minNetSales/maxNetSales,minMarginTotal/maxMarginTotal,minMarginAverage/maxMarginAverageandminAvailableStock/maxAvailableStock. Margin % is a decimal:minMarginAverage=0.3means "margin of at least 30%".mincannot be greater thanmax. - Pagination (listing only):
page(default1) andpageSize(default25, max100). The response includespagination.totalandpagination.totalPages. - Sorting (listing only):
sortBywith one of these fields —productName,grossSales,netSales(default),netUnits,averageTicket,marginAverage,marginTotal,newCustomers,recurringCustomers,ltv180,repurchaseRate180,ltvToDate,repurchaseRateToDate,entryCustomers,matureEntryCustomers,availableStock,daysOfStock,returnRate,updatedAt— andsortOrder=asc|desc(defaultdesc). N/D values always come last. - Columns:
columnstakes a comma-separated list of metrics (for examplecolumns=netSales,marginTotal,daysOfStock) and returns only those. Identity, status, coverage and N/D reasons are always returned. - Categories in the listing: the listing also returns category totals in
categories. If you already callGET /v1/products/categories, sendincludeCategories=falseto get the listing faster.
N/D never falls inside a range
A product whose margin or stock is N/D never matches a filter on that metric. For example, minMarginTotal=0 excludes products whose margin is withheld for missing costs.
Important rules
When margin is withheld
Margin (marginTotal, marginAverage) and COGS (cogs) are only returned when every unit of the product in the period has both a sale value and a cost. If some units are missing their cost, the summed cost would be lower than the real one and the margin would look better than it is — so the value is N/D, with the reason in ndReasons.margin. To unlock margin, complete the product cost registration in your e-commerce platform.
Category totals vs. page totals
The totals in categories cover all filtered products, not only the current page, and stay correct on any page or sort order. Orders and customers are recounted (an order with several products of the category counts once) and rates are recomputed from the totals. With metric ranges, each category only adds up the products inside the ranges; categories with no product in range are omitted.
Do not add up different levels
Adding up the rows of a page does not reproduce a category total. And a department already includes its subcategories: use depth and parentCategoryId to avoid counting the same value twice.
Product detail works in blocks
GET /v1/products/{productId} is made of independent blocks — summary, funnel, entry cohort, bought together, retention, purchase frequency, bought after, coupons and acquisition channels. Each block has its own state and reason:
- The response is
200whenever the product exists for the organization and store. One block can come back withstate: "error"or with no data while the others work normally — handle each block on its own. 404means the product does not exist in that store;400means a parameter is invalid.- Not every block uses the requested period.
periodAppliedsays whether the block followeddateFrom/dateTo, andwindowreports the range actually measured. Bought together and bought after use the last 12 months; the entry cohort uses the product's lastcohortWindowMonthsentry months. - Use each block's own
asOfnext to its numbers. The rootasOfis the oldest among the blocks.
Data freshness
Responses can take up to 5 minutes to reflect new data (up to 30 minutes for the sales series). A block that came back as error is not reused: a new call tries to fetch it again.
Do not monitor integration health by HTTP status alone
Even if every source fails, the detail answers 200 with every block in error. Check each block's state.
Entry cohorts overlap across products
A customer whose first purchase held several products enters the cohort of each of them. So cohort rows (and also entryCustomers, newCustomers, ltv180 and the like) must not be summed across products — the same customer would be counted more than once. The overlappingRows: true field in the cohort block is a reminder of this rule. The same applies to bought together and bought after: one order feeds several rows.
Returns are a floor
Returns registered after an order is finalized may not be captured. So returnRate is a minimum (the real return rate can be higher), and net sales and margin can be slightly above the real values. The detail's summary block states this in summary.returnMeasurement.
Common parameters
| Parameter | Required | Description |
|---|---|---|
organizationId | Yes | Organization UUID. Required on every endpoint. |
integrationId | Yes (except on /integrations) | The sourceIntegrationId returned by GET /v1/products/integrations. Never the VTEX App Key. |
dateFrom / dateTo | No | Period as YYYY-MM-DD. Default: the last 30 days up to today. At most 365 days, no future dates, and dateFrom cannot be after dateTo. |
productId | Yes (in the path, single-product endpoints) | The productId exactly as returned by the listing. IDs with slashes (such as Shopify ones) must be URL-encoded. |
Available endpoints
- getList VTEX stores available to the products dashboard
/v1/products/integrationsLists the organization's stores with the identifier the other Products endpoints expect as integrationId. - getList consolidated product categories
/v1/products/categoriesReturns the totals of each category (departments and subcategories) for the same selection as GET /v1/products: store, period, search,… - getList consolidated products
/v1/productsReturns one row per product with the period's sales, margin, stock, returns and repurchase metrics, plus category totals in categories. - getDaily or weekly sales series of one product
/v1/products/{productId}/seriesReturns how a product's net sales, units, orders, cost and margin evolved, day by day (granularity=day) or in 7-day buckets… - getProduct detail with independently degrading blocks
/v1/products/{productId}Returns everything about one product in independent blocks: a metrics summary compared with the previous period, GA4 funnel, entry cohort,…