Products

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"
QuestionEndpoint
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, channelsGET /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%.

FieldWhat it isHow it is calculatedUnit
grossSalesGross salesInvoiced value of the product's items in the period, before returnsCurrency
netSalesNet salesGross sales minus recorded returnsCurrency
grossUnits / netUnitsUnits soldInvoiced units / invoiced units minus returned unitsUnits
averageTicketAverage ticketNet sales ÷ orders containing the productCurrency
cogsCOGSCost of the units the customer kept (did not return)Currency
marginTotalMarginNet sales − COGS. Product margin before other variable expensesCurrency
marginAverageMargin %Margin ÷ net salesDecimal (0–1)
newCustomersNew customersCustomers whose first purchase in the store happened in the period and included the productCustomers
recurringCustomersReturning customersCustomers who bought the product in the period and had bought from the store beforeCustomers
entryCustomersEntry customersCustomers who "entered" the store through this product in the period (first purchase containing the product)Customers
matureEntryCustomersMature entry customersEntry customers who have completed 180 days since their first purchaseCustomers
ltv180180-day LTVAverage net revenue over 180 days of mature entry customersCurrency
repurchaseRate180180-day repurchaseShare of mature entry customers who placed another order (of any product) within 180 daysDecimal (0–1)
ltvToDateLTV to dateAverage net revenue accumulated so far by all entry customers, mature or notCurrency
repurchaseRateToDateRepurchase to dateShare of all entry customers who have already bought againDecimal (0–1)
cohortElapsedDaysAverageAverage cohort ageAverage days since first purchase of the entry customers (capped at 180)Days
availableStockAvailable stockCurrent stock minus reserved stock, summed over the product's SKUsUnits
velocity30dSales velocityUnits sold per day over the last 30 complete days (independent of the selected period)Units/day
daysOfStockDays of stockAvailable stock ÷ sales velocityDays
returnRateReturn rateReturned units ÷ units soldDecimal (0–1)

Period and freshness

  • Sales, customers and margin follow the dateFrom–dateTo period.
  • Catalog, status and stock show the latest snapshot, whatever the period.
  • asOf is the date and time the source data was last updated. Show it next to the numbers so readers know how recent they are.
  • inventoryFreshness says whether stock is up to date (fresh), more than 24 hours old (stale) or was never read (missing). inventoryUpdatedAt holds 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".

  • ltv180 and repurchaseRate180 use only customers who have completed 180 days. Read them together with matureEntryCustomers / entryCustomers to know how many customers stand behind the number.
  • cohortState sums it up: mature (everyone mature), partial (some mature), maturing (nobody mature yet) or no_base (no entry customers in the period).
  • For recent cohorts, use ltvToDate and repurchaseRateToDate, which count every customer. They grow over time, so always compare them alongside cohortElapsedDaysAverage.

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

SituationWhere 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/unlimitedndReasons.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: search finds products whose name contains the text (case-insensitive, up to 120 characters).
  • Status: status=active, inactive or all (default).
  • Categories: categoryIds accepts 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 prefer categoryIds.
  • Metric ranges (inclusive bounds): minNetSales/maxNetSales, minMarginTotal/maxMarginTotal, minMarginAverage/maxMarginAverage and minAvailableStock/maxAvailableStock. Margin % is a decimal: minMarginAverage=0.3 means "margin of at least 30%". min cannot be greater than max.
  • Pagination (listing only): page (default 1) and pageSize (default 25, max 100). The response includes pagination.total and pagination.totalPages.
  • Sorting (listing only): sortBy with one of these fields — productName, grossSales, netSales (default), netUnits, averageTicket, marginAverage, marginTotal, newCustomers, recurringCustomers, ltv180, repurchaseRate180, ltvToDate, repurchaseRateToDate, entryCustomers, matureEntryCustomers, availableStock, daysOfStock, returnRate, updatedAt — and sortOrder=asc|desc (default desc). N/D values always come last.
  • Columns: columns takes a comma-separated list of metrics (for example columns=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 call GET /v1/products/categories, send includeCategories=false to 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 200 whenever the product exists for the organization and store. One block can come back with state: "error" or with no data while the others work normally — handle each block on its own.
  • 404 means the product does not exist in that store; 400 means a parameter is invalid.
  • Not every block uses the requested period. periodApplied says whether the block followed dateFrom/dateTo, and window reports the range actually measured. Bought together and bought after use the last 12 months; the entry cohort uses the product's last cohortWindowMonths entry months.
  • Use each block's own asOf next to its numbers. The root asOf is 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

ParameterRequiredDescription
organizationIdYesOrganization UUID. Required on every endpoint.
integrationIdYes (except on /integrations)The sourceIntegrationId returned by GET /v1/products/integrations. Never the VTEX App Key.
dateFrom / dateToNoPeriod 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.
productIdYes (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

On this page